From fc004a9b1919bfc6f2a718ad7e2446046a657a23 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Fri, 5 Apr 2024 08:17:39 +0300 Subject: [PATCH 1/3] Migrate to GitHub Actions --- .github/workflows/update.yml | 31 ++++++++++++++++++++++++++ .travis.yml | 43 ------------------------------------ justfile | 4 ++++ 3 files changed, 35 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/update.yml delete mode 100644 .travis.yml create mode 100644 justfile diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml new file mode 100644 index 00000000..637ff331 --- /dev/null +++ b/.github/workflows/update.yml @@ -0,0 +1,31 @@ +name: Update Site + +on: + push: + branches: [master, actions-test] + +jobs: + update-site: + runs-on: ubuntu-latest + + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the + # added or changed files to the repository. + contents: write + + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/install-action@v2 + with: + tool: just@1.16.0 + + - name: Setup tools + run: sudo apt-get install -y asciidoc + + + - name: Generate XML from Asciidoc + run: just generate + # Other steps that change files in the repository + + # Commit all changed files back to the repository + - uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ee74ca27..00000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: c -sudo: false - -cache: - directories: - - $HOME/.stack - -addons: - apt: - packages: - - asciidoc - - ghc-7.10.3 - sources: - - hvr-ghc - -install: -- export PATH=$HOME/.local/bin:/opt/ghc/7.10.3/bin:$PATH -- mkdir -p $HOME/.local/bin -- curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64-static.tar.gz | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' - -script: -- rm -f book/generated-xml/* -- book/tools/generate.sh -- book/tools/validate.hs -- git diff -- | - if [ $TRAVIS_PULL_REQUEST != false ] - then - echo Not pushing diff for a pull request - elif [ -n "$(git status --porcelain)" ] - then - mkdir -p $HOME/.ssh - openssl aes-256-cbc -K $encrypted_92ac0cbbb1f3_key -iv $encrypted_92ac0cbbb1f3_iv -in id_rsa.enc -out id_rsa -d - mv id_rsa $HOME/.ssh - chmod 400 $HOME/.ssh/id_rsa - git config --global user.email "michael+travis@snoyman.com" - git config --global user.name "Travis job for yesodweb/yesodweb.com-content" - git add -A - git commit -m "Travis auto-generate XML files, $(date --utc --iso=sec)" - git push git@github.com:yesodweb/yesodweb.com-content.git HEAD:$TRAVIS_BRANCH - else - echo No changes present - fi diff --git a/justfile b/justfile new file mode 100644 index 00000000..6f112f88 --- /dev/null +++ b/justfile @@ -0,0 +1,4 @@ +generate: + rm -rf book/generated-xml + ./book/tools/generate.sh + ./book/tools/validate.hs From 99015c8d541571bac37be079d81d964070698516 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Fri, 5 Apr 2024 08:48:44 +0300 Subject: [PATCH 2/3] Include generated files --- justfile | 18 +- public/404.html | 12 + public/_redirects | 3 + public/assets/baseless-assertion.jpg | Bin 0 -> 83229 bytes public/assets/future-work-warp/eventlog.png | Bin 0 -> 44254 bytes .../assets/http2-priority/priority-bench.png | Bin 0 -> 108393 bytes .../measuring-warp/measuring-warp-graph-1.png | Bin 0 -> 25565 bytes .../measuring-warp/measuring-warp-graph-2.png | Bin 0 -> 23262 bytes public/assets/new-warp/result.png | Bin 0 -> 25623 bytes public/assets/server-push/nopush.png | Bin 0 -> 197541 bytes public/assets/server-push/push.png | Bin 0 -> 189606 bytes public/assets/skinning-conduits/conduit.png | Bin 0 -> 9019 bytes public/assets/skinning-conduits/source-io.png | Bin 0 -> 3875 bytes public/assets/skinning-conduits/source.png | Bin 0 -> 7785 bytes public/assets/streaming/pure-exceptions.png | Bin 0 -> 4513 bytes .../assets/vegito-benchmark-2016-02-28.html | 1016 ++++++++++ .../vegito-benchmark-2016-02-28.html.html | 1016 ++++++++++ public/assets/warp-posa/1.png | Bin 0 -> 16396 bytes public/assets/warp-posa/2.png | Bin 0 -> 26489 bytes public/assets/warp-posa/3.png | Bin 0 -> 23157 bytes public/assets/warp-posa/4.png | Bin 0 -> 21230 bytes .../assets/warp-posa/bytestring-splicing.png | Bin 0 -> 31832 bytes public/assets/warp-posa/bytestring.png | Bin 0 -> 22183 bytes public/assets/warp-posa/eventlog.png | Bin 0 -> 44254 bytes .../assets/warp-posa/middleware-michael.png | Bin 0 -> 15518 bytes public/assets/warp-posa/middleware.png | Bin 0 -> 11743 bytes public/assets/warp-posa/multi-workers.png | Bin 0 -> 20522 bytes public/assets/warp-posa/tcpdump.png | Bin 0 -> 31849 bytes public/assets/warp-posa/timeout.png | Bin 0 -> 22051 bytes public/assets/warp-posa/wai.png | Bin 0 -> 15529 bytes public/assets/warp-posa/warp.png | Bin 0 -> 47492 bytes .../assets/yesod-fay-js/fay-error-message.png | Bin 0 -> 12079 bytes public/blog.html | 23 + .../01/efficient-yaml-parsing-followup.html | 52 + .../blog/2010/01/efficient-yaml-parsing.html | 62 + public/blog/2010/01/new-blog-system.html | 20 + public/blog/2010/01/with-monadio.html | 52 + .../blog/2010/02/request-body-interfaces.html | 105 + public/blog/2010/02/simpler-is-better.html | 30 + public/blog/2010/03/persistence.html | 29 + public/blog/2010/03/persistent-plugs.html | 26 + .../blog/2010/03/tweedle-beetle-battle.html | 420 ++++ public/blog/2010/04/hamlet-is-born.html | 21 + public/blog/2010/04/pedantic.html | 56 + .../2010/04/whats-cooking-with-yesod.html | 30 + public/blog/2010/04/yesod-mini-tutorial.html | 50 + public/blog/2010/05/bigtable-benchmarks.html | 44 + public/blog/2010/05/hamlet-version-0-2.html | 35 + .../blog/2010/05/laying-the-foundation.html | 42 + .../blog/2010/05/migrating-to-yesod-0-2.html | 42 + public/blog/2010/05/persistence-thoughts.html | 96 + .../blog/2010/05/persistent-blog-example.html | 33 + public/blog/2010/05/persistent-progress.html | 86 + .../blog/2010/05/really-type-safe-urls.html | 46 + public/blog/2010/05/status-update.html | 44 + public/blog/2010/05/yesod-0-2-0-released.html | 29 + .../2010/06/first-persistent-new-yesod.html | 33 + public/blog/2010/06/formlets-meet-hamlet.html | 51 + .../06/fringe-benefits-typesafe-urls.html | 46 + .../blog/2010/06/hamlet-and-persistent.html | 18 + public/blog/2010/06/handler-monad.html | 84 + .../optimizing-hamlet-minor-correction.html | 22 + public/blog/2010/06/optimizing-hamlet.html | 42 + public/blog/2010/06/restful-content.html | 53 + public/blog/2010/06/some-plumbing-fixes.html | 49 + public/blog/2010/07/database-migrations.html | 47 + .../blog/2010/07/first-stab-at-widgets.html | 27 + public/blog/2010/07/sessions.html | 47 + public/blog/2010/07/wai-0-2-ideas.html | 61 + public/blog/2010/07/wai-handler-snap.html | 20 + public/blog/2010/07/yesod-0-4.html | 25 + public/blog/2010/08/orange-roster.html | 32 + .../2010/08/static-file-optimizations.html | 39 + .../blog/2010/08/typesafe-runtime-hamlet.html | 92 + public/blog/2010/08/whats-in-a-hamlet.html | 62 + public/blog/2010/08/yesod-0-5.html | 32 + public/blog/2010/09/adding-varnish.html | 35 + .../2010/09/announcing-http-enumerator.html | 21 + .../2010/09/enumerators-tutorial-part-1.html | 227 +++ .../blog/2010/09/modular-authentication.html | 27 + public/blog/2010/09/wai-handler-devel.html | 22 + public/blog/2010/09/yammer-screencast.html | 21 + public/blog/2010/09/yesod-0-5-1.html | 62 + .../2010/09/yo-dawg-template-haskell.html | 19 + .../2010/10/announcing-zlib-bindings.html | 55 + public/blog/2010/10/custom-forms.html | 77 + .../2010/10/enumerators-tutorial-part-2.html | 184 ++ .../2010/10/enumerators-tutorial-part-3.html | 152 ++ .../blog/2010/10/forms-chapter-written.html | 58 + .../blog/2010/10/haskellers-and-openid.html | 48 + ...ertible-monads-exceptions-allocations.html | 209 ++ public/blog/2010/10/yesod-0-6.html | 30 + .../atomic-field-modification-persistent.html | 42 + .../blog/2010/11/javascript-minification.html | 37 + .../2010/11/new-book-comments-system.html | 23 + public/blog/2010/11/please-break-yesod.html | 24 + public/blog/2010/11/wishlist.html | 42 + public/blog/2010/11/yesod-0-6-3.html | 26 + .../2010/12/announcing-xml-enumerator.html | 68 + public/blog/2010/12/announcing-yackage.html | 46 + .../blog/2010/12/early-december-update.html | 51 + public/blog/2010/12/magic-of-yesod-part1.html | 79 + public/blog/2010/12/magic-of-yesod-part2.html | 190 ++ .../12/serendipity-wai-http-enumerator.html | 82 + public/blog/2010/12/yesod-user-survey.html | 22 + public/blog/2010/12/yesods-own-world.html | 30 + public/blog/2011/01/announcing-wai-0-3.html | 71 + public/blog/2011/01/announcing-warp.html | 96 + public/blog/2011/01/data-object-yaml.html | 65 + public/blog/2011/01/hamlet6to7.html | 47 + public/blog/2011/01/i18n-in-haskell.html | 34 + public/blog/2011/02/announcing-yesod-0-7.html | 48 + public/blog/2011/02/chapters-1-4.html | 17 + public/blog/2011/02/reverse-packdeps.html | 18 + .../2011/02/routing-changes-yesod-0-7.html | 76 + public/blog/2011/02/warp-speed-ahead.html | 39 + .../blog/2011/02/yesod-migration-guide.html | 26 + .../blog/2011/03/hamlet-lucius-widgets.html | 55 + .../03/improving-persistent-performance.html | 25 + ...minary-warp-cross-language-benchmarks.html | 27 + public/blog/2011/03/road-to-yesod-1-0.html | 66 + public/blog/2011/04/announcing-wai-0-4.html | 27 + public/blog/2011/04/announcing-yesod-0-8.html | 88 + public/blog/2011/04/beta-yesod-0-8.html | 76 + public/blog/2011/04/new-site-design.html | 17 + public/blog/2011/04/rails-can-scale.html | 18 + .../blog/2011/04/yesod-template-haskell.html | 19 + public/blog/2011/05/forms-api-decisions.html | 82 + .../blog/2011/05/introducing-yesod-wiki.html | 92 + public/blog/2011/05/non-poly-hamlet-i18n.html | 204 ++ .../2011/05/warp-article-functional-web.html | 15 + .../2011/05/yesod-for-non-haskellers.html | 101 + public/blog/2011/05/yesod-form-revamp.html | 98 + public/blog/2011/06/building-better-chm.html | 125 ++ public/blog/2011/06/cookbook-part-2.html | 215 +++ public/blog/2011/06/cookbook.html | 221 +++ .../blog/2011/06/yesod-docs-haskellwiki.html | 25 + public/blog/2011/07/browserid-support.html | 25 + public/blog/2011/07/haskell-on-heroku.html | 37 + public/blog/2011/07/syntax-highlighting.html | 15 + public/blog/2011/07/yesod-community.html | 61 + public/blog/2011/07/yesod-reorg.html | 78 + .../2011/08/0.9-rc3-and-scaffold-upgrade.html | 86 + public/blog/2011/08/announcing-yesod-0.9.html | 96 + public/blog/2011/08/monad-control.html | 340 ++++ public/blog/2011/08/new-forms-chapter.html | 504 +++++ public/blog/2011/08/pdf-book.html | 17 + .../perils-partially-powered-languages.html | 249 +++ public/blog/2011/08/persistent-0-6-0.html | 756 ++++++++ public/blog/2011/08/shakespeare.html | 570 ++++++ .../2011/08/yesod-0-9-release-candidate.html | 150 ++ public/blog/2011/08/yesod-form-overhaul.html | 142 ++ public/blog/2011/09/case-study-sphinx.html | 664 +++++++ .../blog/2011/09/limitations-of-haskell.html | 49 + public/blog/2011/09/new-book-content.html | 341 ++++ public/blog/2011/09/yesod-0.9.2.html | 33 + .../2011/10/code-generation-conversation.html | 72 + public/blog/2011/10/frameworks-drive.html | 37 + public/blog/2011/10/settings-types.html | 86 + public/blog/2011/10/xml-enumerator.html | 615 ++++++ public/blog/2011/10/yesods-monads.html | 265 +++ public/blog/2011/11/cabal-src.html | 34 + public/blog/2011/11/got-right.html | 61 + public/blog/2011/12/0-9-4-release.html | 69 + public/blog/2011/12/conduits-sink.html | 336 ++++ public/blog/2011/12/conduits.html | 375 ++++ .../2011/12/future-of-http-enumerator.html | 29 + public/blog/2011/12/please-use-packdeps.html | 27 + public/blog/2011/12/resourcet.html | 301 +++ .../blog/2011/12/variable-naming-context.html | 24 + .../blog/2011/12/yesod-scaffolded-site.html | 245 +++ public/blog/2012/01/aosa-chapter.html | 626 ++++++ public/blog/2012/01/blog-example.html | 516 +++++ public/blog/2012/01/coming-soon.html | 17 + public/blog/2012/01/conduit-changes-0-2.html | 112 ++ .../2012/01/conduit-versus-enumerator.html | 140 ++ public/blog/2012/01/conduits-buffering.html | 276 +++ public/blog/2012/01/conduits-conduits.html | 326 ++++ public/blog/2012/01/http-conduit.html | 207 ++ public/blog/2012/01/new-pet-peeve.html | 29 + .../01/persisent-0-7-yesod-0-10-beta.html | 33 + public/blog/2012/01/warp-conduits.html | 22 + public/blog/2012/01/wiki-chat-subsite.html | 343 ++++ .../blog/2012/02/answers-about-the-book.html | 24 + public/blog/2012/02/call-for-gsoc-2012.html | 27 + public/blog/2012/02/persistent-0-8.html | 58 + public/blog/2012/02/qcon-video.html | 16 + public/blog/2012/02/release-0-10.html | 45 + .../blog/2012/02/simplifying-resourcet.html | 103 + .../blog/2012/03/announcing-conduit-0-3.html | 126 ++ .../2012/03/announcing-yesod-platform.html | 51 + public/blog/2012/03/cabal-nirvana.html | 35 + .../blog/2012/03/history-of-persistence.html | 29 + .../blog/2012/03/more-powerful-conduit.html | 50 + public/blog/2012/03/more-pure-conduit.html | 45 + public/blog/2012/03/pipes-like-conduit.html | 69 + .../2012/03/shelly-for-shell-scripts.html | 59 + public/blog/2012/03/start-mezzo-haskell.html | 32 + .../03/summarizing-conduit-questions.html | 86 + public/blog/2012/04/announcing-yesod-1-0.html | 52 + public/blog/2012/04/cabal-meta.html | 32 + public/blog/2012/04/client-side.html | 132 ++ public/blog/2012/04/debugging-openid.html | 43 + public/blog/2012/04/replacing-cabal.html | 51 + public/blog/2012/04/skinning-conduits.html | 218 +++ public/blog/2012/04/working-together.html | 82 + public/blog/2012/04/yesod-js-todo.html | 106 ++ public/blog/2012/05/keter-app-deployment.html | 74 + public/blog/2012/05/keter-its-alive.html | 102 + public/blog/2012/05/next-conduit-changes.html | 87 + .../blog/2012/05/response-conduit-bugs.html | 18 + public/blog/2012/06/attoparsec-position.html | 15 + public/blog/2012/06/complicating-conduit.html | 43 + public/blog/2012/06/conduit-0-5.html | 130 ++ public/blog/2012/06/updated-conduit-docs.html | 20 + .../07/announcing-baseless-assertion.html | 15 + public/blog/2012/07/announcing-wai-1-3.html | 15 + .../2012/07/clarification-classy-prelude.html | 100 + public/blog/2012/07/classy-prelude.html | 41 + public/blog/2012/07/haskell-vm.html | 20 + public/blog/2012/07/new-logging-system.html | 15 + public/blog/2012/07/resumablesource.html | 65 + public/blog/2012/07/shelly-update.html | 47 + public/blog/2012/08/announcing-yesod-1-1.html | 79 + .../2012/08/classy-prelude-good-bad-ugly.html | 171 ++ .../08/joining-forces-advance-haskell.html | 15 + public/blog/2012/08/webinar-oreilly.html | 90 + public/blog/2012/09/building-haskell-ide.html | 77 + public/blog/2012/09/caching-fd.html | 123 ++ public/blog/2012/09/header-body.html | 53 + public/blog/2012/09/header-composer.html | 86 + public/blog/2012/09/improving-warp.html | 47 + public/blog/2012/09/project-templates.html | 121 ++ public/blog/2012/09/small-status-update.html | 65 + .../10/announcing-http-reverse-proxy.html | 82 + public/blog/2012/10/avoid-syscall.html | 63 + public/blog/2012/10/future-work-warp.html | 116 ++ public/blog/2012/10/generic-monoid.html | 101 + public/blog/2012/10/haskell-and-ci.html | 98 + public/blog/2012/10/jqplot-widget.html | 180 ++ public/blog/2012/10/keter-updates.html | 49 + public/blog/2012/10/measuring-warp.html | 16 + public/blog/2012/10/yesod-fay-js.html | 198 ++ public/blog/2012/10/yesod-pure.html | 250 +++ .../blog/2012/10/yesod-user-survey-2012.html | 19 + .../2012/11/changes-scaffolding-system.html | 63 + public/blog/2012/11/solving-cabal-hell.html | 157 ++ .../blog/2012/11/stable-vetted-hackage.html | 85 + .../2012/11/updates-julius-interpolation.html | 21 + public/blog/2012/11/warp-posa.html | 544 ++++++ .../2012/12/simplified-persistent-types.html | 80 + public/blog/2013/01/adding-css-js.html | 58 + public/blog/2013/01/meaning-of-power.html | 178 ++ public/blog/2013/01/new-goal-yesod-1-2.html | 39 + public/blog/2013/01/school-of-haskell.html | 18 + public/blog/2013/01/so-many-preludes.html | 130 ++ public/blog/2013/02/ajax-with-scaffold.html | 220 +++ .../2013/02/announce-conduit-1-0-wai-1-4.html | 34 + .../2013/02/authentication-for-testing.html | 118 ++ public/blog/2013/02/upcoming-conduit-1-0.html | 232 +++ public/blog/2013/03/big-subsite-rewrite.html | 100 + public/blog/2013/03/resourcet-overview.html | 158 ++ .../2013/03/simpler-streaming-responses.html | 254 +++ .../2013/03/yesod-1-2-cleaner-internals.html | 144 ++ .../2013/03/yesod-dispatch-version-1-2.html | 159 ++ public/blog/2013/03/yesod-platform-1-1-8.html | 20 + .../blog/2013/04/changes-to-online-book.html | 36 + .../blog/2013/04/mixin-support-in-lucius.html | 55 + public/blog/2013/04/new-chapter.html | 22 + public/blog/2013/04/persistent-1-2-out.html | 27 + .../blog/2013/04/shakespeare-typescript.html | 15 + public/blog/2013/05/yesod-1-2-released.html | 19 + public/blog/2013/06/first-11-chapters.html | 26 + .../blog/2013/07/catching-all-exceptions.html | 141 ++ public/blog/2013/07/four-more-chapters.html | 26 + .../blog/2013/07/runtime-lucius-mixins.html | 23 + .../08/exceptions-and-monad-transformers.html | 269 +++ public/blog/2013/09/classy-mono.html | 38 + .../blog/2013/09/folding-lines-conduit.html | 125 ++ .../2013/09/lens-based-classy-prelude.html | 68 + public/blog/2013/09/yesod-platform-1-2-4.html | 25 + .../blog/2013/09/zipsinks-conduit-extra.html | 35 + .../blog/2013/10/core-flaw-pipes-conduit.html | 338 ++++ .../blog/2013/10/pipes-resource-problems.html | 273 +++ .../10/prelude-replacements-libraries.html | 17 + public/blog/2013/10/segfaults.html | 29 + public/blog/2013/10/simpler-conduit-core.html | 825 ++++++++ public/blog/2013/11/wai-2-http-client.html | 51 + .../2013/12/announcing-wai-2-http-client.html | 44 + public/blog/2013/12/book-updated-for-1-2.html | 23 + public/blog/2013/12/book-updates-1-2.html | 28 + .../2014/01/announcing-resource-monad.html | 50 + .../01/conduit-transformer-exception.html | 82 + .../2014/01/initializing-foundation-data.html | 182 ++ .../2014/01/lens-generator-persistent.html | 80 + public/blog/2014/01/new-fast-logger.html | 50 + .../01/psa-please-use-yesod-platform.html | 15 + public/blog/2014/01/st-monad-conduit.html | 144 ++ .../blog/2014/01/update-st-monad-conduit.html | 27 + .../blog/2014/02/announce-mono-chunked.html | 84 + public/blog/2014/02/conduit-zip-source.html | 60 + public/blog/2014/02/ideas-pipes-parse.html | 309 +++ .../blog/2014/02/module-name-conflicts.html | 37 + public/blog/2014/02/new-warp.html | 126 ++ .../03/efficient-directory-traversals.html | 40 + public/blog/2014/03/gsoc-proposals.html | 32 + .../blog/2014/03/network-conduit-async.html | 174 ++ .../blog/2014/03/package-consolidation.html | 105 + public/blog/2014/03/wai-yesod-websockets.html | 98 + .../blog/2014/04/consolidation-progress.html | 35 + public/blog/2014/04/disemboweling-wai.html | 71 + public/blog/2014/04/proposal-changes-pvp.html | 123 ++ .../blog/2014/05/exceptions-cont-monads.html | 110 ++ .../05/foldable-mapm-maybe-recursive.html | 102 + public/blog/2014/05/wai-3-0-alpha.html | 64 + .../2014/06/evil-conditional-compile.html | 95 + .../blog/2014/06/exceptions-transformers.html | 78 + public/blog/2014/06/google-email2.html | 26 + .../07/reading-files-proc-filesystem.html | 61 + .../2014/07/rfc-new-data-conduit-process.html | 162 ++ .../blog/2014/08/announcing-auto-update.html | 116 ++ .../blog/2014/08/announcing-persistent-2.html | 44 + .../2014/08/deprecating-yesod-platform.html | 53 + public/blog/2014/09/announcing-yesod-1-4.html | 51 + .../09/clarification-previous-blog-post.html | 28 + .../09/misassigned-credit-for-conduit.html | 22 + public/blog/2014/09/persistent-2.1-rc.html | 19 + .../blog/2014/09/persistent-2.1-released.html | 46 + public/blog/2014/09/planning-yesod-1-4.html | 48 + .../09/woes-multiple-package-versions.html | 114 ++ public/blog/2014/10/classy-base-prelude.html | 63 + public/blog/2014/10/slides-conduit-ghcjs.html | 23 + public/blog/2014/10/updating-auto-update.html | 97 + public/blog/2014/10/yesod-gitrepo.html | 67 + .../blog/2014/11/cabal-sandbox-stackage.html | 67 + public/blog/2014/11/case-for-curation.html | 104 + .../blog/2014/12/hackage-docs-live-again.html | 37 + .../2014/12/minimal-yesod-multifile-site.html | 27 + .../blog/2014/12/use-stackage-for-docs.html | 38 + .../blog/2014/12/yesods-new-scaffolding.html | 49 + public/blog/2015/01/polyconf-2015.html | 25 + .../2015/02/awesome-haskell-community.html | 70 + .../2015/02/brittle-haskell-toolchain.html | 104 + .../03/designing-apis-for-extensibility.html | 97 + .../2015/04/announcing-stackage-update.html | 33 + .../2015/05/deprecating-system-filepath.html | 43 + .../cabals-does-not-exist-error-message.html | 33 + .../blog/2015/06/cleaning-up-warp-apis.html | 24 + .../2015/06/stack-support-yesod-devel.html | 43 + public/blog/2015/07/http2.html | 45 + .../2015/07/s3-hackage-mirror-travis.html | 56 + public/blog/2015/07/yesod-devel.html | 18 + public/blog/2015/07/yesod-table.html | 124 ++ public/blog/2015/08/ssl-server-test.html | 139 ++ .../2015/08/thoughts-on-documentation.html | 69 + .../blog/2015/09/new-runtime-hamlet-api.html | 51 + public/blog/2015/09/true-root-pvp-debate.html | 69 + .../10/beginner-friendly-code-and-apis.html | 80 + public/blog/2015/10/resurrecting-servius.html | 30 + public/blog/2015/10/using-wais-vault.html | 149 ++ .../12/yesod-hosting-docker-kubernetes.html | 171 ++ .../2016/01/auto-generate-docbook-xml.html | 121 ++ .../2016/02/first-class-stream-fusion.html | 152 ++ ...hy-i-prefer-typeclass-based-libraries.html | 158 ++ public/blog/2016/04/fixing-monad-either.html | 66 + public/blog/2016/04/split-db.html | 60 + .../2016/04/stackifying-the-cookbook.html | 39 + .../are-unused-import-warnings-harmful.html | 45 + public/blog/2016/07/http2-server-push.html | 67 + .../07/new-http-client-mono-traversable.html | 65 + .../2016/09/better-ci-yesod-scaffoldings.html | 54 + .../blog/2016/11/new-yesod-devel-server.html | 73 + .../2016/11/use-mysql-safely-in-yesod.html | 30 + public/blog/2017/02/changes-yesods-ci.html | 31 + .../2017/06/updated-yesod-scaffolding.html | 33 + public/blog/2017/11/mega-sdist.html | 107 ++ .../01/upcoming-yesod-breaking-changes.html | 98 + .../2019/02/deprecating-googleemail2.html | 30 + public/blog/2019/12/bookportuguese.html | 30 + public/blog/2020/05/blogannouncement.html | 15 + public/blog/2021/02/chatwisely-intro.html | 23 + public/book-1.1.html | 160 ++ .../authentication-and-authorization.html | 614 ++++++ public/book-1.1/basics.html | 387 ++++ public/book-1.1/blog-example-advanced.html | 546 ++++++ public/book-1.1/case-study-sphinx.html | 773 ++++++++ public/book-1.1/conduits.html | 1685 +++++++++++++++++ public/book-1.1/creating-a-subsite.html | 153 ++ public/book-1.1/deploying-your-webapp.html | 364 ++++ public/book-1.1/forms.html | 961 ++++++++++ public/book-1.1/haskell.html | 403 ++++ public/book-1.1/http-conduit.html | 311 +++ public/book-1.1/internationalization.html | 384 ++++ public/book-1.1/introduction.html | 177 ++ public/book-1.1/json-web-service.html | 206 ++ public/book-1.1/monad-control.html | 438 +++++ public/book-1.1/persistent.html | 1301 +++++++++++++ public/book-1.1/restful-content.html | 354 ++++ public/book-1.1/routing-and-handlers.html | 510 +++++ .../scaffolding-and-the-site-template.html | 469 +++++ public/book-1.1/sessions.html | 431 +++++ public/book-1.1/settings-types.html | 177 ++ public/book-1.1/shakespearean-templates.html | 932 +++++++++ .../book-1.1/web-application-interface.html | 203 ++ public/book-1.1/widgets.html | 494 +++++ public/book-1.1/wiki-chat-example.html | 423 +++++ public/book-1.1/xml.html | 782 ++++++++ public/book-1.1/yesod-typeclass.html | 547 ++++++ public/book-1.1/yesods-monads.html | 384 ++++ public/book-1.2.html | 188 ++ .../authentication-and-authorization.html | 673 +++++++ public/book-1.2/basics.html | 432 +++++ public/book-1.2/blog-example-advanced.html | 499 +++++ public/book-1.2/case-study-sphinx.html | 786 ++++++++ public/book-1.2/conduits.html | 111 ++ public/book-1.2/creating-a-subsite.html | 223 +++ public/book-1.2/deploying-your-webapp.html | 489 +++++ public/book-1.2/environment-variables.html | 172 ++ public/book-1.2/forms.html | 1113 +++++++++++ public/book-1.2/haskell.html | 462 +++++ public/book-1.2/http-conduit.html | 339 ++++ .../initializing-foundation-data.html | 271 +++ public/book-1.2/internationalization.html | 412 ++++ public/book-1.2/introduction.html | 244 +++ public/book-1.2/json-web-service.html | 222 +++ public/book-1.2/monad-control.html | 451 +++++ public/book-1.2/persistent.html | 1514 +++++++++++++++ public/book-1.2/restful-content.html | 623 ++++++ public/book-1.2/route-attributes.html | 210 ++ public/book-1.2/routing-and-handlers.html | 756 ++++++++ .../scaffolding-and-the-site-template.html | 503 +++++ public/book-1.2/sessions.html | 459 +++++ public/book-1.2/settings-types.html | 196 ++ public/book-1.2/shakespearean-templates.html | 1031 ++++++++++ public/book-1.2/single-process-pubsub.html | 326 ++++ public/book-1.2/understanding-request.html | 660 +++++++ public/book-1.2/visitor-counter.html | 164 ++ .../book-1.2/web-application-interface.html | 346 ++++ public/book-1.2/widgets.html | 636 +++++++ public/book-1.2/wiki-chat-example.html | 553 ++++++ public/book-1.2/xml.html | 834 ++++++++ public/book-1.2/yesod-for-haskellers.html | 1225 ++++++++++++ public/book-1.2/yesod-typeclass.html | 670 +++++++ public/book-1.2/yesods-monads.html | 643 +++++++ public/book-1.4.html | 188 ++ .../authentication-and-authorization.html | 717 +++++++ public/book-1.4/basics.html | 468 +++++ public/book-1.4/blog-example-advanced.html | 503 +++++ public/book-1.4/case-study-sphinx.html | 791 ++++++++ public/book-1.4/creating-a-subsite.html | 226 +++ public/book-1.4/deploying-your-webapp.html | 582 ++++++ public/book-1.4/environment-variables.html | 172 ++ public/book-1.4/forms.html | 1128 +++++++++++ public/book-1.4/haskell.html | 458 +++++ public/book-1.4/http-conduit.html | 112 ++ .../initializing-foundation-data.html | 271 +++ public/book-1.4/internationalization.html | 411 ++++ public/book-1.4/introduction.html | 221 +++ public/book-1.4/json-web-service.html | 222 +++ public/book-1.4/monad-control.html | 451 +++++ public/book-1.4/persistent.html | 1594 ++++++++++++++++ public/book-1.4/restful-content.html | 601 ++++++ public/book-1.4/route-attributes.html | 324 ++++ public/book-1.4/routing-and-handlers.html | 792 ++++++++ .../scaffolding-and-the-site-template.html | 505 +++++ public/book-1.4/sessions.html | 488 +++++ public/book-1.4/settings-types.html | 196 ++ public/book-1.4/shakespearean-templates.html | 1060 +++++++++++ public/book-1.4/single-process-pubsub.html | 327 ++++ public/book-1.4/sql-joins.html | 556 ++++++ public/book-1.4/understanding-request.html | 667 +++++++ public/book-1.4/visitor-counter.html | 164 ++ .../book-1.4/web-application-interface.html | 346 ++++ public/book-1.4/widgets.html | 636 +++++++ public/book-1.4/wiki-chat-example.html | 597 ++++++ public/book-1.4/xml.html | 834 ++++++++ public/book-1.4/yesod-for-haskellers.html | 1236 ++++++++++++ public/book-1.4/yesod-typeclass.html | 674 +++++++ public/book-1.4/yesods-monads.html | 643 +++++++ public/book-1.6.html | 187 ++ .../authentication-and-authorization.html | 661 +++++++ public/book-1.6/basics.html | 466 +++++ public/book-1.6/blog-example-advanced.html | 511 +++++ public/book-1.6/case-study-sphinx.html | 790 ++++++++ public/book-1.6/creating-a-subsite.html | 225 +++ public/book-1.6/deploying-your-webapp.html | 581 ++++++ public/book-1.6/environment-variables.html | 171 ++ public/book-1.6/forms.html | 1127 +++++++++++ public/book-1.6/haskell.html | 450 +++++ public/book-1.6/http-conduit.html | 127 ++ .../initializing-foundation-data.html | 270 +++ public/book-1.6/internationalization.html | 439 +++++ public/book-1.6/introduction.html | 220 +++ public/book-1.6/json-web-service.html | 221 +++ public/book-1.6/monad-control.html | 450 +++++ public/book-1.6/persistent.html | 1653 ++++++++++++++++ public/book-1.6/restful-content.html | 600 ++++++ public/book-1.6/route-attributes.html | 321 ++++ public/book-1.6/routing-and-handlers.html | 814 ++++++++ .../scaffolding-and-the-site-template.html | 545 ++++++ public/book-1.6/sessions.html | 487 +++++ public/book-1.6/settings-types.html | 195 ++ public/book-1.6/shakespearean-templates.html | 1067 +++++++++++ public/book-1.6/single-process-pubsub.html | 326 ++++ public/book-1.6/sql-joins.html | 560 ++++++ public/book-1.6/understanding-request.html | 666 +++++++ public/book-1.6/visitor-counter.html | 163 ++ .../book-1.6/web-application-interface.html | 345 ++++ public/book-1.6/widgets.html | 635 +++++++ public/book-1.6/wiki-chat-example.html | 594 ++++++ public/book-1.6/xml.html | 833 ++++++++ public/book-1.6/yesod-for-haskellers.html | 1235 ++++++++++++ public/book-1.6/yesod-typeclass.html | 676 +++++++ public/book-1.6/yesods-monads.html | 645 +++++++ public/book.html | 187 ++ public/book/.html | 187 ++ .../authentication-and-authorization.html | 661 +++++++ public/book/basics.html | 466 +++++ public/book/blog-example-advanced.html | 511 +++++ public/book/case-study-sphinx.html | 790 ++++++++ public/book/creating-a-subsite.html | 225 +++ public/book/deploying-your-webapp.html | 581 ++++++ public/book/environment-variables.html | 171 ++ public/book/forms.html | 1127 +++++++++++ public/book/haskell.html | 450 +++++ public/book/http-conduit.html | 127 ++ public/book/initializing-foundation-data.html | 270 +++ public/book/internationalization.html | 439 +++++ public/book/introduction.html | 220 +++ public/book/json-web-service.html | 221 +++ public/book/monad-control.html | 450 +++++ public/book/persistent.html | 1653 ++++++++++++++++ public/book/restful-content.html | 600 ++++++ public/book/route-attributes.html | 321 ++++ public/book/routing-and-handlers.html | 814 ++++++++ .../scaffolding-and-the-site-template.html | 545 ++++++ public/book/sessions.html | 487 +++++ public/book/settings-types.html | 195 ++ public/book/shakespearean-templates.html | 1067 +++++++++++ public/book/single-process-pubsub.html | 326 ++++ public/book/sql-joins.html | 560 ++++++ public/book/understanding-request.html | 666 +++++++ public/book/visitor-counter.html | 163 ++ public/book/web-application-interface.html | 345 ++++ public/book/widgets.html | 635 +++++++ public/book/wiki-chat-example.html | 594 ++++++ public/book/xml.html | 833 ++++++++ public/book/yesod-for-haskellers.html | 1235 ++++++++++++ public/book/yesod-typeclass.html | 676 +++++++ public/book/yesods-monads.html | 645 +++++++ public/contributors.html | 12 + public/favicon.ico | Bin 0 -> 15086 bytes public/feed.xml | 122 ++ public/index.html | 52 + public/page/community.html | 49 + public/page/quickstart.html | 19 + public/page/screencasts.html | 148 ++ public/robots.txt | 1 + public/static/7of9.jpg | Bin 0 -> 44955 bytes .../benchmarks/2011-03-17/extra-large.png | Bin 0 -> 16446 bytes .../benchmarks/bigtable/cgipackages.png | Bin 0 -> 13856 bytes public/static/benchmarks/bigtable/fastcgi.png | Bin 0 -> 11613 bytes .../static/benchmarks/bigtable/fastcgi2.png | Bin 0 -> 12385 bytes public/static/benchmarks/bigtable/rawcgi.png | Bin 0 -> 13771 bytes .../static/benchmarks/bigtable/standalone.png | Bin 0 -> 12428 bytes public/static/benchmarks/bigtable/yesod.png | Bin 0 -> 11060 bytes public/static/benchmarks/warp03/bigtable.png | Bin 0 -> 6439 bytes public/static/benchmarks/warp03/pong.png | Bin 0 -> 6216 bytes .../static/benchmarks/warp03/static-file.png | Bin 0 -> 6913 bytes public/static/benchmarks/warp0321.png | Bin 0 -> 17546 bytes public/static/blog.png | Bin 0 -> 890 bytes public/static/js/modernizr.js | 4 + public/static/logo-home.png | Bin 0 -> 33122 bytes public/static/logo-home2-no-esod-smaller1.png | Bin 0 -> 11360 bytes public/static/logo-home2-no-esod-smaller2.png | Bin 0 -> 9461 bytes public/static/logo-home2-no-esod.png | Bin 0 -> 11013 bytes public/static/logo-home2-no-esod.psd | Bin 0 -> 65240 bytes public/static/logo-home2-no-notches.png | Bin 0 -> 23641 bytes public/static/logo-home2-no-notches.psd | Bin 0 -> 98234 bytes public/static/logo-home2.png | Bin 0 -> 23750 bytes public/static/logo-home2.psd | Bin 0 -> 95429 bytes public/static/logo-topleft.png | Bin 0 -> 17942 bytes public/static/oreilly-book.gif | Bin 0 -> 12955 bytes public/static/oreilly-the-book.gif | Bin 0 -> 12505 bytes public/static/toc-closed.png | Bin 0 -> 323 bytes public/static/toc-open.png | Bin 0 -> 247 bytes public/static/warpspeedahead.png | Bin 0 -> 335376 bytes public/testimonials.html | 12 + 588 files changed, 125653 insertions(+), 1 deletion(-) create mode 100644 public/404.html create mode 100644 public/_redirects create mode 100644 public/assets/baseless-assertion.jpg create mode 100644 public/assets/future-work-warp/eventlog.png create mode 100644 public/assets/http2-priority/priority-bench.png create mode 100644 public/assets/measuring-warp/measuring-warp-graph-1.png create mode 100644 public/assets/measuring-warp/measuring-warp-graph-2.png create mode 100644 public/assets/new-warp/result.png create mode 100644 public/assets/server-push/nopush.png create mode 100644 public/assets/server-push/push.png create mode 100644 public/assets/skinning-conduits/conduit.png create mode 100644 public/assets/skinning-conduits/source-io.png create mode 100644 public/assets/skinning-conduits/source.png create mode 100644 public/assets/streaming/pure-exceptions.png create mode 100644 public/assets/vegito-benchmark-2016-02-28.html create mode 100644 public/assets/vegito-benchmark-2016-02-28.html.html create mode 100644 public/assets/warp-posa/1.png create mode 100644 public/assets/warp-posa/2.png create mode 100644 public/assets/warp-posa/3.png create mode 100644 public/assets/warp-posa/4.png create mode 100644 public/assets/warp-posa/bytestring-splicing.png create mode 100644 public/assets/warp-posa/bytestring.png create mode 100644 public/assets/warp-posa/eventlog.png create mode 100644 public/assets/warp-posa/middleware-michael.png create mode 100644 public/assets/warp-posa/middleware.png create mode 100644 public/assets/warp-posa/multi-workers.png create mode 100644 public/assets/warp-posa/tcpdump.png create mode 100644 public/assets/warp-posa/timeout.png create mode 100644 public/assets/warp-posa/wai.png create mode 100644 public/assets/warp-posa/warp.png create mode 100644 public/assets/yesod-fay-js/fay-error-message.png create mode 100644 public/blog.html create mode 100644 public/blog/2010/01/efficient-yaml-parsing-followup.html create mode 100644 public/blog/2010/01/efficient-yaml-parsing.html create mode 100644 public/blog/2010/01/new-blog-system.html create mode 100644 public/blog/2010/01/with-monadio.html create mode 100644 public/blog/2010/02/request-body-interfaces.html create mode 100644 public/blog/2010/02/simpler-is-better.html create mode 100644 public/blog/2010/03/persistence.html create mode 100644 public/blog/2010/03/persistent-plugs.html create mode 100644 public/blog/2010/03/tweedle-beetle-battle.html create mode 100644 public/blog/2010/04/hamlet-is-born.html create mode 100644 public/blog/2010/04/pedantic.html create mode 100644 public/blog/2010/04/whats-cooking-with-yesod.html create mode 100644 public/blog/2010/04/yesod-mini-tutorial.html create mode 100644 public/blog/2010/05/bigtable-benchmarks.html create mode 100644 public/blog/2010/05/hamlet-version-0-2.html create mode 100644 public/blog/2010/05/laying-the-foundation.html create mode 100644 public/blog/2010/05/migrating-to-yesod-0-2.html create mode 100644 public/blog/2010/05/persistence-thoughts.html create mode 100644 public/blog/2010/05/persistent-blog-example.html create mode 100644 public/blog/2010/05/persistent-progress.html create mode 100644 public/blog/2010/05/really-type-safe-urls.html create mode 100644 public/blog/2010/05/status-update.html create mode 100644 public/blog/2010/05/yesod-0-2-0-released.html create mode 100644 public/blog/2010/06/first-persistent-new-yesod.html create mode 100644 public/blog/2010/06/formlets-meet-hamlet.html create mode 100644 public/blog/2010/06/fringe-benefits-typesafe-urls.html create mode 100644 public/blog/2010/06/hamlet-and-persistent.html create mode 100644 public/blog/2010/06/handler-monad.html create mode 100644 public/blog/2010/06/optimizing-hamlet-minor-correction.html create mode 100644 public/blog/2010/06/optimizing-hamlet.html create mode 100644 public/blog/2010/06/restful-content.html create mode 100644 public/blog/2010/06/some-plumbing-fixes.html create mode 100644 public/blog/2010/07/database-migrations.html create mode 100644 public/blog/2010/07/first-stab-at-widgets.html create mode 100644 public/blog/2010/07/sessions.html create mode 100644 public/blog/2010/07/wai-0-2-ideas.html create mode 100644 public/blog/2010/07/wai-handler-snap.html create mode 100644 public/blog/2010/07/yesod-0-4.html create mode 100644 public/blog/2010/08/orange-roster.html create mode 100644 public/blog/2010/08/static-file-optimizations.html create mode 100644 public/blog/2010/08/typesafe-runtime-hamlet.html create mode 100644 public/blog/2010/08/whats-in-a-hamlet.html create mode 100644 public/blog/2010/08/yesod-0-5.html create mode 100644 public/blog/2010/09/adding-varnish.html create mode 100644 public/blog/2010/09/announcing-http-enumerator.html create mode 100644 public/blog/2010/09/enumerators-tutorial-part-1.html create mode 100644 public/blog/2010/09/modular-authentication.html create mode 100644 public/blog/2010/09/wai-handler-devel.html create mode 100644 public/blog/2010/09/yammer-screencast.html create mode 100644 public/blog/2010/09/yesod-0-5-1.html create mode 100644 public/blog/2010/09/yo-dawg-template-haskell.html create mode 100644 public/blog/2010/10/announcing-zlib-bindings.html create mode 100644 public/blog/2010/10/custom-forms.html create mode 100644 public/blog/2010/10/enumerators-tutorial-part-2.html create mode 100644 public/blog/2010/10/enumerators-tutorial-part-3.html create mode 100644 public/blog/2010/10/forms-chapter-written.html create mode 100644 public/blog/2010/10/haskellers-and-openid.html create mode 100644 public/blog/2010/10/invertible-monads-exceptions-allocations.html create mode 100644 public/blog/2010/10/yesod-0-6.html create mode 100644 public/blog/2010/11/atomic-field-modification-persistent.html create mode 100644 public/blog/2010/11/javascript-minification.html create mode 100644 public/blog/2010/11/new-book-comments-system.html create mode 100644 public/blog/2010/11/please-break-yesod.html create mode 100644 public/blog/2010/11/wishlist.html create mode 100644 public/blog/2010/11/yesod-0-6-3.html create mode 100644 public/blog/2010/12/announcing-xml-enumerator.html create mode 100644 public/blog/2010/12/announcing-yackage.html create mode 100644 public/blog/2010/12/early-december-update.html create mode 100644 public/blog/2010/12/magic-of-yesod-part1.html create mode 100644 public/blog/2010/12/magic-of-yesod-part2.html create mode 100644 public/blog/2010/12/serendipity-wai-http-enumerator.html create mode 100644 public/blog/2010/12/yesod-user-survey.html create mode 100644 public/blog/2010/12/yesods-own-world.html create mode 100644 public/blog/2011/01/announcing-wai-0-3.html create mode 100644 public/blog/2011/01/announcing-warp.html create mode 100644 public/blog/2011/01/data-object-yaml.html create mode 100644 public/blog/2011/01/hamlet6to7.html create mode 100644 public/blog/2011/01/i18n-in-haskell.html create mode 100644 public/blog/2011/02/announcing-yesod-0-7.html create mode 100644 public/blog/2011/02/chapters-1-4.html create mode 100644 public/blog/2011/02/reverse-packdeps.html create mode 100644 public/blog/2011/02/routing-changes-yesod-0-7.html create mode 100644 public/blog/2011/02/warp-speed-ahead.html create mode 100644 public/blog/2011/02/yesod-migration-guide.html create mode 100644 public/blog/2011/03/hamlet-lucius-widgets.html create mode 100644 public/blog/2011/03/improving-persistent-performance.html create mode 100644 public/blog/2011/03/preliminary-warp-cross-language-benchmarks.html create mode 100644 public/blog/2011/03/road-to-yesod-1-0.html create mode 100644 public/blog/2011/04/announcing-wai-0-4.html create mode 100644 public/blog/2011/04/announcing-yesod-0-8.html create mode 100644 public/blog/2011/04/beta-yesod-0-8.html create mode 100644 public/blog/2011/04/new-site-design.html create mode 100644 public/blog/2011/04/rails-can-scale.html create mode 100644 public/blog/2011/04/yesod-template-haskell.html create mode 100644 public/blog/2011/05/forms-api-decisions.html create mode 100644 public/blog/2011/05/introducing-yesod-wiki.html create mode 100644 public/blog/2011/05/non-poly-hamlet-i18n.html create mode 100644 public/blog/2011/05/warp-article-functional-web.html create mode 100644 public/blog/2011/05/yesod-for-non-haskellers.html create mode 100644 public/blog/2011/05/yesod-form-revamp.html create mode 100644 public/blog/2011/06/building-better-chm.html create mode 100644 public/blog/2011/06/cookbook-part-2.html create mode 100644 public/blog/2011/06/cookbook.html create mode 100644 public/blog/2011/06/yesod-docs-haskellwiki.html create mode 100644 public/blog/2011/07/browserid-support.html create mode 100644 public/blog/2011/07/haskell-on-heroku.html create mode 100644 public/blog/2011/07/syntax-highlighting.html create mode 100644 public/blog/2011/07/yesod-community.html create mode 100644 public/blog/2011/07/yesod-reorg.html create mode 100644 public/blog/2011/08/0.9-rc3-and-scaffold-upgrade.html create mode 100644 public/blog/2011/08/announcing-yesod-0.9.html create mode 100644 public/blog/2011/08/monad-control.html create mode 100644 public/blog/2011/08/new-forms-chapter.html create mode 100644 public/blog/2011/08/pdf-book.html create mode 100644 public/blog/2011/08/perils-partially-powered-languages.html create mode 100644 public/blog/2011/08/persistent-0-6-0.html create mode 100644 public/blog/2011/08/shakespeare.html create mode 100644 public/blog/2011/08/yesod-0-9-release-candidate.html create mode 100644 public/blog/2011/08/yesod-form-overhaul.html create mode 100644 public/blog/2011/09/case-study-sphinx.html create mode 100644 public/blog/2011/09/limitations-of-haskell.html create mode 100644 public/blog/2011/09/new-book-content.html create mode 100644 public/blog/2011/09/yesod-0.9.2.html create mode 100644 public/blog/2011/10/code-generation-conversation.html create mode 100644 public/blog/2011/10/frameworks-drive.html create mode 100644 public/blog/2011/10/settings-types.html create mode 100644 public/blog/2011/10/xml-enumerator.html create mode 100644 public/blog/2011/10/yesods-monads.html create mode 100644 public/blog/2011/11/cabal-src.html create mode 100644 public/blog/2011/11/got-right.html create mode 100644 public/blog/2011/12/0-9-4-release.html create mode 100644 public/blog/2011/12/conduits-sink.html create mode 100644 public/blog/2011/12/conduits.html create mode 100644 public/blog/2011/12/future-of-http-enumerator.html create mode 100644 public/blog/2011/12/please-use-packdeps.html create mode 100644 public/blog/2011/12/resourcet.html create mode 100644 public/blog/2011/12/variable-naming-context.html create mode 100644 public/blog/2011/12/yesod-scaffolded-site.html create mode 100644 public/blog/2012/01/aosa-chapter.html create mode 100644 public/blog/2012/01/blog-example.html create mode 100644 public/blog/2012/01/coming-soon.html create mode 100644 public/blog/2012/01/conduit-changes-0-2.html create mode 100644 public/blog/2012/01/conduit-versus-enumerator.html create mode 100644 public/blog/2012/01/conduits-buffering.html create mode 100644 public/blog/2012/01/conduits-conduits.html create mode 100644 public/blog/2012/01/http-conduit.html create mode 100644 public/blog/2012/01/new-pet-peeve.html create mode 100644 public/blog/2012/01/persisent-0-7-yesod-0-10-beta.html create mode 100644 public/blog/2012/01/warp-conduits.html create mode 100644 public/blog/2012/01/wiki-chat-subsite.html create mode 100644 public/blog/2012/02/answers-about-the-book.html create mode 100644 public/blog/2012/02/call-for-gsoc-2012.html create mode 100644 public/blog/2012/02/persistent-0-8.html create mode 100644 public/blog/2012/02/qcon-video.html create mode 100644 public/blog/2012/02/release-0-10.html create mode 100644 public/blog/2012/02/simplifying-resourcet.html create mode 100644 public/blog/2012/03/announcing-conduit-0-3.html create mode 100644 public/blog/2012/03/announcing-yesod-platform.html create mode 100644 public/blog/2012/03/cabal-nirvana.html create mode 100644 public/blog/2012/03/history-of-persistence.html create mode 100644 public/blog/2012/03/more-powerful-conduit.html create mode 100644 public/blog/2012/03/more-pure-conduit.html create mode 100644 public/blog/2012/03/pipes-like-conduit.html create mode 100644 public/blog/2012/03/shelly-for-shell-scripts.html create mode 100644 public/blog/2012/03/start-mezzo-haskell.html create mode 100644 public/blog/2012/03/summarizing-conduit-questions.html create mode 100644 public/blog/2012/04/announcing-yesod-1-0.html create mode 100644 public/blog/2012/04/cabal-meta.html create mode 100644 public/blog/2012/04/client-side.html create mode 100644 public/blog/2012/04/debugging-openid.html create mode 100644 public/blog/2012/04/replacing-cabal.html create mode 100644 public/blog/2012/04/skinning-conduits.html create mode 100644 public/blog/2012/04/working-together.html create mode 100644 public/blog/2012/04/yesod-js-todo.html create mode 100644 public/blog/2012/05/keter-app-deployment.html create mode 100644 public/blog/2012/05/keter-its-alive.html create mode 100644 public/blog/2012/05/next-conduit-changes.html create mode 100644 public/blog/2012/05/response-conduit-bugs.html create mode 100644 public/blog/2012/06/attoparsec-position.html create mode 100644 public/blog/2012/06/complicating-conduit.html create mode 100644 public/blog/2012/06/conduit-0-5.html create mode 100644 public/blog/2012/06/updated-conduit-docs.html create mode 100644 public/blog/2012/07/announcing-baseless-assertion.html create mode 100644 public/blog/2012/07/announcing-wai-1-3.html create mode 100644 public/blog/2012/07/clarification-classy-prelude.html create mode 100644 public/blog/2012/07/classy-prelude.html create mode 100644 public/blog/2012/07/haskell-vm.html create mode 100644 public/blog/2012/07/new-logging-system.html create mode 100644 public/blog/2012/07/resumablesource.html create mode 100644 public/blog/2012/07/shelly-update.html create mode 100644 public/blog/2012/08/announcing-yesod-1-1.html create mode 100644 public/blog/2012/08/classy-prelude-good-bad-ugly.html create mode 100644 public/blog/2012/08/joining-forces-advance-haskell.html create mode 100644 public/blog/2012/08/webinar-oreilly.html create mode 100644 public/blog/2012/09/building-haskell-ide.html create mode 100644 public/blog/2012/09/caching-fd.html create mode 100644 public/blog/2012/09/header-body.html create mode 100644 public/blog/2012/09/header-composer.html create mode 100644 public/blog/2012/09/improving-warp.html create mode 100644 public/blog/2012/09/project-templates.html create mode 100644 public/blog/2012/09/small-status-update.html create mode 100644 public/blog/2012/10/announcing-http-reverse-proxy.html create mode 100644 public/blog/2012/10/avoid-syscall.html create mode 100644 public/blog/2012/10/future-work-warp.html create mode 100644 public/blog/2012/10/generic-monoid.html create mode 100644 public/blog/2012/10/haskell-and-ci.html create mode 100644 public/blog/2012/10/jqplot-widget.html create mode 100644 public/blog/2012/10/keter-updates.html create mode 100644 public/blog/2012/10/measuring-warp.html create mode 100644 public/blog/2012/10/yesod-fay-js.html create mode 100644 public/blog/2012/10/yesod-pure.html create mode 100644 public/blog/2012/10/yesod-user-survey-2012.html create mode 100644 public/blog/2012/11/changes-scaffolding-system.html create mode 100644 public/blog/2012/11/solving-cabal-hell.html create mode 100644 public/blog/2012/11/stable-vetted-hackage.html create mode 100644 public/blog/2012/11/updates-julius-interpolation.html create mode 100644 public/blog/2012/11/warp-posa.html create mode 100644 public/blog/2012/12/simplified-persistent-types.html create mode 100644 public/blog/2013/01/adding-css-js.html create mode 100644 public/blog/2013/01/meaning-of-power.html create mode 100644 public/blog/2013/01/new-goal-yesod-1-2.html create mode 100644 public/blog/2013/01/school-of-haskell.html create mode 100644 public/blog/2013/01/so-many-preludes.html create mode 100644 public/blog/2013/02/ajax-with-scaffold.html create mode 100644 public/blog/2013/02/announce-conduit-1-0-wai-1-4.html create mode 100644 public/blog/2013/02/authentication-for-testing.html create mode 100644 public/blog/2013/02/upcoming-conduit-1-0.html create mode 100644 public/blog/2013/03/big-subsite-rewrite.html create mode 100644 public/blog/2013/03/resourcet-overview.html create mode 100644 public/blog/2013/03/simpler-streaming-responses.html create mode 100644 public/blog/2013/03/yesod-1-2-cleaner-internals.html create mode 100644 public/blog/2013/03/yesod-dispatch-version-1-2.html create mode 100644 public/blog/2013/03/yesod-platform-1-1-8.html create mode 100644 public/blog/2013/04/changes-to-online-book.html create mode 100644 public/blog/2013/04/mixin-support-in-lucius.html create mode 100644 public/blog/2013/04/new-chapter.html create mode 100644 public/blog/2013/04/persistent-1-2-out.html create mode 100644 public/blog/2013/04/shakespeare-typescript.html create mode 100644 public/blog/2013/05/yesod-1-2-released.html create mode 100644 public/blog/2013/06/first-11-chapters.html create mode 100644 public/blog/2013/07/catching-all-exceptions.html create mode 100644 public/blog/2013/07/four-more-chapters.html create mode 100644 public/blog/2013/07/runtime-lucius-mixins.html create mode 100644 public/blog/2013/08/exceptions-and-monad-transformers.html create mode 100644 public/blog/2013/09/classy-mono.html create mode 100644 public/blog/2013/09/folding-lines-conduit.html create mode 100644 public/blog/2013/09/lens-based-classy-prelude.html create mode 100644 public/blog/2013/09/yesod-platform-1-2-4.html create mode 100644 public/blog/2013/09/zipsinks-conduit-extra.html create mode 100644 public/blog/2013/10/core-flaw-pipes-conduit.html create mode 100644 public/blog/2013/10/pipes-resource-problems.html create mode 100644 public/blog/2013/10/prelude-replacements-libraries.html create mode 100644 public/blog/2013/10/segfaults.html create mode 100644 public/blog/2013/10/simpler-conduit-core.html create mode 100644 public/blog/2013/11/wai-2-http-client.html create mode 100644 public/blog/2013/12/announcing-wai-2-http-client.html create mode 100644 public/blog/2013/12/book-updated-for-1-2.html create mode 100644 public/blog/2013/12/book-updates-1-2.html create mode 100644 public/blog/2014/01/announcing-resource-monad.html create mode 100644 public/blog/2014/01/conduit-transformer-exception.html create mode 100644 public/blog/2014/01/initializing-foundation-data.html create mode 100644 public/blog/2014/01/lens-generator-persistent.html create mode 100644 public/blog/2014/01/new-fast-logger.html create mode 100644 public/blog/2014/01/psa-please-use-yesod-platform.html create mode 100644 public/blog/2014/01/st-monad-conduit.html create mode 100644 public/blog/2014/01/update-st-monad-conduit.html create mode 100644 public/blog/2014/02/announce-mono-chunked.html create mode 100644 public/blog/2014/02/conduit-zip-source.html create mode 100644 public/blog/2014/02/ideas-pipes-parse.html create mode 100644 public/blog/2014/02/module-name-conflicts.html create mode 100644 public/blog/2014/02/new-warp.html create mode 100644 public/blog/2014/03/efficient-directory-traversals.html create mode 100644 public/blog/2014/03/gsoc-proposals.html create mode 100644 public/blog/2014/03/network-conduit-async.html create mode 100644 public/blog/2014/03/package-consolidation.html create mode 100644 public/blog/2014/03/wai-yesod-websockets.html create mode 100644 public/blog/2014/04/consolidation-progress.html create mode 100644 public/blog/2014/04/disemboweling-wai.html create mode 100644 public/blog/2014/04/proposal-changes-pvp.html create mode 100644 public/blog/2014/05/exceptions-cont-monads.html create mode 100644 public/blog/2014/05/foldable-mapm-maybe-recursive.html create mode 100644 public/blog/2014/05/wai-3-0-alpha.html create mode 100644 public/blog/2014/06/evil-conditional-compile.html create mode 100644 public/blog/2014/06/exceptions-transformers.html create mode 100644 public/blog/2014/06/google-email2.html create mode 100644 public/blog/2014/07/reading-files-proc-filesystem.html create mode 100644 public/blog/2014/07/rfc-new-data-conduit-process.html create mode 100644 public/blog/2014/08/announcing-auto-update.html create mode 100644 public/blog/2014/08/announcing-persistent-2.html create mode 100644 public/blog/2014/08/deprecating-yesod-platform.html create mode 100644 public/blog/2014/09/announcing-yesod-1-4.html create mode 100644 public/blog/2014/09/clarification-previous-blog-post.html create mode 100644 public/blog/2014/09/misassigned-credit-for-conduit.html create mode 100644 public/blog/2014/09/persistent-2.1-rc.html create mode 100644 public/blog/2014/09/persistent-2.1-released.html create mode 100644 public/blog/2014/09/planning-yesod-1-4.html create mode 100644 public/blog/2014/09/woes-multiple-package-versions.html create mode 100644 public/blog/2014/10/classy-base-prelude.html create mode 100644 public/blog/2014/10/slides-conduit-ghcjs.html create mode 100644 public/blog/2014/10/updating-auto-update.html create mode 100644 public/blog/2014/10/yesod-gitrepo.html create mode 100644 public/blog/2014/11/cabal-sandbox-stackage.html create mode 100644 public/blog/2014/11/case-for-curation.html create mode 100644 public/blog/2014/12/hackage-docs-live-again.html create mode 100644 public/blog/2014/12/minimal-yesod-multifile-site.html create mode 100644 public/blog/2014/12/use-stackage-for-docs.html create mode 100644 public/blog/2014/12/yesods-new-scaffolding.html create mode 100644 public/blog/2015/01/polyconf-2015.html create mode 100644 public/blog/2015/02/awesome-haskell-community.html create mode 100644 public/blog/2015/02/brittle-haskell-toolchain.html create mode 100644 public/blog/2015/03/designing-apis-for-extensibility.html create mode 100644 public/blog/2015/04/announcing-stackage-update.html create mode 100644 public/blog/2015/05/deprecating-system-filepath.html create mode 100644 public/blog/2015/06/cabals-does-not-exist-error-message.html create mode 100644 public/blog/2015/06/cleaning-up-warp-apis.html create mode 100644 public/blog/2015/06/stack-support-yesod-devel.html create mode 100644 public/blog/2015/07/http2.html create mode 100644 public/blog/2015/07/s3-hackage-mirror-travis.html create mode 100644 public/blog/2015/07/yesod-devel.html create mode 100644 public/blog/2015/07/yesod-table.html create mode 100644 public/blog/2015/08/ssl-server-test.html create mode 100644 public/blog/2015/08/thoughts-on-documentation.html create mode 100644 public/blog/2015/09/new-runtime-hamlet-api.html create mode 100644 public/blog/2015/09/true-root-pvp-debate.html create mode 100644 public/blog/2015/10/beginner-friendly-code-and-apis.html create mode 100644 public/blog/2015/10/resurrecting-servius.html create mode 100644 public/blog/2015/10/using-wais-vault.html create mode 100644 public/blog/2015/12/yesod-hosting-docker-kubernetes.html create mode 100644 public/blog/2016/01/auto-generate-docbook-xml.html create mode 100644 public/blog/2016/02/first-class-stream-fusion.html create mode 100644 public/blog/2016/03/why-i-prefer-typeclass-based-libraries.html create mode 100644 public/blog/2016/04/fixing-monad-either.html create mode 100644 public/blog/2016/04/split-db.html create mode 100644 public/blog/2016/04/stackifying-the-cookbook.html create mode 100644 public/blog/2016/05/are-unused-import-warnings-harmful.html create mode 100644 public/blog/2016/07/http2-server-push.html create mode 100644 public/blog/2016/07/new-http-client-mono-traversable.html create mode 100644 public/blog/2016/09/better-ci-yesod-scaffoldings.html create mode 100644 public/blog/2016/11/new-yesod-devel-server.html create mode 100644 public/blog/2016/11/use-mysql-safely-in-yesod.html create mode 100644 public/blog/2017/02/changes-yesods-ci.html create mode 100644 public/blog/2017/06/updated-yesod-scaffolding.html create mode 100644 public/blog/2017/11/mega-sdist.html create mode 100644 public/blog/2018/01/upcoming-yesod-breaking-changes.html create mode 100644 public/blog/2019/02/deprecating-googleemail2.html create mode 100644 public/blog/2019/12/bookportuguese.html create mode 100644 public/blog/2020/05/blogannouncement.html create mode 100644 public/blog/2021/02/chatwisely-intro.html create mode 100644 public/book-1.1.html create mode 100644 public/book-1.1/authentication-and-authorization.html create mode 100644 public/book-1.1/basics.html create mode 100644 public/book-1.1/blog-example-advanced.html create mode 100644 public/book-1.1/case-study-sphinx.html create mode 100644 public/book-1.1/conduits.html create mode 100644 public/book-1.1/creating-a-subsite.html create mode 100644 public/book-1.1/deploying-your-webapp.html create mode 100644 public/book-1.1/forms.html create mode 100644 public/book-1.1/haskell.html create mode 100644 public/book-1.1/http-conduit.html create mode 100644 public/book-1.1/internationalization.html create mode 100644 public/book-1.1/introduction.html create mode 100644 public/book-1.1/json-web-service.html create mode 100644 public/book-1.1/monad-control.html create mode 100644 public/book-1.1/persistent.html create mode 100644 public/book-1.1/restful-content.html create mode 100644 public/book-1.1/routing-and-handlers.html create mode 100644 public/book-1.1/scaffolding-and-the-site-template.html create mode 100644 public/book-1.1/sessions.html create mode 100644 public/book-1.1/settings-types.html create mode 100644 public/book-1.1/shakespearean-templates.html create mode 100644 public/book-1.1/web-application-interface.html create mode 100644 public/book-1.1/widgets.html create mode 100644 public/book-1.1/wiki-chat-example.html create mode 100644 public/book-1.1/xml.html create mode 100644 public/book-1.1/yesod-typeclass.html create mode 100644 public/book-1.1/yesods-monads.html create mode 100644 public/book-1.2.html create mode 100644 public/book-1.2/authentication-and-authorization.html create mode 100644 public/book-1.2/basics.html create mode 100644 public/book-1.2/blog-example-advanced.html create mode 100644 public/book-1.2/case-study-sphinx.html create mode 100644 public/book-1.2/conduits.html create mode 100644 public/book-1.2/creating-a-subsite.html create mode 100644 public/book-1.2/deploying-your-webapp.html create mode 100644 public/book-1.2/environment-variables.html create mode 100644 public/book-1.2/forms.html create mode 100644 public/book-1.2/haskell.html create mode 100644 public/book-1.2/http-conduit.html create mode 100644 public/book-1.2/initializing-foundation-data.html create mode 100644 public/book-1.2/internationalization.html create mode 100644 public/book-1.2/introduction.html create mode 100644 public/book-1.2/json-web-service.html create mode 100644 public/book-1.2/monad-control.html create mode 100644 public/book-1.2/persistent.html create mode 100644 public/book-1.2/restful-content.html create mode 100644 public/book-1.2/route-attributes.html create mode 100644 public/book-1.2/routing-and-handlers.html create mode 100644 public/book-1.2/scaffolding-and-the-site-template.html create mode 100644 public/book-1.2/sessions.html create mode 100644 public/book-1.2/settings-types.html create mode 100644 public/book-1.2/shakespearean-templates.html create mode 100644 public/book-1.2/single-process-pubsub.html create mode 100644 public/book-1.2/understanding-request.html create mode 100644 public/book-1.2/visitor-counter.html create mode 100644 public/book-1.2/web-application-interface.html create mode 100644 public/book-1.2/widgets.html create mode 100644 public/book-1.2/wiki-chat-example.html create mode 100644 public/book-1.2/xml.html create mode 100644 public/book-1.2/yesod-for-haskellers.html create mode 100644 public/book-1.2/yesod-typeclass.html create mode 100644 public/book-1.2/yesods-monads.html create mode 100644 public/book-1.4.html create mode 100644 public/book-1.4/authentication-and-authorization.html create mode 100644 public/book-1.4/basics.html create mode 100644 public/book-1.4/blog-example-advanced.html create mode 100644 public/book-1.4/case-study-sphinx.html create mode 100644 public/book-1.4/creating-a-subsite.html create mode 100644 public/book-1.4/deploying-your-webapp.html create mode 100644 public/book-1.4/environment-variables.html create mode 100644 public/book-1.4/forms.html create mode 100644 public/book-1.4/haskell.html create mode 100644 public/book-1.4/http-conduit.html create mode 100644 public/book-1.4/initializing-foundation-data.html create mode 100644 public/book-1.4/internationalization.html create mode 100644 public/book-1.4/introduction.html create mode 100644 public/book-1.4/json-web-service.html create mode 100644 public/book-1.4/monad-control.html create mode 100644 public/book-1.4/persistent.html create mode 100644 public/book-1.4/restful-content.html create mode 100644 public/book-1.4/route-attributes.html create mode 100644 public/book-1.4/routing-and-handlers.html create mode 100644 public/book-1.4/scaffolding-and-the-site-template.html create mode 100644 public/book-1.4/sessions.html create mode 100644 public/book-1.4/settings-types.html create mode 100644 public/book-1.4/shakespearean-templates.html create mode 100644 public/book-1.4/single-process-pubsub.html create mode 100644 public/book-1.4/sql-joins.html create mode 100644 public/book-1.4/understanding-request.html create mode 100644 public/book-1.4/visitor-counter.html create mode 100644 public/book-1.4/web-application-interface.html create mode 100644 public/book-1.4/widgets.html create mode 100644 public/book-1.4/wiki-chat-example.html create mode 100644 public/book-1.4/xml.html create mode 100644 public/book-1.4/yesod-for-haskellers.html create mode 100644 public/book-1.4/yesod-typeclass.html create mode 100644 public/book-1.4/yesods-monads.html create mode 100644 public/book-1.6.html create mode 100644 public/book-1.6/authentication-and-authorization.html create mode 100644 public/book-1.6/basics.html create mode 100644 public/book-1.6/blog-example-advanced.html create mode 100644 public/book-1.6/case-study-sphinx.html create mode 100644 public/book-1.6/creating-a-subsite.html create mode 100644 public/book-1.6/deploying-your-webapp.html create mode 100644 public/book-1.6/environment-variables.html create mode 100644 public/book-1.6/forms.html create mode 100644 public/book-1.6/haskell.html create mode 100644 public/book-1.6/http-conduit.html create mode 100644 public/book-1.6/initializing-foundation-data.html create mode 100644 public/book-1.6/internationalization.html create mode 100644 public/book-1.6/introduction.html create mode 100644 public/book-1.6/json-web-service.html create mode 100644 public/book-1.6/monad-control.html create mode 100644 public/book-1.6/persistent.html create mode 100644 public/book-1.6/restful-content.html create mode 100644 public/book-1.6/route-attributes.html create mode 100644 public/book-1.6/routing-and-handlers.html create mode 100644 public/book-1.6/scaffolding-and-the-site-template.html create mode 100644 public/book-1.6/sessions.html create mode 100644 public/book-1.6/settings-types.html create mode 100644 public/book-1.6/shakespearean-templates.html create mode 100644 public/book-1.6/single-process-pubsub.html create mode 100644 public/book-1.6/sql-joins.html create mode 100644 public/book-1.6/understanding-request.html create mode 100644 public/book-1.6/visitor-counter.html create mode 100644 public/book-1.6/web-application-interface.html create mode 100644 public/book-1.6/widgets.html create mode 100644 public/book-1.6/wiki-chat-example.html create mode 100644 public/book-1.6/xml.html create mode 100644 public/book-1.6/yesod-for-haskellers.html create mode 100644 public/book-1.6/yesod-typeclass.html create mode 100644 public/book-1.6/yesods-monads.html create mode 100644 public/book.html create mode 100644 public/book/.html create mode 100644 public/book/authentication-and-authorization.html create mode 100644 public/book/basics.html create mode 100644 public/book/blog-example-advanced.html create mode 100644 public/book/case-study-sphinx.html create mode 100644 public/book/creating-a-subsite.html create mode 100644 public/book/deploying-your-webapp.html create mode 100644 public/book/environment-variables.html create mode 100644 public/book/forms.html create mode 100644 public/book/haskell.html create mode 100644 public/book/http-conduit.html create mode 100644 public/book/initializing-foundation-data.html create mode 100644 public/book/internationalization.html create mode 100644 public/book/introduction.html create mode 100644 public/book/json-web-service.html create mode 100644 public/book/monad-control.html create mode 100644 public/book/persistent.html create mode 100644 public/book/restful-content.html create mode 100644 public/book/route-attributes.html create mode 100644 public/book/routing-and-handlers.html create mode 100644 public/book/scaffolding-and-the-site-template.html create mode 100644 public/book/sessions.html create mode 100644 public/book/settings-types.html create mode 100644 public/book/shakespearean-templates.html create mode 100644 public/book/single-process-pubsub.html create mode 100644 public/book/sql-joins.html create mode 100644 public/book/understanding-request.html create mode 100644 public/book/visitor-counter.html create mode 100644 public/book/web-application-interface.html create mode 100644 public/book/widgets.html create mode 100644 public/book/wiki-chat-example.html create mode 100644 public/book/xml.html create mode 100644 public/book/yesod-for-haskellers.html create mode 100644 public/book/yesod-typeclass.html create mode 100644 public/book/yesods-monads.html create mode 100644 public/contributors.html create mode 100644 public/favicon.ico create mode 100644 public/feed.xml create mode 100644 public/index.html create mode 100644 public/page/community.html create mode 100644 public/page/quickstart.html create mode 100644 public/page/screencasts.html create mode 100644 public/robots.txt create mode 100644 public/static/7of9.jpg create mode 100644 public/static/benchmarks/2011-03-17/extra-large.png create mode 100644 public/static/benchmarks/bigtable/cgipackages.png create mode 100644 public/static/benchmarks/bigtable/fastcgi.png create mode 100644 public/static/benchmarks/bigtable/fastcgi2.png create mode 100644 public/static/benchmarks/bigtable/rawcgi.png create mode 100644 public/static/benchmarks/bigtable/standalone.png create mode 100644 public/static/benchmarks/bigtable/yesod.png create mode 100755 public/static/benchmarks/warp03/bigtable.png create mode 100755 public/static/benchmarks/warp03/pong.png create mode 100755 public/static/benchmarks/warp03/static-file.png create mode 100644 public/static/benchmarks/warp0321.png create mode 100644 public/static/blog.png create mode 100644 public/static/js/modernizr.js create mode 100644 public/static/logo-home.png create mode 100644 public/static/logo-home2-no-esod-smaller1.png create mode 100644 public/static/logo-home2-no-esod-smaller2.png create mode 100644 public/static/logo-home2-no-esod.png create mode 100644 public/static/logo-home2-no-esod.psd create mode 100644 public/static/logo-home2-no-notches.png create mode 100644 public/static/logo-home2-no-notches.psd create mode 100644 public/static/logo-home2.png create mode 100644 public/static/logo-home2.psd create mode 100644 public/static/logo-topleft.png create mode 100644 public/static/oreilly-book.gif create mode 100644 public/static/oreilly-the-book.gif create mode 100644 public/static/toc-closed.png create mode 100644 public/static/toc-open.png create mode 100644 public/static/warpspeedahead.png create mode 100644 public/testimonials.html diff --git a/justfile b/justfile index 6f112f88..1adeae15 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,20 @@ -generate: +default: + just --list --unsorted + +generate-xml: rm -rf book/generated-xml ./book/tools/generate.sh ./book/tools/validate.hs + +generate-static: + git rm -r public + rm -rf yesodweb.com + git clone https://github.com/yesodweb/yesodweb.com + cd yesodweb.com/make-it-static && cargo run + mv yesodweb.com/public . + git add public + rm -rf yesodweb.com + +generate: + just generate-xml + just generate-static diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..507cf247 --- /dev/null +++ b/public/404.html @@ -0,0 +1,12 @@ + Not Found +

Not Found

+

/404

+ +
\ No newline at end of file diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 00000000..02e98e6d --- /dev/null +++ b/public/_redirects @@ -0,0 +1,3 @@ +/feed /feed.xml +/wiki https://github.com/yesodweb/yesod-cookbook +/wiki/* https://github.com/yesodweb/yesod-cookbook diff --git a/public/assets/baseless-assertion.jpg b/public/assets/baseless-assertion.jpg new file mode 100644 index 0000000000000000000000000000000000000000..21dfab067f228a52bfebb14d5d99dab840f7b1c8 GIT binary patch literal 83229 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 aqr|A+dAYMid^iI5qa>#;TO(~2`u_tiLG8={ literal 0 HcmV?d00001 diff --git a/public/assets/measuring-warp/measuring-warp-graph-1.png b/public/assets/measuring-warp/measuring-warp-graph-1.png new file mode 100644 index 0000000000000000000000000000000000000000..914eb64f44ab7b818ed278a21a99052a9741fbd0 GIT binary patch literal 25565 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%0Ac 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
4utPuOo-1!`F$RW} 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�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
+
+

criterion performance measurements

+ +

overview

+ +

want to understand this report?

+ +
+ +

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vegito

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.04698339805408e-65.058126555367965e-65.0814215532222215e-6
Standard deviation2.7559530528929736e-84.6512743123217627e-88.761050021819734e-8
+ + +

Outlying measurements have no + (5.5864158565838064e-3%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/gotenks

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.062140922903756e-65.075952813887726e-65.091200413636181e-6
Standard deviation3.9371246343271035e-84.880622795629583e-86.44440402052526e-8
+ + +

Outlying measurements have slight + (5.5542819141926165e-2%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/conduit-combinators unqualified

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time1.1181095312286869e-31.1356401409082068e-31.1661312170029943e-3
Standard deviation5.3390468744207256e-58.533695308299818e-51.281909475729944e-4
+ + +

Outlying measurements have severe + (0.5881477694601457%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/conduit-combinators qualified

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.06693941595305e-65.1026746014408814e-65.1950427538039535e-6
Standard deviation5.648093624150736e-81.6225630266956213e-73.0510118826434783e-7
+ + +

Outlying measurements have moderate + (0.3955108305055059%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/conduit

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.062207883103922e-65.081944961114066e-65.153059118355966e-6
Standard deviation3.5085619081380075e-81.2020458513794528e-72.4623421455957536e-7
+ + +

Outlying measurements have moderate + (0.2685189553749743%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vector boxed

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.057778935483031e-65.0697247966163474e-65.092574020921399e-6
Standard deviation3.186354317873175e-85.314405713977064e-88.628427042088357e-8
+ + +

Outlying measurements have slight + (6.877276137113449e-2%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vector unboxed

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.0769111591411775e-65.104424164005706e-65.198288106302256e-6
Standard deviation5.1897927901574893e-81.4082341994381395e-72.98641675387848e-7
+ + +

Outlying measurements have moderate + (0.3291521152030871%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vector unboxed foldM

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.05937659811085e-65.073262967569812e-65.092720320952292e-6
Standard deviation4.3930310039045714e-85.576834742633988e-87.51249398088904e-8
+ + +

Outlying measurements have slight + (7.41578693737416e-2%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/direct implementation

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.980777495909067e-66.001788421220449e-66.0363379192345105e-6
Standard deviation5.97640859602847e-88.224398460349541e-81.2313198130959798e-7
+ + +

Outlying measurements have moderate + (0.108624445414539%) + effect on estimated standard deviation.

+
+ +

understanding this report

+ +

In this report, each function benchmarked by criterion is assigned + a section of its own. The charts in each section are active; if + you hover your mouse over data points and annotations, you will see + more details.

+ +
    +
  • The chart on the left is a + kernel + density estimate (also known as a KDE) of time + measurements. This graphs the probability of any given time + measurement occurring. A spike indicates that a measurement of a + particular time occurred; its height indicates how often that + measurement was repeated.
  • + +
  • The chart on the right is the raw data from which the kernel + density estimate is built. The x axis indicates the + number of loop iterations, while the y axis shows measured + execution time for the given number of loop iterations. The + line behind the values is the linear regression prediction of + execution time for a given number of iterations. Ideally, all + measurements will be on (or very near) this line.
  • +
+ +

Under the charts is a small table. + The first two rows are the results of a linear regression run + on the measurements displayed in the right-hand chart.

+ +
    +
  • OLS regression indicates the + time estimated for a single loop iteration using an ordinary + least-squares regression model. This number is more accurate + than the mean estimate below it, as it more effectively + eliminates measurement overhead and other constant factors.
  • +
  • R² goodness-of-fit is a measure of how + accurately the linear regression model fits the observed + measurements. If the measurements are not too noisy, R² + should lie between 0.99 and 1, indicating an excellent fit. If + the number is below 0.99, something is confounding the accuracy + of the linear model.
  • +
  • Mean execution time and standard deviation are + statistics calculated from execution time + divided by number of iterations.
  • +
+ +

We use a statistical technique called + the bootstrap + to provide confidence intervals on our estimates. The + bootstrap-derived upper and lower bounds on estimates let you see + how accurate we believe those estimates to be. (Hover the mouse + over the table headers to see the confidence levels.)

+ +

A noisy benchmarking environment can cause some or many + measurements to fall far from the mean. These outlying + measurements can have a significant inflationary effect on the + estimate of the standard deviation. We calculate and display an + estimate of the extent to which the standard deviation has been + inflated by outliers.

+ + + +
+
+ + + diff --git a/public/assets/vegito-benchmark-2016-02-28.html.html b/public/assets/vegito-benchmark-2016-02-28.html.html new file mode 100644 index 00000000..01dc296d --- /dev/null +++ b/public/assets/vegito-benchmark-2016-02-28.html.html @@ -0,0 +1,1016 @@ + + + + + criterion report + + + + + + + +
+
+

criterion performance measurements

+ +

overview

+ +

want to understand this report?

+ +
+ +

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vegito

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.04698339805408e-65.058126555367965e-65.0814215532222215e-6
Standard deviation2.7559530528929736e-84.6512743123217627e-88.761050021819734e-8
+ + +

Outlying measurements have no + (5.5864158565838064e-3%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/gotenks

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.062140922903756e-65.075952813887726e-65.091200413636181e-6
Standard deviation3.9371246343271035e-84.880622795629583e-86.44440402052526e-8
+ + +

Outlying measurements have slight + (5.5542819141926165e-2%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/conduit-combinators unqualified

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time1.1181095312286869e-31.1356401409082068e-31.1661312170029943e-3
Standard deviation5.3390468744207256e-58.533695308299818e-51.281909475729944e-4
+ + +

Outlying measurements have severe + (0.5881477694601457%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/conduit-combinators qualified

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.06693941595305e-65.1026746014408814e-65.1950427538039535e-6
Standard deviation5.648093624150736e-81.6225630266956213e-73.0510118826434783e-7
+ + +

Outlying measurements have moderate + (0.3955108305055059%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/conduit

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.062207883103922e-65.081944961114066e-65.153059118355966e-6
Standard deviation3.5085619081380075e-81.2020458513794528e-72.4623421455957536e-7
+ + +

Outlying measurements have moderate + (0.2685189553749743%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vector boxed

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.057778935483031e-65.0697247966163474e-65.092574020921399e-6
Standard deviation3.186354317873175e-85.314405713977064e-88.628427042088357e-8
+ + +

Outlying measurements have slight + (6.877276137113449e-2%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vector unboxed

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.0769111591411775e-65.104424164005706e-65.198288106302256e-6
Standard deviation5.1897927901574893e-81.4082341994381395e-72.98641675387848e-7
+ + +

Outlying measurements have moderate + (0.3291521152030871%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/vector unboxed foldM

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.05937659811085e-65.073262967569812e-65.092720320952292e-6
Standard deviation4.3930310039045714e-85.576834742633988e-87.51249398088904e-8
+ + +

Outlying measurements have slight + (7.41578693737416e-2%) + effect on estimated standard deviation.

+
+

sum $ map (+ 1) $ map (* 2) $ enumFromTo 1 9001/direct implementation

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
lower boundestimateupper bound
OLS regressionxxxxxxxxx
R² goodness-of-fitxxxxxxxxx
Mean execution time5.980777495909067e-66.001788421220449e-66.0363379192345105e-6
Standard deviation5.97640859602847e-88.224398460349541e-81.2313198130959798e-7
+ + +

Outlying measurements have moderate + (0.108624445414539%) + effect on estimated standard deviation.

+
+ +

understanding this report

+ +

In this report, each function benchmarked by criterion is assigned + a section of its own. The charts in each section are active; if + you hover your mouse over data points and annotations, you will see + more details.

+ +
    +
  • The chart on the left is a + kernel + density estimate (also known as a KDE) of time + measurements. This graphs the probability of any given time + measurement occurring. A spike indicates that a measurement of a + particular time occurred; its height indicates how often that + measurement was repeated.
  • + +
  • The chart on the right is the raw data from which the kernel + density estimate is built. The x axis indicates the + number of loop iterations, while the y axis shows measured + execution time for the given number of loop iterations. The + line behind the values is the linear regression prediction of + execution time for a given number of iterations. Ideally, all + measurements will be on (or very near) this line.
  • +
+ +

Under the charts is a small table. + The first two rows are the results of a linear regression run + on the measurements displayed in the right-hand chart.

+ +
    +
  • OLS regression indicates the + time estimated for a single loop iteration using an ordinary + least-squares regression model. This number is more accurate + than the mean estimate below it, as it more effectively + eliminates measurement overhead and other constant factors.
  • +
  • R² goodness-of-fit is a measure of how + accurately the linear regression model fits the observed + measurements. If the measurements are not too noisy, R² + should lie between 0.99 and 1, indicating an excellent fit. If + the number is below 0.99, something is confounding the accuracy + of the linear model.
  • +
  • Mean execution time and standard deviation are + statistics calculated from execution time + divided by number of iterations.
  • +
+ +

We use a statistical technique called + the bootstrap + to provide confidence intervals on our estimates. The + bootstrap-derived upper and lower bounds on estimates let you see + how accurate we believe those estimates to be. (Hover the mouse + over the table headers to see the confidence levels.)

+ +

A noisy benchmarking environment can cause some or many + measurements to fall far from the mean. These outlying + measurements can have a significant inflationary effect on the + estimate of the standard deviation. We calculate and display an + estimate of the extent to which the standard deviation has been + inflated by outliers.

+ + + +
+
+ + + diff --git a/public/assets/warp-posa/1.png b/public/assets/warp-posa/1.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1219355ce74dfc8ad8233320c361b4e8fd0da1 GIT binary patch literal 16396 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 L)MXJe&)@tXuShg~ literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/3.png b/public/assets/warp-posa/3.png new file mode 100644 index 0000000000000000000000000000000000000000..b005bf111ab27a9ab18eb42f3bde4e6596a6787d GIT binary patch literal 23157 zcmZ_01z40_+ciw5lt`C!*8oyOcStG?(%p=NpmY!2-6bI@-7s{wbVw*20#g6Q{oK#{ ze*bqIpT~jDjQiSqu6^yj_Bz+OPQukxqsl zM*E*m{@ahVg`1hHjWfi?$?^4bzb2+mA0Z+%G|vP5&!7K%Pl%1>|BmG7_U~;0H^}+? zKb+hgT%7;YH*hKV`BNb&CkJO&3pY2Qe^Ea0KP~@%pZ)JR|7ovg?F4ZGX2I3QOu-Ri z;R;*~F?s%WqM(1T{(ralzxz^kwXp#1`k$-0|GoPE{OsTMV9w_o|34<;KePPLr@%Ce zVt_gS=aGqGI3QzY!@-HeDM(Aa^MpUjLR}&MI5qIZ#2HZf?m%8mC@+{sW&@oMS1%0L zHn#ZP2l@SJyh8qi0Cc`fzdR4Lc8^%KfOv*0(PiZzZc^f-u0tnH5v>Rv;E`;@i4XX<|Hzaa+xh_0sq*wkn6rm8bNv_BB zr-${fAIT7UhStBr>$fVYmgzPKcFA<+_x)Zfu7+BBeYX}F^vn-HyAQ_x9`jK&+cVBp zNmkp9Q{aK01%%n#_qAiwOc57{pe(9is|+(Cg=E4~`O$nBhrW+{zCAOlVic60FQ>Rx zAu~42UaV;NTJ!o24exI@aeRq8?p9B> z2#e-cP4HIS1M*dQKpkR6Bv$})crn1g?t`EG`Oi}I+Na;aB$lapu$R+Wq9#2-r}O$w z!Tp5#Y(6L6>*Y+<#Hcq9e}8}LO;8nGdiBNY2boh_O&1T94j-)p#!b%CeFskls7gyc&~sMQY1S_VDs=`T=LsI;=7WyuKS0Kc_{Ri-fH$tRVm z-gfv~2#uW(fs{BVLVusTFOsh%ubL&(p(S3b2`_ZdcF|>}Wg(vFs4W|Fg^N7N`Mb9p zcH5ge8YIUJs85#?U*=}k#i%&Hl5E$w#Sc2>^^d>}kW28pyYSjB%6g@0lyBB5s|KS& zWgF&E3^ib!dkpG}X&_QosCTavU-r?<8(H)Wm->e&qh7?eUK}O9h@1~cE8yr}vzJTN z-fkU3Ja);JY8oAe#WS5*O;GoIOlg+i4>GtEp2VsP0aa3w!SYaUL5LM9#p1l1bw$T3TtK)R1 zj@wtY9_ydAQR6wAHUhYuH@Y}Nr>NF}{S0jD+@$BGmYFWF>WgvVP%QqH>Qk_`fs2g5 z&qU3hRBkhjJD=YFH0{E(=0kc(*6IqK;9D^Gl?SUqrSudRX+GU;dHA#3`>XsL<@Y5^ zG737er~3;;bvnzO&wsdFcLu9hTt|Y#@fkyKL~mZ>{)BG<2KtCiupmLKHm>i!WYPO@ zy-rotdfdXbY+n})Yy&V`!>q<9Y_aWu7#wA3i*vT?4ByAwt3_R_EP{DGyGrK?k!vIx zCvu0b4UoI-w>M3!d7n^d0Z7DUzgT@bt;BiYWtHt^*|Za{)bn_E@(mX4{rP4y^wSI4 zFzSb!?HI((GGa4`>wf@rUj@n5cFCkZwG^ZMdd-{k0pZ#*3=K={ZfT+in9+i#Td&h@ zRKC$^hsM?7I}|L^!Z=YbBjC|B%BJ4zB&c2zy9J0oTxvQ{sHA~p^!`%m$#;?z`>l}U z^xqSO0D+IBRm1$t-tU(x;*pVO{241Gq0Xlp-J40;MpMVifAH6F2k8@%DGV{+<7;W^VqY}C4>8^P;mDv|uY<7WuXK0Od8#}P# z4Mb3UJwt>9|8qDCAcRM)Ho0uGTHbfI`&$+!(pmo= z`Q-kOxbk4jg%9u@{~`te10RuA790MOxjz2eNP~6*#=#e4^voKbXQLc=Up2QLd$IQ) zE)h!#&+lGFeThKsKQ5I!Yu9p-J(*LwjO%QcvK7@BA;~3bZ}?aKd(Ht#>;tFBxA#wE z>tj1OlsfUIDVJ?+Iq0JCr>1*LLQWlDlbpcIEtfvaPPHaaN)vW-PcRRj+ux8hj{ff^ znmCG1E5}bq+v&hce4l0jKnrPgOWcG^AxVH48Rx_5GR@HI73Iy1d&^TyU*$(D(ulJu zs)@6QQ@cI>z>~(cPfD6^=kjyYqpt2wI=`LkU``l23ao!zFeH6odZZ++IRdSsB?g9g z(S6oqKcA`sIfS*;@X*;B6!>}F0;rND>zHN|Lc$x%m#~)xmqX<8;aCA=2ok})Aq~`5 zu#VCm@18V%jlLSP>Hk5p{ini83i^|I$;MirEMJk{QHSOJhId{tA%^+T4S-jCvEpk! zfvf_Y?U&P}?V(nFRLP3w;EQZFBuYh0@{`Ef@D=5F;0&-EyUAV0<6pZd$;1+H zFVVGontK`tV}Z+3>HA`(5jQjK2zjh0bvk8hD+!VWQ?y}C^bC!yFLZS>Bz1;Ei`|+A z)=kl~%7iNHPG6X*f+HoR;LR?(*uuMckd-Y=fjyv}z1bQkOX}l$SwO}7r0@b2T<7z2 zw~li4{BFo)*#ra7NR8cn=vB*7}-?P;xdtFpGY<`osH6^p|v z&5TRo%3}XisR>!0gkY{(1xgg&kUn~W53ZKxV;@z2*gMT0OMA2*Q7^YY@qF?^9eb0l zZhAUms@O!j^T);1-n#$%uumR1Y@Fsib%-1qTx~_LCsxR$kS(=pHnQ9*Ub>K*McWfk z7{tVl&o~p0!+4Sq*Lc9%w7qE&XzWhI=^y8H-BPS>K)@+&SgiDO*1Ui+E&D5*I-&Z3 z%2Un@cqj@xo$V1D(}pX2O+r%gA)AqC$cU{KG43nYC&78DJo8N~O7#{t`x%*}5`>T( zjch!FZ4!F@a9q{x<(9dwOnRp&zRb$Vu@`a;5y%sW6MgnY=TebWU&xrrHtYm?efct@ ztZss=^xQ+-3V!xlv`iDl9qTj>LC7m@fO*+0bs)2dq!DLMoihILlKy@-!^yTv4Eib} z3?k1|L>F|nL27$=_OZcklR$J^>7`MnLHt#G&ZJ(vh=>RPGXpTwHvOffd7Im8UYqz` z^<EM`N^jT!DmzP=6$+QmstGVuJ6|VjMH;yHzKO zAA-nQHKc#te<+tM1VA0{{K7=qnj$^EEFrirWp0=ix!`6$Bv8&1*3cdY?nU$&Som8q zjKgQuVrL`K%gOgkcGJFME|5Qo>LsbZwi~Ti6{=PCoW+kd&-0HWI<3l#1N>hsj~u#h z<~p)4;4^tg)eGkJAYaKATx!nbl!9wm_27;qnO-@+>7VBCX(Sup_yt-eK5$K|qOjt3 zZ!k+fAV6~vW-{5Xi(`i>`zQDOh`V-=jhenTuhnSjoO>k8IbtFTu@F9AV@q>4v(lI; zY-mVLhuoz*E~~W7UmCOfxC)!Dsel{tzp=9w^VQ6=jjk@vU8dig{}XwU`@2hbMHvcr z@a--Ai`fhS2+tv<n)(m3(|2lbAn?-4Ml<}l`f-8a#`}8Jqqo|m7vt&k{}4wmB;Gf> z>T(oCE;m(e=fje`G1^96^!Hmtos&%0ZWAJnS?eG>%T1@whd(Aar<=nw6JP$+ae1BZ zByUcTxcrAbSyLbtKOELg^c1BSk?!|ELtS)%fTs7u<2lh-IC3k}lnB7)dV#>MSbbYA zn!K!1EIP|;H_aAu+;%a3I!GH6^c<}n0?do_>;Z@`v51U3O%@$nk9rns%=pglEi)Vl zLtc>d6e&LQPJ9Msv`CJY((fF5IlfhXck^}pov5(1q9crfYXS&Z*KX`lx{B!6?~kD9#z2$5C$1ONw=e}JufI;*LA z;1X@W{!~iDeOz!a2wz2j+P?j2-gflQira+qf}uw+eTo6dm}^Ms z`w&A$5Ws+R3(h}4rer=*h+UoQGj;L*dx#66_}i>|pR|9yZ6_X})t-AMp*W(}lds>V zxsGxV1Z8KhA!RFZwnKGz7hPF8Xg69)kD8MTz&I>AK2K1fceTr zMVsEtK@6DjId%FkxPd)iXwNgR$CjthJ3)g8Ncp)dT{Q;lPfhFOk~!?Pz{>F7-xduV^e|!HnJ41 zUtjhUeF&->7h-*03V*2(2~?MH*DXgm9&<45sZinx*9o)@@_wGW{Yx{;+(u-{912&{ z%<)c`X;K21yM=dB#h#p-OBuL7%i9 zPea9K%@#iReqQ{1y*k%4`J-nN2=Bij(>>l!_!5365i_g62DE~2v(5zA!zd`j`@j~{ zDj@`m2&U9D0b5fW#|>K38BAoPQzBda-7FIhM__@=$n%Fy3BfVXGq?S8%h%y^^zVZc z1c0I-IEdP;TOw%Q$cGdmCUSrlPqE{6m+UG4N(JoisBd2PHXGl|m!)BbVp}pKcp|fBxUGl=&uQ zN2*@F6??c$!e00cwLyANUx7=`HVdhObUsW(eqdeyZ%|MfBd%^gI&ZZg2>Gv?Wh)0D z>QU+~!M}^cAp`)-H{K@WRsYi1BLGp$DR?@2{(Zn>0AXFQ1cpOZ2%%o9LReImRFKng_ z${^WnkSs3P?toTP`L{q>*j7$UMZMp4%ZES`gkgX^EGIyP$7Z0x(5i?i@^Bg@R{qrPYYys^!<$O#l^f=t?bW!7wBl<*+(Haieyz3|%2+Vjqwr zH7@PPG>haZEsY_Iw@*sWNKkU{<7J81Tr%DgFPh*$-6>`IRFVcdv4(*^CmwVoA$RW` zM=XIkL$+X{hG3K^pkfjC0b0%GA&i6K3*U#RxYxIAl|@ap^d(foqfmu0*@Jj?7({sGK zTFoH&tSoKDfc;kMObJr=oW{ce$f5uF{!!NZ)zZZ;6vDM<3X$+Gp; z-xyC@2$@NgssVF~?EID7)l3^X7mTebRbp{=e=%Wq#$-&6v)^GWBbgR=9qY)a=fPw= zEivW#^;Kj2(gecu5T`Frhy}BY-eaMH#D7O8iv>YnvsV82TIX>Ha**F?bx*6J`oQmg zWu|?d#H6REIJ;;ww|PJHwNl7iUbObTmadGx2c#~~Prv!Sl}|5iTYOBMBJ#FUrG0K3 zmN|S{?#IyQ{%kX`tWs>GXr3BIAfJg_YszsNXSH?T-uR9cx)uR95 z^VNHY@b9%#TpgQnT0F35$59y~!}_0Cp$*5eQSR1rB=a0IA<`XoDF=T~ACBWBuMB@_ z7fHZv`@43HM4zQ9GDlCZFpk(Az|2Kg0Xe4-8~@%3m1Ye80^y82%lo)le{GSPtZo#f zbOQ)9y}$`V^5ry;svdDPq8xZ)MoA-(BLP*~hSkzPBli((xNx6d&S+>#bvGbR)%N-a z)O>T_B+2A3+b|+yI&``9$ja1}R&JFjH#Tc*jhG1bLMZ$)dIWxWfI3^#! z#|I;&Q35AC$``KCS}-(w1o8!Rgv~-?$KCeG1p&kn9b$e*17begMA_v5T!_%-(V)+F z0CiXmK3~m|cKZTQuPKvx#fuD<tU32$As&1}!J({x{rT|pSGz>263-;e?-}tz zf%glWfLuRwQ{)vS@!LJk(&zv;Gta3iTVXGaaK~5W=-e^{S;@M%D$q&i`Ns1>* z(n*9C@Qe!}ju0x~Zb20tJ58n0l}MRO!z`C*7114fU&!2hZ5fb@X#BjR<%IuP;)+vTeR=`FK~vI_maCBaEwD*O z;|+=*VoY3^|E~B*Kjr zE}F2>VCIuo7b}BG8uh-Pv1QlHgJSv2v6l+$hc^!~DiKpZwM zh!&#d*wZKUOlEu;#3JT$BTBt9p#)*`Nhn1K3s$poirfT?ljUJ-H?QkH$dGcc!s15> zW#T~dZhLXx4BF>0euqHJL#dGWw$-!qM!pYMT-#8duh*}S&mms5u%e8r3Blb|TDJO0 z$$uori!pvq*Rm3D^{$lhav2VMg2a4^p}X!PpqdeA)-p|w*$C8)%?2-7<%o;|vT1sY z4UkpQQl0OZ4%o`niW?aDo#?O=t%!`NLpuY4?SQg+F~>EER~IdA0@QYy;y63+LA@Hw zjZCNF+|`!;&T+oLlHgqqBw-upE>a{60~p-NCZNb+5*b3`WFI<@zAwMhmg*U%*X$$u z;A7I-`YCGEKXkM0IU%H}B1j)btBT8r|9+U9C(|a$=X{896Ogc9G_JTR-8`K5QgHwR z4wui(DGhlu%%TP7)7FMsA% zosqm+o|mbppH`ewV9a`@(CK{%-0>XmgZK;M%RR9t!Fa+C5I`;_a>k{v%yy0u{cBwh z5PWG=3W(aKPNO3i7Gj#zpI8a=8bVWy0nRUHk@6boWhv!`zX4FctUPJjj<#O&+KXaP zPQ`tj&dVG>tQAzjK%H_x$8?pQ5UPBaYWU;VKno>49-9hZKBkNHlrGL078 zIAZtJvOLuJ5g+sBK=3JUfSNc^{WDoVgxd`EiVMza*o+!y1Ngd?hVsGqEzV}Z*@?+o zQ7yTOXP0{83xxY$XP$Ein&OidEpWk(liT8vUi*`I!SXDFCK|43DiQC{yapkTu;}BL zlKfK^79-F3z`>4{@SsVv!~L~IlTeC!r7-h6Pb_ybyzk{DC5(pvjKH7pIhV;W_nr0R zz6AvG)~!V+7_bhuKQU3w4{9~y)iwZuZaL~H5MzC<)#jWxcTPc{`B7;j(4tKCQlga^ zorJ;d5JDY(2AO%QT{?J`rTclrqQw__o_MeVJ0uR({W$l&7l?r|R2i_|WqI_ER23zE zV0zsh0@*Hgr86?7MW-K4Mb|AVW6t^V_h54?l5^gEr*A&;`iwvRo9r6q&%s&D#IEN6 z8pvX>N1i%=Ki#FM_X{{eHlabZW0R z1#eb;ERBG}wXyueG?INbBYv0w6na?1?Vji$KsKMaa0F_*UlbRHCE{A!9$%Gpb|>hCHb-tP&%A>M~& z0LdCqmQL25+pPYZ2`5=J#8Am(#PWTVM-1^O*ow2RAhi09vJbY*5E?^IJ( z7#3sF6q7~89~eYY42M~7%FdFKe4s{vZD*j9$p#Mb7Z*5R?4Fu_a60JZssMlqH zg6Q$@Lg%k0q@S;?0kuJ@iy+I96~?RrK9m0Ve$PNsE|0jG(~JbM@UA*eTALz*!Xid; zi#D%3YEtVBUV~;*bwiU{4>3(5VhAPhxHdfbdeo?IjIA#>0&#vB^DUC9C%tqW5%A= zO`+O7Em}$Q_DFUJQU#0F;}01^qYd&iKK#DyC_E+s^Pv*eXc0c^km5GIH2QQv|KKM` zWaiR?&KRVrD)JsV25hvG93WpL6@f3mQu6T_7azP};P!TPVV~+|U_){gzE@W&|GfMq z3=WcCBc(&VcGBgKI7gPGFsyY~Isdg+Uj1ydoo6r7k#;}Z`*HA1Mb)5YwTH5&B;#nNmW-a-d%P_di`Qd88j{hehqU2Y; z(eqUP(bv9!Mz2^tj^+WO{nHpfK#P(coaqSc*Oc#yT(G8TccDDEV_;|U2@n}L`;)Th z8E>dpSq!u$L*##rnkDp-c+lpah`7~zU^2!OI#+_{|7J8zwEAlNmEXnMKiE$7)*=I)tok`xM!9hSo-%{?r5MD)( z?^##`Fr&>qzOFC@yZqB97?~5a(7W!mGmh+(d6++5(9$zcIv+~vb(h?n0@anxaQLoE zSLu@S651EX6wi@0 z2#tei+t%Y~wiMZs@J#PCz6<(GYZGT34L(?dqP!r+H8E&SGcOfq4I88cp(*^XpJvR4me04uBqn*Mn?wXugu?SJL-q#?2lzBFV0AgeTsS zWI5}Ow3lVIEWKX`rWujVc%9L>k3|=+EbVSJqUC6>ftg&>AP*C#aoL!f4`wzgpIvM& zva_>MLvfW5sC8uI$}?4<$0ey(q$GIZavENE_qG%LMds#cdSOe_pv5O!$Rz)z=pclq zvhO*x(lkr5RHX!EWz;BLVj5Y^-7LkFh@PW#} z@imS6PQG^;Fgs~KrSR*^K73VaPMIU% zCUrk{knCIxvwnm2;jyIy>yPid8NHuD@kl}7%Frxxe~Ur+qD2eXvEg`3uh{LQTHFsa zmY()TBbGf}bthegssy4k#K-*CkW--{zN|0N#Qa$(uQ4P<9*0FZPtZL)lGh)OJsz^H z76R((b$D|qmf2L=DU&v!_gA6l7rkND=IT+>%-TWGu1pgaEyN6E&Ij*kG7C;4kV~2h zB7g^HboD4DT9#R^|+jKxrx>1D(H`T}7$yGFpS+WtQa2*I5bZ zH>kY_Uwbm<*#f6-iX0vZFa4-H-`9}35y%soCG!lxu-%AGa(*60Rjhrpp2iE1({5nX%X5DG~@6=a0ocKh7gE) zmxgOUu2ZzSvD&4L_btW|_R-|+Jm{fW^%c78ZgH!AOG3+|mwX|+X!MTtY{A+nRVPv- z%;U@t^KLqp8MP;nTcKqcT{#D^Q&gY!U=lUtst?-?0W%=s%rJ-3jSn>_#tJ{22e=ml zEV`zFciXN(T$-wxU0KJGKr(N}P$CE!;o{BkP2=!I$~iSJG&Z?zJq{3)%|OF-R3Bgd;afOKw9HOQ{$pMvL11?f06=b3%_6|WZW121V^aKkfGQ8{-SdjZyNd63@LCWBHYx2SX z5`M?b5K7t&$b8U79d1uV7_+ph$-OyXZ5jHj#r}a(olXsnm>t2>Y=yXfZxv5H^gd)u zz3fIu8@Ez~@Q{rA$Gi zciU7?rNn9E@p~~`c9(u?B)P@PtYglWqY2k^RhOjsQLg5u%^_4g^A&6?wW}O6`*@}& z0c3cYlk9_hNgS8qBlQYz|9&(AiA4Fpm8~wJ9Z5HF=HJDT^09=mHj@&*!tIerSg*yc zj&Tbg83=7UtQ`p{O4ehEZT5?aoKy`Gqshd}E96r1JeZ;nJU1JSu9lg7=lgh(W0z}9 znJ4$^H;~ZbwL`FQ!*(IO%^eDq{eyH29}|d*qw*xHs3@M6TWZk}H6P#YH;PScLh~o1 zmZQ0%;L$XQK`EMVjn##WUchgw!>>?y>+=+is$!cai+KI1`FY)U(I&!ec~1eo=#@zE z%`Fuw`6FI{7TC~#C4+%)s!9#gL+SIT<=ujOG?#Yt6%~W5Xpd>9Zpxgew?cpgp{Yo1 z1#*yLeg7isR77b5Es1|?mEp()NXoIrd5u6UVbO<(#58w<`_VlclNRUW!Y4@Q84~sG zT%w?B#e!K{HYBC*8=bQnw~OYkHN|1gcgL-W{yEgP+K>|YrfM29xi+yEv@UO=td}St zlM+AXWUjHOc!#+-CA&=*ZXGMe4j8X-tN)&F9Um)d4}ToCRbB=kG46p zYTyIzG-jmn@J3T&LJ6_?d}&|gkO=+BBUU$OgW#p+Z4(1dGbMD|JHRJMWFgdby1~@b zguSq1v%i7*vuDKAC(ufcdD#=aLHrf(#Cn-I!pK03i5WTesFzjSMU(w(0KD#zP4g6i z+(_)58sWC)3Zn9n7&iAT_R>OG5=OPu{JtJtITE|Dv+QoHhTWl86O&hHOs#uZFKFon z@~zteG}oJz&}U>2@IYDNHBx!00W~?DPqA15oQL%f<7cM(PPFfbr zsH5I{1wWV6VU|e0+i_Izjl(p>Gn;afaK1CqB%)Rdn-{4QWD2N8t?Z_GeQ`!PsTT+& z!{!_{W2aBCNjcC+8{GC?11307F&?#~cnnn+XU>&9B={`T^5wnhzRM4jo(CYwQNgYc zlM%|Ii}ZKD;_`^Bp-Xg`z8)7~jUec?2-0TsRJ{DM{9dOnM~eslbJ|unFH)(&_oE&e ziduhv#1AZS?-0gL89#%IA5N>FgBYC6^nr!u(;$b=%2vbT1Hs&DYd-;lX61}`=B?xU z#gQ5Bm(Gpe>1hOOL#8^PW?dcXuqaloiG?m=&C-#n1|vB;bdINlY^hGrW8`7)&#=ui z-G3%&Ysn~AR*K~(sj_B!nJ5!+htj_~1=8sseGeKg;U?qF(uaV9hib5Pj1F?VKIrm8 zql5H{iZ&*+#++;N#Sa0SDFj(OZ5oE#8^WN@&1tQ+#tkIzQV#0;M`eT#(AlO7OHP>yA#O7!e*hbz+Zn%BAxym?815&|)`HXo0svQ?_Kt#GI-f`ljRjuq% ze+bb8Ayw!{ae&EPomG$B^UF~UoWFF{MYzysMx9m_l{b>(jix#(NWzkYikv%`h_0H< z?FmCMGT;oWDjb(WE2!O|Sljd1RrPmKu%_+k!a<5 zPSr=PNO=ga!h*n%v~1FHUhnhRgTG+eX|PPFQ6!DYmcT`~J>}a_QQNHnQ)R>wd}j$4 zMF-VDOfDC%iXa5qcwgvvqg7!inZpV&O%@kEJv z`fK(85`h(ng2m#RggPLFEhx- zd6UMEip{jpxhfp=5U?5904FST+=)IY$0Uje2fyeJ2z<^5Q}6p(T{vcb)+pBT+W`)j zlwF6uDn5E>KXL0vU=D2pPn+!@(!fQKI5b=moM?i=q36#-`PjGy`s z!tD1f2E~EDM%v6L7_@39`V_Z;-6x2m zW3hpWXqRstFulH3}-{b<~tF=sW( zKXsXrd!vp`TYR|OH*Btv0bk!PhtrV>ON&74W4&@0*rC{6Be zB*-%3VIz(}ozP-BeshgjGZAlfQ27_?Zxt9xzqKqH6l<@7m7t9?<>25l<4u}um0@w+ z-+1o?#0xBdVtpRvXuU3`&oj5_Hd$0_V}o*^H_4)hCH@4h>g$dx6}MZY)b!=s ztI1YyJ9>`Bl|+S?&SJbR<>_#kiQM+$2Adauu&Zp~EdWQ*;7AShx{9%qA=BYK^%lg4 zlFE~g5EIUZ!=m>>tPSmJhZszrb3V#~lZb-%&Z;Q~Mrq^a=P*0Bv~lg{aRObt1~;jj zW>kc24$PM+-`ML&2<|zLBs0a6a=NVnhBC#gzWa-uCj;x%pWV3)zoNnC@^9CKfI;6>TPxi#HSr;$+7(phQ%dcScAIEl>Y+xp!Yb&nas7PynsI$CWMpbj+$f&{rwnf z^;$9Z41Y`F&F~~p*4Bf``D~F=5))^r-VUHDfk#9E+=pM$^F~v#r_dzfd5!`ELzp($ z2Ekm+ME!NE?#mYH_W7W}1`oi0Xk$(eUEPU4Bov6^n#wriIq1PsVsA){KB8j*_bUvN zUQ7Q;HOH3K9n1mJr<}nqCP0PQxG-k8(+nw)1TAwJmjW_{Z=>7{y-UMmw=u4-*=*+& zQpIoM6Y-JnfdKN-8(lHYVyiFQzw~G)D&aWw%f!SlICex9IJS-6r~1WI1el-c{tQcc z;l!r_3XdSbkEkUg>Xq#(O^-N0N0>-W5ZQ|@TtS_L>5PouD}-sb2QoZnzm`3Nx9j-G z;v!I92&CZ7_bu3G4%RfhC_i#WgvC(eh|nfdV8EaBsnC{ZnVfo#w}rFKv>w(fHQnxJ zV5;lxOX}O#^v~kQj}AeursneGp?`(L}hgE!5I+cnuY@?`cp&(20xGF-{do5>e;v_uhiks@P3jz#40)IF#hi ze#$5>MSh1&Pc2ioY``c8ul*7BmQgu1*A06himYUFY-`V|9g9JCYYSxfh~AN4(K@#mM_B@-Qw}*4EAdi)d{+n-715Gnc=RNJSL(zZyb1muJx0bEjJQOJAZz zCo5$*4rGX@ zOVzu`=R&OBXAd7FX;f)Op7xMj>`R-UK@jxlzZ|Y2KXGeeH$b5t;74wF6 z@EHnzwLjV&isjWLQ00?*5@GkO=}IcqWY>N*G;4Y%{4IIg%k%{mz_XlsfhqR{j3WEA zzaL6vs^YWln&?D2r@fj5OPw{r+%6GJa(yWU?*mFm#|i3pJW1+LOObIXgzBA1*ri{j z;wh*{Eq)OCHX8l$N4ofcz=pWMQ(+qCK%b6lCv~ZEY|C{?#zYE;Pv)NMhzm4^} z`acdCeQ>7ua&4~{5`OH5Hh;v$*BwgF3`vD3PpgKNp|4I0sRWSxb2~PS))!T50nqkd zIv+@{U`Xf)q{C?$UXl20 z7tC|UcDsT$se*TD4P})N>3v>KNeYFJwh|!b1XDMHm==bQ>DqpMP8OgHQE#_!p|H>? z8g=22oj!F2PDuLSz8o{tvBRYASYy&j@xWiFIDKzNx?VEA$S?F#8iQ5A>MfJugUw(hR?2$H139RK##sJwB;|UPncQM}Zf&$+F6uQ3 z9pNkL4bW<+OtwD93>L3Dw1B%~o(ib&o%<_XvV1*bF``4)C&1uBW=CymvLzeS%C9N% zS`Pxa*~*Jjs|1LYD;hAhw7BKZPmzKd%8}y0o^piDVIBj|Teu4d-Xhx?TMUcI-jwnI z3^59BeI1j2I;hX)@TB^L2(T(K7}$0$RI+5L0vC!Gvr zPEbH->0YS1hb)zubS~^nq%E>c&3OM2sAc>`-^R0M*uHu`cf4FR01R@y`#hB8p|cM6 z^>5d2L<0#o(&w&wHyyZu;g>!tGe8*3jwTs`w_Y2+urWa8OAeF}#!40w0)-?^MzvTUu#SMmoBNlr8T>#|8gY)paNn!Hh<^q1iC$!$+lZY<#HsO*o zJZC_9NC5wVv?}je9)^WT=32fCj#nBZXY~v>pR)up+erq+8D<=y2q578j#o2ri68t! zY7_jquz|XN__@ZF?)>v6FGH>w*ODCjJy7N_$&3|@FJvB=3_rsa_JAJ8ALSL+hmJg> zQ6P7jwjC|Zu>x#s88o#siYLa+5y+M(3f`{uZYndyJyOT5Rlld z>>+_zZ`6Fyxr|7O(<5XL$kkX$oK!yXXR^?4fDB1;*karZlvOk6{St#si<|^RbHem8 znn#x(SOjH^(QL-1K!xum6A92_-rCN45|DCJ2E+w&Rh&(#2p~gTiP>I|8Kwj2`$|$T zFx<9uBo6XhrBq(>`+++we)F|BZxqU$1O{u5sVJfQm=Vafkuy--whN1n$>fmld0G^pFa#$Lj2jhE)IFVDv6NTjqr50` z0a#+_b0s+r5BE7>;hbkWcmD#`z=$X&F`qE~EqgA`O45(XK^Q-LygoC7uakRoH%Z}e z-^&yDvEZyY{@GhstD`5-YPm%|K*#>Zy}go*nd1m3HEf&N+6LT+-2yNh3+@X?<^QjR zvkYtUZNs>VfWU~68!e2Gl8r_X5tzh~uF*MKIuwyE2S~~WqLM01B}5q^C0zslgaQgk zOsRy6L8UW|I(J{$1{o7368OL1*2N_jZd@OOB` z<5v0Mbk;TW>Y%Z`t!6QO`JbYbY=bVs$F3%s4g>w@;WcNly4~-Ank;zRt?0-##%49O z{}6tYCJ_9}{Se5)-;5OFcx5MrS-Uf370NIr0zO7)4fa}I@|8^Tq_{jUr{`)(Vq*37 ztA>wpyS?qTXy^~|0y>iYjal?k8_%8|HxzsHAz#8mcmx=_>^|m|u4})2Ea#P-b>AC0 zlphVGC1u)ow3&SC$Nbc@1#-uJUgUV};8$hKgPg2u1xq@3_VdVqB=2@Ms;xgyPmGde z+T6KmvFm-X=q9=|R{A9aS2mbrwA3RMg$rzj56^7zN>vP{Fmd$&!nkr=5IgWb#!x;>uMZ@4Puf9PdspGgd!3$Y|_UE>~ta9<<~BuAiM=aIAp6_v3M_ ze^g<5x~>Q!G)nn%iKSqd_2TIo*FX>tWD zYUy{&ebeMfESPfb{=ps;Q=Qs?thUm3KUcw#tWk13G2ka)*hv)AeQEE~+|=DU!5i6Y z+sbjJ?8WrC4p=N*-v=dVY3e!A7%EX9o`0i@U5+|@+R=YePCx%_?+Kc0+esYc!u*u> zXuD3!CV=I;xEWLm8UWOU(NfPJ2_UHnY%-JA57iUmt~MR zT~j!AE2N)8b?V&Eq5ntX=9L!AEbN*7#^J<;!JbK)mcq0yO+P-gu_;RsO!(wkzzevM zytpY3%*m)3%h`^!ed4K_?^nQaf-xmjb7kh1jeyqYh5$=ixG-vGsl>t1zaCO3 zm&RIcBBCtM+7hJTwms8WI!ix5USJle9v%oZaY*yLaO7r@gFkoxG4Gt2HD`91N$`AW z8Q4|G8!m`@Zie!i8&^-3DCrON5!i0(hmV3E@6C_5=vSDgop1OZMIu*1Q9J!lf43RG zn1d9(*chE8B<(y4DO4UVjk!O%x$@hG6P-fkQlEd|g!K6XWuTlEtR88VYkBti^VGIl z8LO*fWtpry<9ONaWX4!-3%4=E^WLMxKw;@Af8dR@%~TX0_}f1Cd>2hnH4t>&OntEu z*cb@#hOhZM{R-M?JSLAMtDNu&mHKCjO$E&@`#y{<1pWkL++LtZ)LR6Upxsp7`iqdy zt)ai%=f)K>@-SPPyE_dMb)$WoG82~ZEPtqGx29-n5PR8G$W^0;D8f}K8>3M0khPj53-H3q!dyWIA8yH)Qrv5czWN@?tKbr{o2?!`q-*y%#1# z(F&aG6vL#fm!qEh;}R#XBdnOLdGBOK^}#QJQb*iYVfw`irmp9)(HX-rHO|^B-_a6e zUb8~P62ewy+1BZ&_>d*kh*)hXiN7+{0XGV2TvZ`OaJa0%mtz^IC_Lb4sv0~8&?t2g zBbZhZt5?;%L2okh8gS!A0hs2$9EVS`kMb-EYKwGl8#!%M0ss&d*&556z*(T^n>UKg zLdeGHLSKe`rd!4w8E@bXH!7$~&E%7@pB~p*OL}Qpc%OFg^DfH+ijUUo&dx?~Ts(gi zIg;&6%Ba>7aj(7*^yTA_g_)Td0xnhooYm6akF(pHJe>VytvNT*s1{get)GdEM@X7l zOrd_ya1-Ktkf8awJ;d6}yp$SCxUMa>_1C<&=K9FhnAVYq%MX5PjAc9PbbV}aY(Ehh z7Yx*+x0fqy=B_R!rVS}gXmJ_MunD;qW+0zr#J1R;jpp$#5^z?fZ;NXaIPyyz=bQi zlNkd>z5=N-fDo!BFp?mJ9cwOMd2!Eg@m1(sls1r5n3o1f`(XEG$Fe9)FMI$jlz`wb z`P8S3{Z4(^u8x=Su^?O|{7<!T};%-0kYS39`DX+ zZ%s!-V9mdcv+z43yX1GRordl%wK&DYcdq~>K$PacQq8Y=;qP>uRALzBXK-w*&mRfb z*#tw;9K?xG#L)~9mBTo$dNF#|0p~Yz1k~9lpIaqp&pp~e=kL!~fuQ2qwD!Bm!^1zt z$lh}3Vz?o4JOGhsR2Q9Yhdm@Tg=Yfv`j0eV^hD> zp?Y)zk5GESU-IMqt@lwXk$-?dsSobD3=lI>duEdvAJ;MAIFdYX&p<*FPKd~h3g6wg zWJm4=FL>Xz`MYzyM(%^OSV2dSoz`y z7sTt&34}K*fZ!`%kVBOV{D3YU_@^3LKM@qa2taT=33dB@4aE6m!rreE($iSgi zw^Z{fRPk_WFTNIPyyf%en}`AYT9K`M)i6_`0iiG(T8k20X|a5XAne%($ytYS`KxUL zT%v04JD81TBSPzK(J_1KBJMGSijyrI3`UK!vb)ym$GI-PK##O3Z~WLy~X!Vhvpz22mXGpS=^T`9j8<+%5r%bxYR0~2Lo~6{^He+!MQ9a za7uMKJX!L3d$1sZKTG_mYylw#Z;)OtGYbSY=Sfau!knwa5iT`8n$IF~X7-bt|7Kl< z(!=@$8lIc=+~jFlxD(te>S?XJxWMu&v&F1968=MGyDnPqB7>=hUPyqYpv?%;eB@@| zydR;^G{he+TfhFLag&o$4*87g*0}?Dp_5;HT^%IyWU(cy{5reLM_9eR)fgk0whQcf5YUR$_+ae4i)@rf5@wjF28vOiTpTq%U>2sF>wm14`(-L$SRc-_ z4Z9O+Dld#NY3QH)h7&b%8EH@-pWn!MbaI#-uRvm^TP&S&$r5H+u+Ux>+r+L{KN8zX8IY zY4v%Tb+0$T!o+eWhGU2gz0TKeAy+F+5yJ zGJ~)Fuawj>sK~j^l=@O{dnen@g2O;Wu>`%^$BEA7_rCQRd(au{+V1EjzgHHgFs(2x z`3Ss?g>TU?PG@qB06sMs0k305?a2);s_(d90a2J7bBUMRhr}_Y@6nO+)R0Wt!eW8= z&?BC!RiQDdF1;MDJ4NTmM^Kl2^ss>G{dFPG#=)dwDrK!UcIo~7S9jousrV&lx2apt zm_^oeA967g6sIvt=Ux|=lA0son^II&@8{-{_95({9F!YVGL@0cydTr*ineN2v7F>2 zjrD0$W4mi+X1TmlIg*LN{vnlZ`NhAEW2`1*xh6+eqXBlz7%I%nsVy~V@5wRnK-4qt zMWda%$eM9oc@~@Vqd|TYqoibdY5`bP4#$sN?J<*s+8J^*dGC?+Pqj1gl-D-n`j2Bn z;E*qeSBPW4LG#oU2RaiQbt_G8<7MjJ&Al6cgwJ=Vk)WaP?cTUtm!sY?y2s_!*d*hG zy@Wr?{DI@xL+=>B5$(3J^)yoTkz#`r_lrZ(AvJY;dI`D@Bl`~>I{MnGl1{w;10^it zO5D@_vYEZO-(XV5VH&Bp!q(4%d|Dbz_Oo|S<>AI<{(0-bA@sZ*dAK`&VEb!waA}|0%uhC1__#mD!=73k4^jvs$~bGeLm000pYA>t-}uBF2I5%23SwEG z-9b#h*S37LsCU@;7J7+Rkzv6hZ9?t})P+)}x-=4lk^URaF>eVVOwDUYvsyMg_+sIC z6*nwDOp{x&pHJ&&K<4bhZ=Vm40L6_;y72GrzTLk7lQwNz<8 z-I0T@7~rFYC(ahsBFz?wYX4kj(EpL@kBE!>RwuE9kgje4>&v$vH}t;a z7H1qEJ*fQkPKMhd+R?TbAWr<37X(29CBs0>x#h;&m3h6Y!)Ll**a{gF=u6w$TJlIa4zyGw@% zR;Xkr79SQy`I(S1uby>Zj#$-negpFjNyAk<{it#AF83K=_0~M+@$DW?y$v&$?n<>G zEfq?yx0wDDZ)XQemG|28j zIeZhdm{YFLlfl}=4eXGwTSWJKnOc1jE@oO&%xSJ^;$q!eYWtVYB#H0?YKF#dmjBP!EJAA#8CNNXLtv7{9(isY^g+`Z%WISxzb%!YB zNDmrd6sd1|X$;C5sY zWy)(6#gaBJXrTzReOJ$t!rDIs&U{l@c} z^~Fy-S%jd{J&gh%A>xgUWxHw$I(L-A;=K9(-Ao@Kk-+mjLp-|KFol->eGmPq66;;T~MdYQ=aMrj*;2@ z@Z_{m#jlB>Iv_E zthm{h`3Ce;q`wJTfj`3H&ljhR-}XRwUq4=Dpei|4o7ZlncvSJz-6K zz2?MpFECeQc42kF)h{O;i3;A%qV3=MfBt|_M%O>Of8}3)deEYTkAtC&46f=wz3i6s EKSr98M*si- literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/4.png b/public/assets/warp-posa/4.png new file mode 100644 index 0000000000000000000000000000000000000000..fba7b5ffa6ace087d46c069283e8308afe64b834 GIT binary patch literal 21230 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 literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/bytestring-splicing.png b/public/assets/warp-posa/bytestring-splicing.png new file mode 100644 index 0000000000000000000000000000000000000000..297a7756175f68c59f24bab4f8162a2fcbdf84e7 GIT binary patch literal 31832 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 TxQ)9200000NkvXXu0mjfCUy2S literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/bytestring.png b/public/assets/warp-posa/bytestring.png new file mode 100644 index 0000000000000000000000000000000000000000..eea5969817a1c1a2bb679e80f87eb9c553339c1b GIT binary patch literal 22183 zcmZ^L1ymecyCoLfwQ-l=5Zv9}2@)hYH0~bU-Ge(pf+R?A4bVuCAOS*<#)7*~aqs`$ zyqQ_UT68znr%qKJ`Og0K-iH_sHF*qF5>yx%7z{-P87&wX*jC{4E;1tU>L^eX2)v=V zC>XlKz@Xv1{KCTIegpxf=IwM0JPcG-ge;w%*v;QNTUfFCIJp3&VPHgjgn*AuRvzY5 zK2DBccOf59ntw_N0iRzEbI?%zQ^dnTl*T|+gG$=j&5DYbor|5DMhumTib}-oy|s{* zjNE@t2mTVJvGwq95#r$R_V#A?e#7qUX2ZcHC@9Fm$<4vd%?6ZUbN2;%nESAS-D&@= z|Ni;+K0WNL|M!)^?*Dl#-~l;azTx0v z=j8bBx`9(gUXBV$J3G0!S-HCd^^5U}{8RG(IQGA<^KW?#TW1evpcUNgEET~XR&Kz_ z9_BB1C&vAsv;Y4k{_ncf-R!J@r~c<`uK%3n3ga40)__vk+ISMqh7^(=z ze|MP}s?#g90vH$x7)2RL9Us`ke55vf-I?p8ZwK}Dg7qD-OtEl?_^z)sQ|MwrKSK;< zOx%5jYMGz}1O$B{C_}6cFbmPq*_tY5y`Q6D@j#pn$M>twK@q&ImB)~-S-T4VihW-{ zKcRfFNAY5{`D*Pi8q(4OW0UQI?GxXzIT6rdrF*ffP!NLj@!pwJnj?eYB15RnkbSUk z!ghKdCKTPk#Sw&V&&SV?;v>Nd*KQg};sHIl&z`gH^UK%92 zhIWE`VotXz+~RB8XHV*9`I%p-Vc=dt5{8kq?|(l(zT>wV!@s^afv{Taq8W3 zpKs*1SH7xegks~earTh3|+wD~F)25!G6+!DsKU!sM zpP?`B6`>(h!|=*-&s!-fj`r8?_qRKRs}Hy5+obOTFLhhb#!kBL%GAXlb%fMU48ZN% zDQn>s1$^UwsqbFJiP0cZb#<@Kit5go5H|#Fd8uZQTyK z83WUDcL)8ZM2UiSYVsnr+r7#{GFl6uRM5>Iq&=CwsL4;MxNygtT^<|5DugI%4Y2nP^HY;Z>uzU4h&s?8^&&=nd zLt2eVru420GxY5qX8*^H?nU*k&+tSQ-_}4WL~34>Pn-9z)Ec8Y!20O86=~jF&B*JS z4mBz|_!3Oj}-~EGvrTar~6+Kg2{g8ee}OSY+bGSRKI`JwwWl9_N4_qR0iie$A;PkTftzZY916*^g_e?hb+;k z;dA58PmN$ozg=qY{p!jNF3aJz1it0(CO&^6>h&8;!FLyX8%iuXCA?45oxbNQf0vq9 z-KRu%_Z$E;lEC`nO!^hhZ!SF!=GGi_u|<@|?R3f6j>B?UvqJb_P3k z>{YmtZI6u6gZ$>%IVKKA84Ld|w>lr6a9g}bevcBmIG=id+|wg`+<9)iGnp^T>$BoI z(oUNsqQ$8hMj)i}be~iThh!-mG1F`_6`&Qz+7xYMnZUbpJ|$i#uJ0TSoy=#^Vm-I} z1^!mIu6C>PQ9!e3+Re;W2y~ZJx?#1Ai{59Sl?5fVyI=D^&S*CfwegpxVnH5cZts&A zP9#4t_1mLgsMcoeD{@$tDwd+`{IfTk<=oJJ-UGq+he-a8bavDe$eh)_D?N zTQBt6uYTYP7JIs}9QkmNq4&0WJ>b_@r&XeN0h%uL1(@t@>u}eV;*X=EC%y0+4~9K~ zC+mGjUlvT=84G+$zHOx{m&Ip)k|{Z-IC$(3-z!U_vk#~6zYe^+aJ9+z%)Cy|P=+O8 zqtyA<`TfsMVek>A$caRbjgftI`P<2sRdeJr^iYEwQi%1ERhB2IbEoUmn{S>64CKse z*$XD#Kzwq0T8DiOfyZR>W0@%|{Ji_&Vd3dvM_l-;AN^Rld=ka0_ypJ?AP!U7_lc#t z=J7ej_ABzBh#R)w;MhI2EpJDZP2j`61)=N4qldE6V6(VewzOU?I}qKN(#fDxE4p;y zrSCGud1jW++>XC{qRFzJ$kpvN%jxOWC^3z3lk~g9#lh_N6O|M~lhR|CoPJBitJ~a} z%@O>!aYQEnLRD%6k`Q9?%})UYpOAx9tsJSAKhXLYQBkXpZ#r~5}Hd^s!EQD__}Bj zCrBoZL|v=-$ZQ@+L2yBA^RYOpsnzXuNa;|ReHIa-{91=pByrgc6jVh*3}+}or3d|b zJ}E2=wXAQ4l8i)S4TQ7QxAlAOPWO54&CqjFe5(7J`^zfJBvx!kU(^i`5vPiamJ=$m;a~WwK-4t$4P-;&sPaFkYn8!>(&;z;b#uCDBB~xcFvv-^WEO^D{7J8FMV>Nn%j*I#t@in9Nbc_sip&Ap@?VX=TJP0!}619*7^%c(rlU1)@PCIKpn;3>X34cQM$NbJl z8PSqYD2a^GT|<_`d7TOauW}3&>U*BNYcR{V*9hLxi7f)0<+vNa~w$jl$b%-4(Z?DzWEop zAjgMrwe}`2!T$qVKyY^QK>20m?`Jmu)_@HPUCjQK{#tvGRB@e*?gl3AJKMfHb*~+3 zC1?E-YEO+<60njMCG1erN>fcB4-T)mqz%Cx@nFY|7?yQj%qYjv?({rA9w(lVPXo&U z6}Zwxbo+O-nZ`kSg&Pftu-aEcM|Fb|+C%}V4;4TCh!JkPc`~w~d5d75$3~EIZ1mx5 zw1nfq%2CHb{oGAHZ4g;~1%>YxPAd=qV$s-*!*D>Jz^dUMj(>`mj|`5o&!5p!i+_ko zo-7GA4sBGp`8j z{sjtyN65X?o&8VxkgR_2B)Z!r$B#XF%xTKUv<&*vRiofBCHn<6IjH1lGTz@glcENb5ym$4!ybKgpa;rk2jFazZim=tp5h zMo+(<*ME0t8gMGR(BbJ2H8s$N2;-6JlRJXNqy=Mok|cJw%b=F!P%QHHE|N8!=(j|C zkL%`e46R4vK-=7=Agy!}E;53avQ|5aaFQ~MF~VW%?;!4dhEmJncpSx;=ikB4UHAK+ z8qR*@d#~-WcU@>*A2js1E#(J4-ELs#v=h?34$$0i5yZ5R8NKg(!U7gghCZPJYh zOnuI0Tcd8G`&f`ng;yk^A1;_G{o*MzBhE?!PKe^J!a_?@sFS@A;1t2}8|DZjWbW|+ z`qxLR5uvx@Zqr4Qw+g=$u=rP8!oC3!lDt^Nda8iU+%dawz+GHs9pdzLNsa~uyJ=Wi zveb2RBvp-^hNcggdyGNs^6?;QW+mt42l)sT+S;$*ID@$rJJEU{{-$||Kr87Drz4+$QpAPQ)BnaKO2Zye8{$)p%~{rC@Z zJ69E<9cgZI+Yw_o0A>aA^{m-6KZ(a}XLv%`o-acssGyNTA0{!s{FX!_-wJxVwK3NK zy}H@^<=jIM6TSGZ>P`B|PxWc(V4e`s>lJOCbicx*Fu6^+`CH%?qr=QV#H2H5jen8} zyC4ZMuGu{1X7&*-U(sfvMw(m&?vsMtQ!!&xq$m1f2=rMbflx&EL_Uqp7AKOrK@U~5`2LypbDwOFUErfpX|Hay}XymqVtP){EY z6?MC(hubqX)=T>sWZv+Ys~E-tre7=V?&SNHIxRMZfnI%;+`&>y<}Ur6m07)UR(Q-N z?iexTy5)t9u57j*M{lM?v}vmGT2-n8@2}1ljMV(<^sWtGH@qGC-Tlc6hbgbUkr2h* zSMCBb#rX_(UV?XlM}B~(E<&9;002fZ(m^t`eeV8(j{uUhNS)fv_s~?l&7qq@NElvi zw3JU-&W8UGn%V=vDOHv1(X7--QmzGk2lnCf5gIUQRXodR?U^Al*{_*L>m~EAWcrJ*o%Pg)KSs7cj?+{DV{OrC13B) z(=s+|)AjXES^`Mmd=cCrR@)yV%%#reQ!4@pBIzU@KJFg#KSyWI_a-=9*_dWf4cxnEZ+a`~=U$Mj#|^T3vImXQmX-Fxq{42xsTr zMio~v+N%yQ+WWNO1yH*|0O~yB z?fWT`n)1xIH7AkWi5ka$hUi9CR^&*X=-l;KQAa+*Re!-9d>}m6P9Oi&(7k^J;N%kE zInm^}(<#SckSRzrtMF>E5TeMM(eqSBcdFC@sGbPv;_5;D%9fIe#=MQezoNkdm#)m^ zX{T&A6xj81oCV#~j=h2Nx8>A(_|T^L)H}gi<+uS=f_xAg?Z!vFg{vb*-U_^Qoa94&yQxSuGyAPM4 z3q5&{o@vB(^3@qLq>g+Z70wqb7gmNhB13t6;)B!orxB67U}v5iwRk$(cCnTAXs6D(Nvx>93k=qNE%&26bmAV&((uameR*f6mCdsQQYai$ zj5h&zR_Bc<_qd_!G~mn$XF|ECR#m#BpXq z8*O&+hm$Z?tp<;Rno}_svqPGrK?72G?4S0T%CFIDI|-?Pb%9wo(M#+ena3O~Ks@{l zzCe^p=oZNJ6#M7q(A>jvm@pNb2H%ugM(q8t?F$X0kG|&IU4KkRyZr;aKrk5RFQv?q z-76VXFoqEk*W480G4)In%9A6`>>+iJpLh|@Aga2l9u#dgCGh28&!;1l^vv#E9wi=X z%&-4^jm5BoH8$fmw)upA%3db7hLu)Mc7v5RU`2xKDT*z?lvZh~C2xLZRvebdvu?Ki zr3fDw5O!5>GNayOe}BVY7wr%%7tUAHI>->TK?{R@5*n_P=2da;UiYtStCxxYRc{P zl>*y#%WNW`j=R$u+^9?-v;IS#k{~SA`13rRwSFJ#;wLF8vqL5X+pB15Y~0Y;yG%NX zJ$NjU^+nPMX)}N{I0Ar{7f07c>NjYvgE9LDzqatd)iqQSt37`rNmmB2#Emnqe<@>x z`bid*Kn|bokG&u7uf1DW+?2c4Ne{eBA_KNk>i*hnz3Im z(PqfO411kV@W*K?9{j8`sZ`7Vhs^-MgwbB@;yrq3hXimdbxyy&!tH-M?lvM?AHGd7 zSo!na4~Xe$ULYR!_$FG1e$LxTq|1QuI;O2g#L>htH%WL~Snb!ZEV50_VyBG4eV#5)n!xBDOOF-(jipo-JrsW+w zKGl>Nh`>C#5{-w1qc&1yg^;`qI;8#b2k|dJQsGx8xQKiO*H}H&xJG3)HWK1Q$`Nmz z_~to%bp>+V60WcI`0-uM_1IMc?zCbg7XZPzKrn4Ceg)8EgacvodPb^ce2A|M6mo@F zSW8C5kSxEQ4wG83W5i(CEO7E=EXFs|$Muwhk(uBK05TE08^_T-Ewb2+04Gpaz+;zc zu;^RPXOWXLrw7-dheQpC~ozD2&IiiZ?7-F-9xj59!BE5)9;O(MJWa|l>KKE#Y5zjj`Pg2)Weusj zjg6S&bKQ_hSq9yTU$3|yIS1e8WvAvvygy&my{oCA;iBkcPAzJxReX=JarMjjINQd$ z`yUpZIZ9$aJFl8Pk7KJ^yHbg2GDF{VDG(qaXxf4Fe+asq8&$WV4}ki6X;P<_lqxF2 zuYDo!qsYCmkrw#GnNJpzs5LYZ;@frckIqaZBk06vPv!cfgO^+Ez`)=pis^+ej}3gr z^?q$BZJ|9};qExR$gBx>g(+Rc1E*^Vwq`Tz&^Wol(jI8?SnKp{Do*7=DdMrt=-#k$ zv?r+kr??5pm6&wV>z6hbP`9=Soct)L>aJaB(`oLI?I3MGpo@o&DYww>Zlmd*Gnbi& z3TFx6c*oH89&Wy$jhZ7)RH=zgO~!K^a8|Ea67VR5Y@sRf?Yt#pWEiGT{ujG{fSiq% zOQ!b(u@oThZW($)zMZ1n>$4$r#$)J~u}@nk)&aa2KyYWi8P==oZ@eafqj(u|KKxJg zDA%W(N#aU7>!a$ck5;Pq@f3EMLp?89+%JGXlhu;`Fc3wzrVw9~1QVa|nHSRd#re0G zGeI@mkT3}#*%Jj`e z`;G`zi$DCq0#}!3rhWli2Wa2Em72SumX2)8d1E7F<;W>a#|I)kT{Ls_xgOyPRdmeY zp!F}Mo+g9SH3tJKLgrRX;=uQ9OTWHpGdI?m;W!a@0gtK?%HV(}iUAg1V6LqNoU67z z-5q=bV#rVekbml=fNpPyN#SwNMN$KD7D>@Vi;#7!CKJ@)O5nNd--D}|GBjlSA7a`9 zrzMfv-vvL;3u(S${PP(6bWV8<=p<`(?a*ABjK_;vbsAZJV$x8HghqN}?95JTY}n8m zPJSI>1x8jiKKoc0W%w77#N77VQO75UX0FFHer4HYCon(z+ceMyrlSKU&7>zDV~SMM z5n0m8lzL})5lUz^2)@uU^LIL{6pESD!j}fi9hON@&75wI2#Y*lAOHSc*E}gS)Aw_d zsN!B(Ls5%^%g=%$K@y~r=&R0Z_^ZW^R?;$|vHTkun|CD8;-sIQzs&1#7s}Lr@?+GM z?>-x2IjXKKaJxzVyVmth0SUwg(#3?0|7d2c;J7AxUOPc&_O~=iY(M#LJRm+%ts?6r zwEtlvC*cPhci_}W9GQW`TXTfH>t*c0Ks@(J16ZX|DMdQ>ZN>Sz6#8 zx&m+l>@`ZiOTBCXvgfswS+TLDiC|V??0vt~V^}N_FWJE=pyg!X_i44)ex?G&i4e9l zPhya1{PHW09NVtw{vB?$4+J}o)Hmb08nPRX>%bt+EGMpF=DL@U-4v2zlml3$WHp2! zagW_;dN)H-$4j_YXTRH;k~$10TDE z`b!H;0h?*|xgPsKp%xgn46^)5IVQXaGqDo$Iiz?O28e1o+(Q^-0vaEYDhXJ}^$%$! z0(m0!?zbJ^8Qdf5OuW@9E~jn3{ApyB8U)Ga+SpfUSX#rb;(-GKRYF_W6t)6lXpJ)X z&d`U&{E$>HjL5_zg;b<%gO5Eo!{AB*rei2|-8p9Z*1|kS5_4}vmbr0PEYX9vL6H;v zjF=JeZnYz6^IlP!EBZ}C*{9n%(Vdc1==|9DGSMaTq|m2Joomg58nFh?jLy5)ypV+W zEKUpy!KIl*@)16b$238`O$7=J4j8JyK%Fer71eA<>ZZH%7Z ztlG0*eh~}}mWB;Yw{)eohEmj3!~7)3MyeM5#^bC=ws`GxbLH76SvVNS-_4ksrVFbi z2%a6aSF=6&CvtY8++AQy8@TI1dJdx{cXmAntorsd_EjF}fOE0^no+u=tvjo|Vsg$%;tR6UQ0~ zEZN@YJ5yNc6*crJA}DEqc=V544OEScXA9XK&uG`BFEE-`r>Eil1&B$T;Ep{8Z-5bP z1JtE=e9&gTH6P2AzZ$xK+PeTmxiRzUXo>5^mV1ar#Y+)w!L=P=H0=!_0D z(mpjMJO$9gfon*dAx}Hc9`~=LbN8+AG;`JgT7f#BP9~PfNr*pyiC2_4y5{IkUa!%= zFm$Ip#X<&Rro>n(2@`0M-SXEf_s7A{Mqhg#51soBVjA>fJ0TdSi%cfSyST9)+hcCp ziU_vH_AVNu*VeZtispni{$&lrOz}Ltt14Xl&aaj{W>v|5E!h;ZDI*!k^_FcoG;}d& zA|h+{N6wdUb4q=G*i#QqRg`Bve7ghb=EbJh%MB{QDJl`!uPR-E<~bkr7@vU{Y#?=X zBD(D^fMlfD5)p2XC5M~pO}jN zE-|&ZA#Y{_dRKqfi&&8L{f_seA|56a`k#j^4Yr_u{6g8Y7b)*q%Wwpp0>pX~0}Hdq zZL7e-gm3W%%Zw6pecq>U4A6;{BW)ntkK-ib+ugFq>|6fN2%)Dn8~9e+N-hO zheXNxwlxQ-k|LbCg|=qcCKvW#1A}jo>I(V7d(|_`*1ZG(L4Pqw`5>ZKi@usAx}Elu(Yq4?U2xb7Bn!rYmZf&;4rq|kPa=n>U4Y?0K=oaZSX znN_YCW_}9QIu)GZ?hfby*-?Xj1mn6Q&{7I(HVZ)S9H28wvEjrl*|Z{8S&rU4kSB8u z51*GzNE{RoxN*sO*L}DDN%{>xdcA>^v}nb7glnpp%JGYm=^)uy6r{!9hbxaJbGG}d zwZQ=NhF8N;V}3^1mIFv@3S|T;#WZc~h&Zub%#w+T`jQQ_@G~?*aS%~O&h{Jp?T~>Saqoq|KgzaD9F3bdUvs_r6BQReNhvG8vfUl_ zFeAvhe^6jl;0j0xZC8iClnYKP3ibLw#F~8$Z&IW^Y~LxEi3zirDiEcQgS;x0lclG~ zWWwvtgT+{eF<$2KDgB$=+mD@gRmKHg5dhHr)aV)U6D>ygJP%d1iT~jjt)wb-kACcJ z*~$)phaARzjS%Os??Q%2{nOZ8Ro=h9jqMPUAjuw9`PoX%kaM?Ji5z1A&>97GCD5xH z1B;wP6jgJ16#r63TZ_N{j!leBB(g3j04TGAXZd|1vvQ^uOA7!q%itLz1XoJIW2>>` z$u_SH+ew+b6+9=N@!l~2Eny?f*hQs*IU_O=^-nK6R{Wv6xk7w65S5)<0R<>hDN89L zmmqSFk=Y}6hbTpBm#0gs_L5>(A~o$s01@yb+#~*g4UBe=>ub~ zz-~VpM>;IA6NE_s4!uKDnNZu)^7BaV4uB7dNRc!8K^mN^Gh}~u;b=uOEpB5xQd)lC z_kK)){s3i+SE;(4)4naXL?!R>2@(&gdR#1-0TSLr2DOh&`O>Z6RjH|{WGo>B9e;bz z{RvX{44Dcw+P-Ax&Qm$>qxPK&F8^1)780dlNF>hgdhM^U2nCBelWSu$h{Eze?=QWm z8$v|SEIB>W=QtV3e5XTWX6PAoHR901Qe0;3OdPJ>H>goC6p;qF5$+iAH=TnXue!B~ zC3-`$;Uq0svy#^mMXwA_8UK<8phF@gq%qX)hei>30`EMofmJ&$t0HrTLmY{%{QUwv zwS;_y-bayW)R?h>NTFCI+z1VCF<_|D%)H^p&e*k$#Syx(z?EwBX9-M9p*>fzE5OoG z*@f(dFYVHm;F7CxeX!dnb8kGD$d|Tfv0B&K51c69>5&^5#fRPZLkvtWY;8XO{qBg% zXU6w#-gH~c`I=1vX8u-J-z02y=7kk9X8S0X-v$3PjfauSY$_nht&$&@D1xFG6_uM#4YK|~*pJwy+GFZntO~+0 z!z7JUhQ4Z+cu~ti29AJfr5eKiIA5M+N^&nEoXc=mB{263^&=O*YzgW;kc1RsqF|Qd zuJ~W>&nq+N8E)cbf{M9MXDcGnrr1;AajN8;}hWGjoQ(5Q;yeAbv)Y# zAmKr)Xksp0fUNf$DY&fMj_1d7T$=twr?IjKiTD|M zG3sp4e`8giq332flIU4k;8#=T4Uq3qs`cxFnG7ocw6X{_cA?&YJ&f~pC94;Iy4g&% z$;29xY~h$0{?3MJ;lz9plF{BAnZ;In?z=4Vv!s?(w;zt^bzXPKU9Vg)VYmj(Yba&S41 z>9G^Lvk+Ay99x#@*_Vj*E&Ozx@yM9SdS}bDk2ytFw zCr1F_ye%l!9#Z1dv>=!(@P8mezj2mHkLGm!fuhEVVMC-|-Az}&1$2jZof`FTOa;BC z$iO_YkuaH}*}tZ8-pG@oHg`}|mZN&L%n^yGQlAyDmgRYv67vjNVJMn5(#5|~RZ^M# zau(Y<$zal2+u5XPYM^OR9+Sa!{z;~?HyiyxIc;TQU7&!+?7f1s{b_U-HX(FaHe8|4 zQ3P#|Azz>HVYqQRtBIPB3cBE0v8TE)#CiT|}k-e7C=^^dPWiUyIi7Nup(LoPp%^0E29h!8Vzy@0GX zxFjezScLLBL2h0)BuJmFve8?@-5SCc%B`84r+gPh8HA0sh>!5cDLVBL7)8-J;#}Wf zcu*)sL$!kd=5UU~fpOJ~5#bOSdzIWliS-hB-EY~hvRrhWx3I_JLqs*bCA(KX#8L4> z9*`d+Tn!*?01haQI2_7=6Uozf)#PEnk{555D6pDeUW{JV5zB>uLZ9yzdR7YjkL-pm zU$e`nt<>lU$dEA1VI0)(B-4Dl2HXw7%C?tGOHw3s`9S7rGFL_}?BxMye@^c+_5k3_ zF#`p#G;QOKfxBQQxqK;kKBX8IxB>)@yAStODF{ybpw zJ3L7*lqrupuM4#ag_PxEo#0e846r zNH+!0#>;)X{kZ!IG|70}VFIe~i0H`)Rh_Qb`!p7ko<}Q!iCege@WWmJ2>Rww-!3;d zD-%Wa;RZ*Z3`dc`mgXi(9Xz7>=Bu*=&ha#Lv86-6ve&=T(0QiR3bduL&~(cW z0E@F8?gJ4wO(13q3k9c6ZaG>=f>L4)nNenKkMsl=H)1%Rm^P58em=XxJOn8gdvU4BNNwm&AneJ;@OT|M(<|p0M6YWb6`(F3~nw6wb6im^6^&sKE^-#Ba<# zmalvB{(EvjO_gQ|Hf>LVs6UkUIF%6NoB#DuqJw!3X?!i9wfhp_XOLD$d(0}yc&!0W z3fYLIe5(Ti&hNRPA)#I$X zctWnDX)c|aGvL^w6V-9Qu}_c!YRh(AxGEPqEQll# z)w{c(zI*yY?uwgSabci);+U^%%Fa|5L#dBBlbK{HOV_$e%Qk}v<-bEPBwrkE&u+|# zDfNKEOIMTH@6Dh>exev^AJ6Mjz*DFTRZ3aU9*LDw5Ct;ok7bHN5aK9uPT5iU_9+?@ zzZlEU^b#()(SFmP^f@$-ea~gdqXAYrNBBwLM;kUi59*H*&gY{_rm$BtWnHQ24f8FN z;&;DwCmSc)Gs3hYkIc)Yhb+4z#`;s?J^pZ(GNpA@k%98Le#G5{<8bZM%_J#-u#0kO zmto!g6_7M9uebI9BY~^%VN04qp5}S>+F`X)IZ=i3jhz1A_Kp572~I+e+T5le*KYS7`t&g>+xG z=Ub3;KHwLbNh~!}F{G|5$)w=2kV!AwGDK81h_m=S0E4=hD|uWTML{BTh)FKg_SNZ^ z&_0uutchi}&q|vsHwPtydD=>U5#hia=dR049Ug|Kl(!(9f$XBvM4_=&*AepKwGaCz zfU-*@dI=Z{88X5#F-^oW7+HT12VDG+PaVWSec7m2Q8fIqH`mbM{&{RcKD(a5s=S)7tc?GRZFcbT%# zdpfE)(=a3~%)7l;*R)=AhafDui-ADI)nIR8sO86SYie)_L``UqPV=^3U~grfiZ#)@807}w zZ%E??QJc#k$#YfBK6ZbMb^3E&W%hIOR!Ayjg&G(DK?GMYn`CBT>!U2`zPQ|Y^s`-w z0AsOTfFpJWCrWHHm%~$keb3AeOZ$irJDQAqMLb*GZMo`Eu@d+?Lf;}QEVXkF740U_) z=!sG^?|JbRH9Zax0&sKt(M&54X)R3%X`SSxtM)*n!_>J%-SderggZJM#8B=FI>x!F zKZ7XXoM0(sHNc-BLrf)6!5~1|XXk^;P2&vgnt_Mc$6)5b|F|xw%mH!h3k)YNDRj_) z!+c_-6FU-rci7yKQ>32j7R6OO50~X&bDGk@4-_L|sKCG1Y2kuBC#3FraqQX0Ll_LA z9+ce|LZ*y0XM2j>el@PkH0hD1kS4G);l%++oZU7{-~yQtg(JWW)G5LcMb2(>!Jcjt2Ag1L$u}(OVR~xyW zo^MywU1sduSkKTFmW>oxP9o7e2jMHPmG7le*db9n2PtTuPx=ngtC;#`PK`FuwAp@Z*B}}$4P`2Pl)ez8IFCYAa@UGg=NLiC;h_L z={Lms>9=Z_xmpIdO{sC1SO41Nj1d-~IJwXAEY03(OvP@q*zVNtC1+Ob1QKn7cvK6h zO%!I?GQFlfcvTf|S}DaT?!=Bo*&Y{;%PqHhaP16z*~}hdn=P~_#j4LXR?`V^Ne?r` za;hnfGkkx2QUqJ22)9I@dE(T2@oJ5ez;i%BX~C zA`i^|b50Wq3YO z+JdFfFfN%Ro{Kr<$4{X@jGu0PRtt^jx+X8P+Ro@cD}mVNRo$7}R#BU~RaJcna!umm z*RYK|nT<{o|G>bl74m}`adR^A2b|c?lWFX79G-K(Z#w1(X7*z%OwJ^lpqa@mKgp;) zEBudW(#yB@Hh=YJQ>>?36wzqdgb-EJJo-6N6(Q#Xtt^DO-Ch#r=SsP`>~^my#^Oa9 zIPM#uDI!4gfJDmLJDD7;Tp($iCFs^8RPqNIT&>nM;$oEMrUIyPt;#BIAWtYh(TPM7 zr|>Z(XmC1Cwtrb9m$5g4^l|qXQYwO4y(az0ypniX<#-(*uK6-K?an6dR$V#K4#P*$ z_Oy07$pf-Q&d`Cu;%|lfw{*_Z3O|=`B-blFAQf}F&X=BTvQd^Ea36U}9O%6rS3bbK zElpsy+%y=?&ZGJBOS=uh>b+P(ib$qmxH@$w5+Y4JA$Hgv0<6AbUqWfIUGBsoinW62 z=2c;qOow6_3Lm;qa=~dy{SAMVq5A@p{)@)CpI8N%Fv2R~L=7(J+H+&GUtYVoQTNlF zrF&OazUPWAtGbZ4ATX1r@_4nWDr_&NTuv%q&~>X5u=$@NK973^G6+kAg?IA)T z?KY;Em#}ke0w^c%x4eJ_BkqeOKDwE8I*S#=Hu*V&d7SC?s|{cyE5Z(yBd$LQFrjQb z$@KmDV(*YU_Bj}lWEz1qmsdAiIwAd0l}n-^cgUfCiDCQlg+3cGlLwlnQ@_@LjBpw|ki105P%#C0vi{Z2A<{7LzjbT{pLr&E<0Tp(sADU z?mPQdzylP^h3(oZ${+99A$y<69(#v%9r+lc#dmPOF1X_O%~xP%d}o4hoe zYR0D8Bc}5k;6+p@v$3*OklYTB;K*RmV%;AC{QlEi!NXF%nI`GV2n^)F_KQ5Rz>Zq! zxgE1k1uqb?=CDCGt|S@n8+Z^@XI2H>Zw^ll;AgCem6Ewt&RY%F&_m0xIY$rq-+PLZ zlJhZ(%z-^>Wgr!^m)!9JXklux5z*Mg%$;b`nMHHoyq&o|WOJHJ{gC%Xno*ioYb^JO zp{fgK)+aC9Z@0M5oY^AIcEuhZe#2P}z_*U7i($`|xN7b9CBj8dG6cES{Te87#)$e= zqpp8N?B|$Vp$U~_FI)#xB0vj~v9Db^dQeq`WZN9flq=(DKW}hWY_e)$sZfKN`B$mn zLo#R3Q!}sqv4sC}raS-!_CwM9rRVjKDdl}jz?UWa3C7a8;Jj&^9k11;>PrFuBY+9g zVFMPL8kTZ2nHu(c4SCwrnW=9&BV?>mgZ|}IiAhUU;E&MN)TG z>VvGOs>%C9^2~1#lM+3vgx>`E#(!S6N7O)nn1YeZFkY(dD0p#vmE4&ZMEUZu9;Ci^ z1SLxVRx~0{8hhz7%&Xc4sV>5F&NGmRk4xx9WFL|WX(?`fv-uxTcx<__#UL`j*Ixr~ zKxg>A0+Pe3we*MP*lY>ERq8Rwt@ z-Ibdr`Lg7{S^v{cR$!o4+h(QC*2xZ9cCo`qOv}p6QutEdZdB0CsM4rDf`4|-qiPcA z8#(bB;uil7=R%Kx?Rd-$1#l}wazJoX z`&$=Ns5OL<=knjC`tk`OK^{B{FdTRofG$H*UQ5K5M%Hx@1C-^Inadsdg_OoM#RbRP zV-)vzan=6@kw<`yPZrpx+G+KgQTt?|C=T?&76d4IRLh8Cs2Owzv^!IU$KakvuhR}( z+4JNN>R(tvaE(--H75J*w~dwDEfm?dV>m1M+_n@9vnR-2ITV8)OP1~Ojik3QOz@!E z$Z^t)=YU-Jos1&wL1rg$Hdb}^45~X7G5Tg|IwuV=5^akpmPJ)0q>6`(SM{vi*XpEb zZ?E7$QjwV0pY=^6+>1VC8Mi~oBVK+apOLsjV1aBwugC-b=ubPU8EkmK=ee)b7(YZB z2Q=dX`&a`QnQAWaqg_PDZnLWUVj$3O^@3Y3=(%C?F|T zLbz~M7B6*783u$GcUKmvjxVWx_qt>6+2-bUn})?8P)`u$bLLf)`dUrh7Rd;0Va#+@ zG%Q>MB)4!?R@Q1LHk-yjS6!z6K~iMd^Y4P^>?ZiM_Ocnp0;4G)oY%h%WLd{BL%xgx z>?tzEsX~K7O{V0KhTnGr;~B)KQfW}zKfN?#*bmud-SaU2$SGJRl))J*`fEne3Ue#t zxnrmO^DQI!s)GD1y{M}8wJ}H3Lp)+k0*?Y|fX6$vw`=}9CaYeG{oE}cqo*=)FA#{nVGjUpQSab@?=5x51 zGQDUfrQo;~=Is!59LVrJxZt`%a)-1W{;EQjf_i2%05n|jdt<7yf|R~Nv8?Xqby!$& z-`3)%EkgMM-ebih;?IA~k>K&*7P!TuBHN0RcRer` zrjz6I9x~mc-m%JY4K5CR4;O58YF+(_GmaG*rAzPB`yIQA5y3vQotJ+qv*ovf(DybW z_h75CfpT|snm{Y?lXs-(&rXIuy6+CSAi9)=&o{3EF*E2+FEs}6a4X$+CN0c9e1*$+ zAOTtkU+bIr05Sr<(RlAjNlBk2Jj3%%!W^*t(C2tmYS$wC&>Zecah?cXu}cGgw6ize zRMDPQLJd=ziz{1$WiU_#H&5{4rX>#@v7T6;UIUbzIfY6p+}eyPJzU1#Hp|VP0xu`u zPv$?2xOf4Vq%l^+HB!Ks)x!ztqb@e*lpRDv^yB15^aN$g@H*D$uKfLGj}@A039PV@ zSt!t@&eNs2*v6~aAKl-vZDn9eK`Wzx(KwDBiY*MFH+a$Ds`BB=zlGEqM#yvc?z6sp zBz)&s2$Szcjp#R;=k>mPZWtAUW3@92<-erlV5s%BOt}VjRI&Z98qPc(%J+NYjERw* zvM(VjObtfJHnti2E^BrgW6v(xvrkGCLWMFUB@6~3gh*K;6RJssD7zN??x*kP_b*gmnobWErW|3@wL-9bOhStxYb6TRwTs;| z3j|!@GhrXFPtWg9ZSUqJ3Br`Rru|DF6Sn$Xiap?N!TbfEBDueClP4ZKwE~4dE0LL{ z9e8^k_3ud+-1$xPDrE~dDPEf*G=5rXU1Ff0|A6N+2v)J(|C}ba&L|Dq$43GM<2WVK z;FBTsP)RQ~7F%Ge(=D{K=(utA&H39~Q<@Y6EXuj-zQXdX{YI+36r(hXGE$D?i+*7y z0Gq-c+Zb9o6aOn~H}Lp(CIa95Y{5m(D|c$IzKWBd;^T*naCW--0L9xYdZ;IZ$J>yV zU|3KJDsC3StF&bVzcY1FYzJ82Tfk{zE8I_wAv@MoZ<|_jb$W0&Ig?hQU3*%HVQa)n znY7;>5AU-k21cQ;)R!7+6@Eq`1UwVO3CSIl1e6HyKxRDtp`0NVYWA>@hUbF1$SZ$ zJ4bI^YZql^G>0=hh4M1IdJJ?Q+4;EUgx_=gm_0vt+chrawt zfT2Fm_Y^}=%=eO%-aexWcMBX>Vl}D};iU~7<8iJ*^3rl2lvm3uizq2vLOUkV*%yra zG~UleoJ_8duurFub$`mgl7^^m8{V0OAuN`76jx^vTjQ?e;dwyQ?{w{}sa5K=lOKtg z+GJO|=>sv>x$xxPPu*!8w=G25K(@qWYt<%6&WLM+@8SHZtJg}Ngu^Wsxnx%X&vVm! zORVqL9wm=tQ-r*5W~;d5jVAP;{?M#M#^umUqR*m&a9p!&TZSAE zxmWKV{Jp3-b*pwEs6k7Kh3CCP<(}N zRus1?;JIc_RTk5tIL1e}NI#ku>H!6=o|lryIIn#*?(bS(uP3G>Zf_dkQbAXP-wqn;XAxz8?N@d3nCWC4QD+VGl~)@(bFCTTD68B(`6Cp8SB zdI}X4-~{Hxc`6MEY_i(P^8NgZKG_XNlMu^mVA__{)kd!YANY%aj=k+%-sIOnGaZ`= z?q;jCt~BEgXlq%%q3Fi!1xp-4HSO(ube(^D4af!VC`d~In(e|gK#?{WWts=v*KZRU zNqv8Fl6^p*J@7WzC4O%!>A1Ux#^vq2U(dlbj9GsR#zj@T;Bpaum;mlx3z_tikGlQ0 zFC_LHy`AMR*)?{?5&)MxSpHqS;wO}7f@%U060<}RwTX6MX;)AvH@H6RCW;e{t^l*$ z9ia5COapQH8$>`zZ4aycI3hea@G*{Gf8FF11%SA{y75)Zy~7|+T zri&yd;b6>wlT#zXcHOax1$}z`7tkzQT-sxp%Rc}ja$0MDjbjbvvZSgO1ciz;f1nll z)jB*wO6)1&@PTjVhCmBz3Zg+6+h+ebQC8b0_a{YRg>Hd&TE6CrV#g;tqM%WwzuP@;q@ji)@&=1M67XW_? z5pW@ucjG8dmj(5U;>-7{eBpo@Jr8_Fg-+dF%OD;|=Fp2`zCKj@y%fi1KwOj( zQv-1046l>F<$6pV3mDB(`SVa6mG=o2*4%4R0M?P+PA^e}E2Lqj1GqAlYwCw(G8qU1 zP>{$#Bg^ULPGC8p-j|rcmlI*GLF)-6Btz&eBW9y7MvfYuXn|Ddp*VrU3n9HnYXP5B z3$*Y=7RLGd!PBjIvDe-cv@=>#xg@da0ty=m1t)KQq-S6>2wS=!_y z9`iUJ$PXT#HgOkbV7-xixS#(jvgL=it&ymmqpy^MjmvPT;NIDX z&wgziTdfL$H;B+mGo#UM8E!OJGc|aNTthEPswUt@7h1-AE9u4G=ayv7{*<|g*Y`n6 zR78nd0Az~9Pi{O{O{3(1 zF|NJ)Lm~)U@P_}C&}+R6%C{6nJu&88mev@36ivL=!#E)4Y_^|V**nn2QMuTwK?$$Z zPUi703&!-uT9Lyv$l3NfRocgR2m`=8p{L_q1F}oFyP={STD>Znu=w+H_o25xIu2&R zqh@I3c?B4nQ8@ zJNoZtK~zwmvqDO1GpM0@`^uBsZvK#cuoWB`A|r~Bld1_!@05X?CkE?7{&Idz{uO!F zL+f|dgXR4{D=L2wUOJaNd(t-bze}fQ9_HELkyI9Gmmh6|Oqk$sSw@V>U*7g-e%vKY zU;10Tj3Ee&7BoGOToqC`W#BFW|5DD8%KW8qH-KnQ?eLdCf#tzS3IJj?BmzXPY>tdF zhB$wZl%I6|An6Er4@JG3bv@k12>z5L5FvxN1pOw@I}iC60?}2Rs=cdCp<7uP=>j3P zjMSfCGm}}l2S694L3M<3JvbQ*b$d=J>cQP>DuA_L4O*+I;?iRfwi&J(0%#o+!EE;G zh6A<=bgvU!ZWhfT4kn!%0#ai_z-e^^$()#EY*|Of4V_g#_y}l;Uwy%P+0{?^*F=TO z`6O*Kcw2oISn1C|MHm3Yt!UQqY$lPg>z45CElsoE$A>bHNcaDVF2^pY-5ulz{2#k=wK~4)~GGq1o?e62*>W ztv1r~{6Yo$M42Mq{XfiK@)d_wy5p(ft4Jd%N?VX1*{IC(;wCP|FKi2SrXc?=J|*SB z#XQxZS@o&rv|T*6K`_7_&(bM~Ku!I!;SSmlp6Ee!2g)+^r47U;O^NVrTw7dCZ&h9zc> z^f-r&=Y75E+kP@D{`Fr$Zf{C`0Q+)J8r_b0sO(&r}XJo{kpyx*hCH7iHrtD=q&y zLmorL`n>VP^TGcWYbdcugzxzk%D>+?W`gy*Bhul|>3=}t5BJe-E@T(Tcbcl_fgP>%j+spMxU!!D{mZZ1;$C^LG)Off3tt7!>FR&;_y)0r6c zWu{*eER7wvHqdC&eY#H8e`*=)_+q5Ck|VKNL0RzVd{-1h*B7n(pB>_@%`1pZI~UyO zrHeuTnv2D#1u@!=Ye;yGhpNGYsA6?uT@21DXTbylTw`b%Y$gQk&nOJaNrsdL`wx+r zU2}J<9)(LYF``AC1;y)Vf~<02+RE2_$2*D1NdW>7|_j=RFrE^%wzj>zAK~XSWxsr zo`F;T77l|-xy<3$v~OYiJ{{y}?c4Juo4m>*WABt7+(#xc1l%C|2FUv{vBF(mL1#t#P?;51?nBXvvq)5c%_oem$CGmX zu`O96vN<dd+3(sb;hMZ^fdlSY2rx#zYYgaa%Ji%N}g?6Nmm0xrM$w%M!@)YQ~N#MmBJ z-a`a^9pSQSSCT=Y6yVhv2@~2CM<2xarNuVU?(x6Z{D44D%;&|X**?uXlHy%n%k?zE hezMn41|-SS8S0zq)#|t<{U6rNk)!|s literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/eventlog.png b/public/assets/warp-posa/eventlog.png new file mode 100644 index 0000000000000000000000000000000000000000..9bccc2c27244b15bb01f8ae6356e748c4cdb1091 GIT binary patch literal 44254 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 W-&XOygFbhRNQ*0AKHPoq^nU>@{#;rB literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/middleware.png b/public/assets/warp-posa/middleware.png new file mode 100644 index 0000000000000000000000000000000000000000..10f7d114d45cfcbac6f0d21cde6967372cf2855f GIT binary patch literal 11743 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 literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/multi-workers.png b/public/assets/warp-posa/multi-workers.png new file mode 100644 index 0000000000000000000000000000000000000000..008ea2b9a356737a1261619872255e445599da62 GIT binary patch literal 20522 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 G4gNprA#`W} literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/timeout.png b/public/assets/warp-posa/timeout.png new file mode 100644 index 0000000000000000000000000000000000000000..12938ab4725cf3f7cae150a8d89f4e10936e7fa1 GIT binary patch literal 22051 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 E1EBB}HUIzs literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/wai.png b/public/assets/warp-posa/wai.png new file mode 100644 index 0000000000000000000000000000000000000000..3b2e2ed1b02ce63c6199b89aac742532621ac439 GIT binary patch literal 15529 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 Kc&(^W@c#oOISai2 literal 0 HcmV?d00001 diff --git a/public/assets/warp-posa/warp.png b/public/assets/warp-posa/warp.png new file mode 100644 index 0000000000000000000000000000000000000000..56c1ed739db4b07ffc9fd54e981cc9a00e74ff52 GIT binary patch literal 47492 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 We Made ChatWisely With Haskell +
\ No newline at end of file diff --git a/public/blog/2010/01/efficient-yaml-parsing-followup.html b/public/blog/2010/01/efficient-yaml-parsing-followup.html new file mode 100644 index 00000000..49590e6f --- /dev/null +++ b/public/blog/2010/01/efficient-yaml-parsing-followup.html @@ -0,0 +1,52 @@ + Efficient YAML Parsing- Follow up +

Efficient YAML Parsing- Follow up

January 21, 2010

GravatarBy Michael Snoyman

Just wanted to give an update on my quest for my efficient YAML file parsing. You can read the previous post for more information. The code has not yet been released, but you can download it from github (repos linked in the previous article).

+

The use case I'm working on is generating the Atom feed for the photo blog. I have benchmarked three versions of this generation:

+
  1. The version I've been using up until now, based on data-object-yaml 0.0.0. This version converts the entire YAML data file into an in-memory representation and then converts it to the atom feed data.
  2. +
  3. The same as the previous one, but built on the most recent version of data-object-yaml.
  4. +
  5. A version using the new low-level monadic API in the Text.Libyaml module. This code lives in its own module, and is rather ugly (more on this below).
  6. +
+

Without further ado: the benchmark results.

+ + + + + + + + + + + + + + + + + + + + + + +
ResultsVersion 1Version 2Version 3
Maximum memory residency (bytes)5,474,8403,987,280186,504
Heap allocation (bytes)136,454,032148,156,09613,856,304
Total execution time0.28s0.28s0.03s
+

Unsurprisingly, there are huge gains in memory usage from the third version. There have been minor optimizations made to the library overall (not allocating space for missing tags, for example) which make version 2 more memory efficient than 1, but clearly 3 takes the cake here.

+

However, why the drastic change in execution time? The efficient version aborts the YAML parsing after finding ten entries; the other two versions read the whole file. This is actually a significant change in behavior; the third version will completely ignore YAML parse errors- or any other kinds of failures- beyond those first ten entries.

+

That code is ugly

+

For those of you that looked at the EfficientFeed module, you'll notice how long and tedious it is. Compare it to the beauty and conciseness of the inefficient version. This means:

+
  • Don't bother using the low-level library for small YAML files. If you use YAML for settings, data-object-yaml is, in my opinion, a good compromise between ease and complexity.
  • +
  • The EfficientFeed module looks very boilerplate to me. So boilerplate, in fact, that I think I can write some type of declarative framework for generating that code. I have to play around with the ideas a bit, but I expect it to make the cut into the 0.2.0 release. I'm just not sure if it belongs in yaml or data-object-yaml (I'm leaning to the former).
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/01/efficient-yaml-parsing.html b/public/blog/2010/01/efficient-yaml-parsing.html new file mode 100644 index 00000000..f61fb108 --- /dev/null +++ b/public/blog/2010/01/efficient-yaml-parsing.html @@ -0,0 +1,62 @@ + Efficient YAML Parsing +

Efficient YAML Parsing

January 12, 2010

GravatarBy Michael Snoyman

One of the main applications I have for stress testing my Yesod web framework is the photo blog. This blog stores all information in a single YAML file. This file is now about 200 KB large.

+

A few weeks ago, I made some changes so Atom feeds would be generated using HtmlObject, and suddenly I was running out of memory. At the time I made a few simple strictness hacks and set it aside for later.

+

Now that the release is nearing, I've started attacking memory leaks. The HtmlObject bug was fairly easily solved (it was using too much list appending), but I realized I was unhappy with the performance of my YAML library.

+

Just as a quick intro, this YAML library includes a binding to libyaml and converts the result to be a Data.Object.Object. The problem with this approach is that, anytime you read the file, it reads and stores the entire contents in memory, even if you only need to collect, say, the names of the entries.

+

A simple solution

+

A fairly simple solution to this problem is to run a program to split the single large YAML files into different views. For example, one file for each entry, and then a separate file with the list of all entries. This is most likely something I will want to do regardless, since it will reduce disk read times. Nonetheless, the library should be made more efficient.

+

The Benchmark

+

In order to test things out, I decided to go for a ridiculously simple benchmark: count how many entries are in my YAML file. You can download the sample file here.

+

The main issue I'm interested in here is memory, not processing, so I will only mention those results. In particular, I consider the most important number total memory in use as reported by "+RTS -sstderr". If someone know a reason to trust another number instead, please let me know.

+

The original version of the library ended up using 7MB of memory. Considering the fact that the entire YAML file is only 200KB, there's a lot of fat to trim.

+

The main issue, however, is the fact that I'm reading the entire file into memory. Let's see how to address that.

+

Lazy IO

+

I at first considered using unsafeInterleaveIO to get out of this predicament. The library produced a list of events ([Event]). However, I had plenty of reasons to avoid this:

+
  • I'm calling out to a C library, which made things very hairy. There were all kinds of resource management issues to beware of.
  • +
  • Any step along the way could have generated a failure from the C library, and I didn't want those popping up in pure code. I could have changed the type to [Either YamlException Event], but it seemed like an uphill battle.
  • +
  • I haven't seen examples of unsafeInterleaveIO used in this context, and I wasn't certain how stable it would be.
  • +
+

Left-fold enumerator

+

So I decided that it would make most sense to have a left-fold enumerator here. In this setup, I would provide the library with a function and accumulating parameter, and the library handles all memory management and exception issues. Others have discussed this topic at-length, so I won't get into more details for now.

+

In this process, I decided to split the library into two pieces: yaml would be the low-level binding providing the left-fold enumerator interface, while data-object-yaml would provide high-level conversions to data-object types.

+

Results

+

The results so far are promising. I now have two benchmarks: the easy and efficient one. The easy benchmark follows the same procedure as before: convert the entire YAML file into an Object and then count the entries. After the rewrite, this benchmark takes 5 MB. Still needs lots of work, but I was gratified to see that I shaved off 2 MB without focusing on this area.

+

However, the efficient benchmark is where we see the real possibilities of a left-fold enumerator approach. This code generates no intermediate data structure, it simply counts up the entries. Here's some number comparisons between the two benchmarks:

+ + + + + + + + + + + + + + + + + + +
ResultsEasyEfficient
Total memory used5-6MB1MB
%GC Time50.0%0.0
Maximum residency (bytes)3,545,8407,632
+

It appears to me that this version runs in constant space, which is what I would hope for.

+ +

Conclusion

+

The downside to the efficient method is that it's rather tedious to program. However, I think it should be relatively straight-forward to develop a parsing framework to ease this burden. After all, we're dealing with incredibly simple data structures here: scalars, sequences and mappings.

+

If you'd like to play around with this code yourself, you'll need to get both my yaml and data-object-yaml repositories from Github.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/01/new-blog-system.html b/public/blog/2010/01/new-blog-system.html new file mode 100644 index 00000000..ba5bbb33 --- /dev/null +++ b/public/blog/2010/01/new-blog-system.html @@ -0,0 +1,20 @@ + New Blog System +

New Blog System

January 9, 2010

GravatarBy Michael Snoyman

I decided I would like to streamline my domain name by making the homepage my blog. I've always considered Wordpress to be a temporary solution as it is too heavy for my needs, so I finally decided to bite the bullet and make my own blog system.

+

It's nothing fancy, but it allows me to make my blog posts in Vim and track them with Git, so I consider it a significant improvement to my wordflow.

+

I decided to outsource comments to Disqus; I don't have much experience with them, so I wouldn't mind hearing any feedback on it.

+

And as you might guess, it's built on Yesod Web Framework. This blog was the final project in my queue before tidying up Yesod for its first release. Currently, it has some major space leaks, some of which come from underlying libraries. Once those are addressed, I'll try to do some basic code clean-up, document a bit more and release!

+

Also, expect this blog to contain a significant increase in web development discussions as I move from developing a web framework to developing web sites.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/01/with-monadio.html b/public/blog/2010/01/with-monadio.html new file mode 100644 index 00000000..985675b7 --- /dev/null +++ b/public/blog/2010/01/with-monadio.html @@ -0,0 +1,52 @@ + With MonadIO +

With MonadIO

January 13, 2010

GravatarBy Michael Snoyman

I've been bitten by this one before: I'd like to use a monad transformer, and I'd like to use a "with" function. In this case, we'll give the example of withCString for creating C-style strings from [Char]s. The function signature is:

+
withCString :: String -> (CString -> IO a) -> IO a
+

However, I don't have an IO monad. In this case, let's say I have a "ReaderT Parser IO". No problem, we'll just apply a liftIO... somewhere... and...

+

This is a little tricky in fact. What we want here is a function of type signature:

+
MonadIO m => String -> (CString -> m a) -> m a
+

Unfortunately, there's no way to create it automatically. Now, for the withCString function, we could go ahead and implement things ourselves. But what about allocaBytes, which does some fancy magic under the hood?

+

Dealing with ReaderT

+

Before dealing with a generic solution, let's first play around with ReaderT and see what we come up with. Let's assume that we'll curry away the first argument to withCString, so we're left with a function with the type signature:

+
(CString -> IO a) -> IO a
+

We need to produce a function with the type signature:

+
(CString -> ReaderT Parser IO a) -> ReaderT Parser IO a
+

Turns out this function isn't too bad. We'll call it withReader (I wouldn't mind name suggestions):

+
withReader :: ((CString -> IO a) -> IO a) -> (CString -> ReaderT Parser IO a) -> ReaderT Parser IO a
+withReader orig f = ReaderT $ \parser -> do
+    let f' a = (runReaderT $ f a) parser
+    orig f'
+

You can call this function like such:

+
someOtherFunction :: CString -> ReaderT Parser IO SomeOutput
+...
+withReader (withCString "foobar") someOtherFunction :: ReaderT Parser IO SomeOutput
+

More general

+

Well, that solved the problem. That is, until we want to be able to stack our monads. For example, let's say (for simplicity) that I wanted to have the following monad:

+
ReaderT Parser (ReaderT Emitter IO)
+

(In case you're wondering, this is code used for interleaving parsing and emitting of a YAML document. See my previous blog post for more context.)

+

In any event, we could write a withReaderReader function, but instead I smell a great opportunity for a typeclass:

+
class With m where
+    with :: ((a -> IO b) -> IO b) -> (a -> m b) -> m b
+

I currently only have two instances of this class, but I'm sure other monads could be easily added:

+
instance With IO where -- obvious instance
+    with = id
+instance With m => With (ReaderT r m) where
+    with orig f = ReaderT $ \r -> do
+        let f' a = (runReaderT $ f a) r
+        with orig f'
+

Improvements

+

The number one improvement I need is a better name for this beast. After that, I'd like to add some more methods to handle even more useful functions, such as finally. I know I've been caught in the past wanting to ensure resource release when dealing with a monad stack.

+

This code (with slightly different names) is part of my yaml github repo. If there's demand, I'll split it into its own package.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/02/request-body-interfaces.html b/public/blog/2010/02/request-body-interfaces.html new file mode 100644 index 00000000..e1a70a68 --- /dev/null +++ b/public/blog/2010/02/request-body-interfaces.html @@ -0,0 +1,105 @@ + Four HTTP Request Body Interfaces +

Four HTTP Request Body Interfaces

February 19, 2010

GravatarBy Michael Snoyman

Sorry for the long delay since my last post, I've actually been working on a project recently. It should be released last week, but since it's a web application programmed in Haskell, it's given me a chance to do some real-world testing of WAI, wai-extra (my collection of handlers and middlewares) and Yesod (my web framework). None of these have been released yet, though that date is fast approaching.

+

Anyway, back to the topic at hand: request body interface. I'm going to skip over the response body for now because, frankly, I think it's less contraversial: enumerators seem to be a good fit. What flavor of enumerator could be a good question, but I'd rather figure out what works best on the request side and then choose something that matches nicely.

+

I've been evaluating the choices in order to decide what to use in the WAI. In order to get a good comparison of the options, let's start off by stating our goals:

+
  • Performant. The main goal of the WAI is not user-friendliness, but to be the most efficient abstraction over different servers possible.
  • +
  • Safe. You'll see below some examples of being unsafe.
  • +
  • Determinstic. We want to make sure that we are never forced to use more than a certain amount of memory.
  • +
  • Early termination. We shouldn't be forced to read the entire contents of a long body, as this could open up DoS attacks.
  • +
  • Simple. Although user-friendliness isn't the first goal, it's still something to consider.
  • +
  • Convertible. In particular, many people will be most comfortable (application-side) using cursors and lazy bytestrings, so we'd like an interface that can be converted to those.
  • +
+

One other point: we're not going to bother considering anything but bytestrings here. I think the reasons for this are obvious.

+

Lazy bytestring

+

This is the approach currently used by Hack, which is pretty well used and accepted by the community (including myself).

+

Pros

+
  • Allows both the server and client to write simple code.
  • +
  • Lots of tools to support it in standard libraries.
  • +
  • Mostly space efficient.
  • +
+

Cons

+
  • I said mostly space efficient, because you only get the space efficiency if you use lazy I/O. Lazy I/O is also known as unsafeInterleaveIO. Remember that concern about safety I mentioned above? This is it.
  • +
  • Besides that, lazy I/O is non-deterministic.
  • +
+

In fact, avoiding lazy I/O is the main impetus for writing the WAI. I don't consider this a possible solution.

+

Source

+

The inspiration for this approach is- frankly- every imperative IO library on the planet. Think of Handle: you have functions to open the handle, close it, test if there's more data (isEOF) and to get more data. In our case, there's no need for the first two (the server performs them before and after calling the application, respectively), so we can actually get away with this definition:

+type Source = IO (Maybe ByteString) -- a strict bytestring +

Each time you call Source, it will return the next bytestring in the request, until the end, where a Nothing is returned.

+

Pros

+
  • Simple and standard.
  • +
  • Deterministic.
  • +
  • Space efficient.
  • +
+

Cons

+
  • This makes the server the callee, not the caller. In general, it is more difficult to write callees, though in the particular case of a server I'm not certain how much more difficult it really is.
  • +
  • This provides no mechanism for the server to keep state (eg, bytes read so far).
  • +
+

Overall, this is a pretty good approach nonetheless. Also, at the cost of complicating things a bit, we could redefine Source as:

+type Source a = a -> IO (Maybe (a, ByteString)) +

This would solve the second of the problems above by forcing the application to thread the state through.

+

Recursive enumerator

+

The idea for the recursive enumerator comes from a few sources, but I'll cite Hyena for the moment. The idea takes a little bit of time to wrap your mind around, and it doesn't help that there are many definitions of enumerators and iteratees with slightly different definitions. Here I will present a very specialized version of an enumerator, which should hopefully be easier to follow.

+

You might be wondering: what's a recursive enumerator? Just ignore the word for now, it will make sense when we discuss the non-recursive variant below.

+

Anyway, let's dive right in:

+
-- Note: this is a strict byte string
+type Enumerator a = (a -> ByteString -> IO (Either a a))
+                  -> a
+                  -> IO (Either a a)
+

I appologize in advance for having slightly complicated this type from its usual form by making the return type IO (Either a a) instead of IO a, but it has some real world uses. I know it's possible to achieve the same result with the latter definition, but it's slightly more work. I'm not opposed to switching back to the former if there's enough desire.

+

So what exactly does this mean? An Enumerator is a data producer. When you call the enumerator, it's going to start handing off one bytestring at a time to the iteratee.

+

The iteratee is the first argument to the enumerator. It is a data consumer. To put it more directly: the application will be writing an iteratee which receives the raw request body and generates something with it, most likely a list of POST parameters.

+

So what's that a? It has a few names: accumulator, seed, or state. That's the way the iteratee is able to keep track of what it's doing. Each step along the way, the enumerator will collect the result of the iteratee and pass it in next time around.

+

And finally, what's going on with that Either? That's what allows us to have early termination. If the iteratee returns a Left value, it's a signal to the enumerator to stop processing data. A Right means to keep going. Similarly, when the enumerator finishes, it returns a Left to indicate that the iteratee requested early termination, and Right to indicate that all input was consumed.

+

To give a motivating example, here's a function that converts an enumerator into a lazy bytestring. Two things: firstly, this function is not written efficiently, it's meant to be easy to follow. More importantly, this lazy bytestring is not exactly lazy: the entire value must be read into memory. If we were two convert this in reality to a lazy bytestring, we would want to use lazy IO so reduce memory footprint. However, as Nicolas Pouillard pointed out to me, the only way to do this involes forkIO.

+
import Network.Wai
+import qualified Data.ByteString as S
+import qualified Data.ByteString.Lazy as L
+import Control.Applicative
+
+type Iteratee a = a -> S.ByteString -> IO (Either a a)
+
+toLBS :: Enumerator [S.ByteString] -> IO L.ByteString
+toLBS e = L.fromChunks . reverse . either id id <$> e="" iter="" []="" where="" iter="" ::="" Iteratee="" [S.ByteString]="" iter="" bs="" b="return" $="" Right="" $="" b="" :="" bs<="" /pre="">
+

As this post is already longer than I'd hoped for, I'll skip an explanation and to pros/cons:

+

Pros

+
  • Space efficient and deterministic.
  • +
  • Server is the caller, makes it easier to write.
  • +
  • No need for IORef/MVar at all.
  • +
+

Cons

+
  • Application is the callee, which is more difficult to write. However, this can be mitigated by having a single package which does POST parsing from an enumerator.
  • +
  • Cannot be (simply) translated into a source or lazy bytestring. Unless someone can show otherwise, you need to start the enumerator is a separate thread and then use MVars or Chans to pass the information back. On top of that, you then need to be certain to use up all input, or else you will have a permanently locked thread.
  • +
+

While I think this is a great interface for the response body, and I've already implemented working code on top of this, I'm beginning to think we should reconsider going this route.

+

Non-recursive enumerator

+

The inspiration for this approach comes directly from a paper by Oleg. I found it easier to understand what was going on once I specialized the types Oleg presents, so I will be doing the same here. I will also do a little bit of renaming, so appologies in advance.

+

The basic distinction between this and a recursive enumerator is that the latter calls itself after calling the iteratee, while the former is given a function to call.

+

I'm not going to go into a full discussion of this here, but I hope to make another post soon explaining exactly what's going on (and perhaps deal with some of the cons).

+
type Enumerator a = RecEnumerator a -> RecEnumerator a
+type RecEnumerator a = Iteratee a -> a -> IO (Either a a)
+type Iteratee a = a -> B.ByteString -> IO (Either a a)
+

Pros

+
  • Allows creation of the source (Oleg calls it a cursor) interface- and thus lazy byte string- without forkIO.
  • +
  • Space efficient and deterministic.
  • +
+

Cons

+
  • I think it's significantly more complicated than the other approaches, though that could just be the novelty of it.
  • +
  • It still requires use of an IORef/MVar to track state. I have an idea of how to implement this without that, but it is significantly more complex.
  • +
+

Conclusion

+

Well, the conclusion for me is I'm beginning to lean back towards the Source interface. It's especially tempting to try out the source variant I mention, since that would eliminate the need for IORef/MVar. I'd be interested to hear what others have to say though.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/02/simpler-is-better.html b/public/blog/2010/02/simpler-is-better.html new file mode 100644 index 00000000..70114688 --- /dev/null +++ b/public/blog/2010/02/simpler-is-better.html @@ -0,0 +1,30 @@ + Simpler is Better +

Simpler is Better

February 26, 2010

GravatarBy Michael Snoyman

I was in the middle of writing a post about non-recursive enumerators (see my last post) when I realized it was too much of an uphill battle. While they gave the promise of uniting both request and response bodies under a single interface that allowed easy generation of lazy bytestrings, they were going to be too complicated.

+

So I've broken down and admitted that I will have to have two separate interfaces for these things. It makes sense after all: the requirements for a server spitting out a response body are quite different than an application reading a request body.

+

So I think it's safe to declare the winner on the response body side to be the recursive enumerator (henceforth known as enumerator). In addition, to deal with a number of compile type issues, I've added a newtype, so that the definition of Enumerator is:

+
newtype Enumerator = Enumerator { runEnumerator :: forall a.
+              (a -> B.ByteString -> IO (Either a a))
+                 -> a
+                 -> IO (Either a a)
+}
+

I know it looks scary, but it's not really that bad: first argument is an iteratee and the second is the initial seed. Don't worry, they wai repository has some good examples of how to use it in the Network.Wai.Enumerator module. You can also check out the wai-extra repository.

+

Without rehashing the discussion from the last post, I mentioned what I called a "Source", but complained that it required the use of a MVar. I also alluded to an alternate definition that did away with the MVar, instead allowing explicit state passing. That's what's currently in the WAI. The definition is:

+data Source = forall a. Source a (a -> IO (Maybe (B.ByteString, a))) +

The Source constructor has two pieces: the "a" and that ugly function. The "a" is the initial state of the Source. One example (used by both SimpleServer and CGI in wai-extra) for that "a" is the value of the Content-Length header.

+

The second piece (the ugly function) takes some state, and then gives you the next piece of the request body and a new state, if they're available. Back to the previous example, if given a value of 0, it knows that the request body has been entirely consumed and returns Nothing. Otherwise, it takes another chunk off the request body, and returns that chunk with the remaining length.

+

I think this is by far the simplest approach to program to. I was reluctant to introduce it since it involves two different interfaces, but at this point I see no better alternative. If anyone is actually interested in why I'm rejecting non-recursive enumerators, feel free to ask.

+

At this point, I think the WAI is ready to be released. I'll wait a week for comments, and then put it on Hackage.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/03/persistence.html b/public/blog/2010/03/persistence.html new file mode 100644 index 00000000..4e4eb811 --- /dev/null +++ b/public/blog/2010/03/persistence.html @@ -0,0 +1,29 @@ + Persistence +

Persistence

March 7, 2010

GravatarBy Michael Snoyman

Now that the first version of Yesod Web Framework has been released, I can start to think about the missing piece of the puzzle: the model. Currently, each of my sites built on Yesod uses a hand-rolled model layer. In SearchOnce, this was particularly ugly: a 900 line file with a bunch of SQL code.

+

So I've been playing around with some ideas, and I just want to get them out for later reference. There's no code to back this one up yet, so don't expect anything concrete for a while!

+
  • Using YAML and quasi-quotation worked very nicely for URL dispatch in Yesod; I think that will be a good fit for defining models.
  • +
  • Overall, Django model system is pretty good, probably follow that structure mostly.
  • +
  • There's no reason to tie it down to SQL; I can imagine a YAML-file backend, or Amazon SimpleDB. At the same time, SQL should be a first-class citizen.
  • +
  • Not sure if it should have schema-generation, you tell it the names of your tables, or an option for either one.
  • +
  • IO thunks would probably be a good idea for possibly expensive records (eg, in a one-to-many relationship).
  • +
  • Speaking of relationships, is one-to-many and many-to-many sufficient? And how do you model many-to-many in Haskell?
  • +
  • Probably want to use data-accessor or something similar.
  • +
  • Maybe consider an IOList (or Stream) data structure: data IOList a = IONil | IOCons a (IO IOList).
  • +
  • Have to figure out a way to interoperate with HStringTemplate, which might ultimately involve unsafePerformIO due to the purity of that library.
  • +
  • Does data-object need to include a MonadObject data structure to help this out?
  • +
+

Sorry for just spouting nonsense for now... if anyone has any ideas or recommendations of already-existent libraries, please let me know.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/03/persistent-plugs.html b/public/blog/2010/03/persistent-plugs.html new file mode 100644 index 00000000..2b6fb26c --- /dev/null +++ b/public/blog/2010/03/persistent-plugs.html @@ -0,0 +1,26 @@ + Persistent Plugs +

Persistent Plugs

March 19, 2010

GravatarBy Michael Snoyman

Sorry if the title sounds like a medical condition...

+

Anyway, it's been quite busy since releasing the Yesod web framework. I got a lot of very useful feedback, especially from Jeremy Shaw and Chris Eidhof. There are two main shortcomings of Yesod (and Haskell web programming in general) that we've discussed:

+
  • Persistence
  • +
  • Type-safe URL handling
  • +
+

We've had a very good discussion about the latter on the web-devel mailing list; I highly recommend that everyone subscribe to that list by the way.

+

That discussion inspired me a little bit for a persistence interface, and so I started a project today called persistent. The goal is to provide a unified interface to any storage backend, be it SQL, in-memory, YAML files, or Amazon SimpleDB.

+

At the moment, it only handles String and Int data, allows all CRUD actions and the ability to filter in a very basic way. All of this will need to be augmented to be usable, but it shows the overall structure fairly well. I've also written an in-memory sample storage mechanism; everything together clocks in at a single 145 line file.

+

But that's not the cool part. The cool part is synthesizing these two goals to get completely pluggable web components. As an example, I wrote up a basic web authentication plugin. All of the code is available in the same repository, and is only 164 lines- including a lot of boilerplate HTML.

+

The approach taken for both sides of the puzzle here is to provide a low-level, tedious, boilerplate-requiring interface that gives the user a lot of power. Both of these pieces can then be automated later through some higher-level mechanism, be it template haskell, generics or (my favorite) quasi-quoting.

+

If you want to try out the auth example, I've hard-coded michael/michael as the only username/password combination that works. Eventually, when we hammer out a good API for both sides of this, I would like to actually develop a fully-functional authentication plug-in that supports e-mail validation, allows OpenID, Twitter and other external logins, and more. And then any system that supports persistent and the web plug-in system will be able to have a very advanced authentication system for free.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/03/tweedle-beetle-battle.html b/public/blog/2010/03/tweedle-beetle-battle.html new file mode 100644 index 00000000..82196a54 --- /dev/null +++ b/public/blog/2010/03/tweedle-beetle-battle.html @@ -0,0 +1,420 @@ + Tweedle Beetle Battle +

Tweedle Beetle Battle

March 5, 2010

GravatarBy Michael Snoyman

Now that WAI has been released, my next target is to release Yesod, my web application framework. It's already the basis for about five sites, most of them with source code available. Nonetheless, I've been wanting to write a comprehensive tutorial of writing a real web application with it before releasing.

+

Fortunately, I just had to write the perfect sample application: a bug tracker. It's very simplistic, but does what I need while showing some of the nice features of Yesod. It also shows the very ugly side of Yesod: there is no built in model yet. That's something I hope to tackle in future versions.

+

In any event, here's the code straight from the repo. You can also see it on Github, though this version is prettier. It's just one long literate Haskell file along with three string template files.

+

I know this is a long tutorial, but don't let the size scare you off; you can skip all of the serialization code below without missing the gist of things. And yes: this is actually code I'm using in production.

+
{-# LANGUAGE QuasiQuotes #-}
+

While coming up on the first release of Yesod, I realized I needed a nice, comprehensive tutorial. I didn't want to do the typical blog example, since it's so trite. I considered doing a Reddit or Twitter clone (the former became a bit of a meme a few weeks ago), but then I needed to set up a bug tracker for some commercial projects I was working on, and I decided that it would be a great example program.

Before getting started, a quick word of warning: Yesod at this point really provides nothing in terms of data storage (aka, the model). There is wonderful integration with the data-object package, and the data-object-yaml package provides good serialization, but this is all very inefficient in practice. For simplicity, I've gone ahead and used this as the storage model; this should not be done for production code.

There's a lot of boilerplate code at the beginning that just has to do with object storage; if you'd like to skip it, just start reading from the main function.

Anyway, here's the import list.

import Yesod
+import Yesod.Helpers.Auth
+import Data.Object.Yaml
+import Data.Object.String
+import Control.Concurrent
+import qualified Safe.Failure as SF
+import Data.Time
+import Data.Attempt (Attempt, fromAttempt)
+import Control.Arrow (second)
+import qualified Network.Wai.Handler.SimpleServer
+import Data.Monoid
+import Data.Text (pack)
+import Control.Applicative ((<$>), (<*>))
+import Data.Maybe (fromMaybe)
+

One of the goals of Yesod is to make it work with the compiler to help you program. Instead of configuration files, it uses typeclasses to both change default behavior and enable extra features. An example of the former is error pages, while an example of the latter is authentication.

To start with, we need a datatype to represent our program. We'll call this bug tracker "Tweedle", after Dr. Seuss's "Tweedle Beetle Battle" in "Fox in Socks" (my son absolutely loves this book). We'll be putting the complete state of the bug database in an MVar within this variable; in a production setting, you might instead put a database handle.

data Tweedle = Tweedle Settings (MVar Category) TemplateGroup
+

(For now, just ignore the TemplateGroup, its purpose becomes apparent later.)

This issue database is fully hierarchical: each category can contain subcategories and issues. This might be too much nesting for many uses, but it's what my project demanded.

Also, if I cared about efficiency here, a trie or map would probably be a better data structure. As stated above, it doesn't matter.

data Category = Category
+  { subCats :: [Category]
+  , subIssues :: [Issue]
+  , categoryId :: Integer
+  , catName :: String
+  }
+
data Issue = Issue
+  { issueName :: String
+  , issueMessages :: [Message]
+  , issueId :: Integer
+  }
+

Further simplifications: authors will just be represented by their OpenID URL.

data Message = Message
+  { messageAuthor :: OpenId
+  , messageStatus :: Maybe String
+  , messagePriority :: Maybe String
+  , messageText :: String
+  , messageCreation :: UTCTime
+  }
+
type OpenId = String
+

We need to be able to serialize this data to and from YAML files. You can consider all of the following code boilerplate.

messageToSO :: Message -> StringObject
+messageToSO m = Mapping $ map (second Scalar)
+    [ ("author", messageAuthor m)
+    , ("status", show $ messageStatus m)
+    , ("priority", show $ messagePriority m)
+    , ("text", messageText m)
+    , ("creation", show $ messageCreation m)
+    ]
+messageFromSO :: StringObject -> Attempt Message
+messageFromSO so = do
+    m <- fromMapping so
+    a <- lookupScalar "author" m
+    s <- lookupScalar "status" m >>= SF.read
+    p <- lookupScalar "priority" m >>= SF.read
+    t <- lookupScalar "text" m
+    c <- lookupScalar "creation" m >>= SF.read
+    return $ Message a s p t c
+issueToSO :: Issue -> StringObject
+issueToSO i = Mapping
+  [ ("name", Scalar $ issueName i)
+  , ("messages", Sequence $ map messageToSO $ issueMessages i)
+  , ("id", Scalar $ show $ issueId i)
+  ]
+issueFromSO :: StringObject -> Attempt Issue
+issueFromSO so = do
+  m <- fromMapping so
+  n <- lookupScalar "name" m
+  i <- lookupScalar "id" m >>= SF.read
+  ms <- lookupSequence "messages" m >>= mapM messageFromSO
+  return $ Issue n ms i
+categoryToSO :: Category -> StringObject
+categoryToSO c = Mapping
+  [ ("cats", Sequence $ map categoryToSO $ subCats c)
+  , ("issues", Sequence $ map issueToSO $ subIssues c)
+  , ("id", Scalar $ show $ categoryId c)
+  , ("name", Scalar $ catName c)
+  ]
+categoryFromSO :: StringObject -> Attempt Category
+categoryFromSO so = do
+  m <- fromMapping so
+  cats <- lookupSequence "cats" m >>= mapM categoryFromSO
+  issues <- lookupSequence "issues" m >>= mapM issueFromSO
+  i <- lookupScalar "id" m >>= SF.read
+  n <- lookupScalar "name" m
+  return $ Category cats issues i n
+

Well, that was a mouthful. You can safely ignore all of that: it has nothing to do with actual web programming.

Next is the Settings datatype. Normally I create a settings file so I can easily make changes between development and production systems without recompiling, but once again we are aiming for simplicity here.

data Settings = Settings
+

Many web frameworks make the simplifying assumptions that "/" will be the path to the root of your application. In real life, this doesn't always happen. In Yesod, you must specify explicitly your application root and then create an instance of YesodApproot (see below). Again, the compiler will let you know this: once you use a feature that depends on knowing the approot, you'll get a compiler error if you haven't created the instance.

{ sApproot :: String
+  , issueFile :: FilePath
+

Yesod comes built in with support for HStringTemplate. You'll see later how this ties in with data-object (and in particular HtmlObject) to help avoid XSS attacks.

, templatesDir :: FilePath
+  }
+

And now we'll hardcode the settings instead of loading from a file. I'll do it in the IO monad anyway, since that would be the normal function signature.

loadSettings :: IO Settings
+loadSettings = return $ Settings "http://localhost:3000/" "issues.yaml" "examples/tweedle-templates"
+

And now we need a function to load up our Tweedle data type.

loadTweedle :: IO Tweedle
+loadTweedle = do
+  settings <- loadSettings
+

Note that this will die unless an issues file is present. We could instead check for the file and create it if missing, but instead, just put the following into issues.yaml:

{cats: [], issues: [], id: 0, name: "Top Category"}

issuesSO <- decodeFile $ issueFile settings
+  issues <- fromAttempt $ categoryFromSO issuesSO
+  missues <- newMVar issues
+  tg <- loadTemplateGroup $ templatesDir settings
+  return $ Tweedle settings missues tg
+

And now we're going to write our main function. Yesod is built on top of the Web Application Interface (wai package), so a Yesod application runs on a variety of backends. For our purposes, we're going to use the SimpleServer.

main :: IO ()
+main = do
+  putStrLn "Running at http://localhost:3000/"
+  tweedle <- loadTweedle
+  app <- toWaiApp tweedle
+  Network.Wai.Handler.SimpleServer.run 3000 app
+

Well, that was a lot of boilerplate code that had nothing to do with web programming. Now the real stuff begins. I would recommend trying to run the code up to now an see what happens. The compiler will complain that there is no instance of Yesod for Tweedle. This is what I meant by letting the compiler help us out. So now we've got to create the Yesod instance.

The Yesod typeclass includes many functions, most of which have default implementations. I'm not going to go through all of them here, please see the documentation.

instance Yesod Tweedle where
+

The most important function is resources: this is where all of the URL mapping will occur. Yesod adheres to Restful principles very strongly. A "resource" is essentially a URL. Each resource should be unique; for example, do not create /user/5/ as well as /user/by-number/5/. In addition to resources, we also determine which function should handle your request based on the request method. In other words, a POST and a GET are completely different.

One of the middlewares that Yesod installs is called MethodOverride; please see the documentation there for more details, but essentially it allows us to work past a limitation in the form tag of HTML to use PUT and DELETE methods as well.

Instead of using regular expressions to handle the URL mapping, Yesod uses resource patterns. A resource is a set of tokens separated by slashes. Each of those tokens can be one of:

  • A static string.
  • An integer variable (begins with #), which will match any integer.
  • A string varaible (begins with $), which will match any single value.
  • A "slurp" variable, which will match all of the remaining tokens. It must be the last token.

Yesod uses quasi quotation to make specifying the resource pattern simple and safe: your entire set of patterns is checked at compile time to see if you have overlapping rules.

resources = [$mkResources|
+

Now we need to figure out all of the resources available in our application. We'll need a homepage:

/:
+      GET: homepageH
+

We will also need to allow authentication. We use the slurp pattern here and accept all request methods. The authHandler method (in the Yesod.Helpers.Auth module) will handle everything itself.

/auth/*: authHandler
+

We're going to refer to categories and issues by their unique numerical id. We're also going to make this system append only: there is no way to change the history.

/category/#id: # notice that "id" is ignored by Yesod
+      GET: categoryDetailsH
+      PUT: createCategoryH
+  /category/#id/issues:
+      PUT: createIssueH
+  /issue/#id:
+      GET: issueDetailsH
+      PUT: addMessageH
+  |]
+

So if you make a PUT request to "/category/5", you will be creating a subcategory of category 5. "GET /issue/27/" will display details on issue 27. This is all we need.

If you try to compile the code until this point, the compiler will tell you that you have to define all of the above-mentioned functions. We'll do that in a second; for now, if you'd like to see the rest of the error messages, uncomment this next block of code.

{-
+homepageH = return ()
+categoryDetailsH _ = return ()
+createCategoryH _ = return ()
+createIssueH _ = return ()
+issueDetailsH _ = return ()
+addMessageH _ = return ()
+-}
+

Now the compiler is telling us that there's no instance of YesodAuth for Tweedle. YesodAuth- as you might imagine- keeps settings on authentication. We're going to go ahead a create an instance now. The default settings work if you set up authHandler for "/auth/*" (which we did) and are using openid (which we are). So all we need to do is:

instance YesodAuth Tweedle
+

Running that tells us that we're missing a YesodApproot instance as well. That's easy enough to fix:

instance YesodApproot Tweedle where
+  approot (Tweedle settings _ _) = sApproot settings
+

Congratulations, you have a working web application! Gratned, it doesn't actually do much yet, but you can use it to log in via openid. Just go to http://localhost:3000/auth/openid/.

Now it's time to implement the real code here. We'll start with the homepage. For this program, I just want the homepage to redirect to the main category (which will be category 0). So let's create that redirect:

homepageH :: Handler Tweedle ()
+homepageH = do
+  ar <- getApproot
+  redirect RedirectPermanent $ ar ++ "category/0/"
+

Simple enough. Notice that we used the getApproot function; if we wanted, we could have just assumed the approot was "/", but this is more robust.

Now the category details function. We're just going to have two lists: subcategories and direct subissues. Each one will have a name and numerical ID.

But here's a very nice feature of Yesod: We're going to have multiple representations of this data. The main one people will use is the HTML representation. However, we're also going to provide a JSON representation. This will make it very simple to write clients or to AJAXify this application in the future.

categoryDetailsH :: Integer -> Handler Tweedle RepHtmlJson
+

That function signature tells us a lot: the parameter is the category ID, and we'll be returning something that has both an HTML and JSON representation.

categoryDetailsH catId = do
+

getYesod returns our Tweedle data type. Remember, we wrapped it in an MVar; since this is a read-only operation, will unwrap the MVar immediately.

Tweedle _ mvarTopCat _ <- getYesod
+  topcat <- liftIO $ readMVar mvarTopCat
+

Next we need to find the requested category. You'll see the (boilerplate) function below. If the category doesn't exist, we want to return a 404 response page. So:

(parents, cat) <- maybe notFound return $ findCat catId [] topcat
+

Now we want to convert the category into an HtmlObject. By doing so, we will get automatic HTML entity encoding; in other words, no XSS attacks.

let catHelper (Category _ _ cid name) = Mapping
+          [ ("name", Scalar $ Text $ pack name)
+          , ("id", Scalar $ Text $ pack $ show cid)
+          ]
+  let statusHelper = fromMaybe "No status set"
+                   . getLast . mconcat . map (Last . messageStatus)
+  let priorityHelper = fromMaybe "No priority set"
+                     . getLast . mconcat . map (Last . messagePriority)
+  let issueHelper (Issue name messages iid) = Mapping
+          [ ("name", Scalar $ Text $ pack name)
+          , ("id", Scalar $ Text $ pack $ show iid)
+          , ("status", Scalar $ Text $ pack $ statusHelper messages)
+          , ("priority", Scalar $ Text $ pack $ priorityHelper messages)
+          ]
+  let ho = Mapping
+          [ ("cats", Sequence $ map catHelper $ subCats cat)
+          , ("issues", Sequence $ map issueHelper $ subIssues cat)
+          ]
+

And now we'll use a String Template to display the whole thing.

templateHtmlJson "category-details" ho $ \_ -> return
+      . setHtmlAttrib "cat" ho
+      . setHtmlAttrib "name" (catName cat)
+      . setHtmlAttrib "parents" (Sequence $ map catHelper parents)
+
findCat :: Integer -> [Category] -> Category -> Maybe ([Category], Category)
+findCat i parents c@(Category cats _ i' _)
+    | i == i' = Just (parents, c)
+    | otherwise = getFirst $ mconcat $ map (First . findCat i (parents ++ [c])) cats
+

Now we get a new missing instance: YesodTemplate. As you can imagine, this is because of calling the templateHtmlJson function. This is easily enough solved (and explains why we needed TemplateGroup as part of Tweedle).

instance YesodTemplate Tweedle where
+  getTemplateGroup (Tweedle _ _ tg) = tg
+

Now we actually get some output! I'm not going to cover the syntax of string templates here, but you should read the files in the examples/tweedle-templates directory.

Next, we need to implement createCategoryH. There are two parts to this process: parsing the form submission, and then modifying the database. Pay attention to the former, but you can ignore the latter if you wish. Also, this code does not do much for error checking, as that would needlessly complicate matters.

createCategoryH :: Integer -> Handler Tweedle ()
+createCategoryH parentid = do
+

Yesod uses a formlets-style interface for parsing submissions. This following line says we want a parameter named catname, which precisely one value (required) and that value must have a non-zero length (notEmpty).

catname <- runFormPost $ notEmpty $ required $ input "catname"
+  newid <- modifyDB $ createCategory parentid catname
+  ar <- getApproot
+  redirect RedirectPermanent $ ar ++ "category/" ++ show newid ++ "/"
+

And here's the database modification code we need. Once again, this is not web-specific.

modifyDB :: (Category -> (Category, x)) -> Handler Tweedle x
+modifyDB f = do
+  Tweedle settings mcat _ <- getYesod
+  liftIO $ modifyMVar mcat $ \cat -> do
+      let (cat', x) = f cat
+      encodeFile (issueFile settings) $ categoryToSO cat'
+      return (cat', x)
+
createCategory :: Integer -> String -> Category -> (Category, Integer)
+createCategory parentid catname topcat =
+  let newid = highCatId topcat + 1
+      topcat' = addChild parentid (Category [] [] newid catname) topcat
+   in (topcat', newid)
+    where
+      highCatId (Category cats _ i _) = maximum $ i : map highCatId cats
+      addChild i' newcat (Category cats issues i name)
+          | i' /= i = Category (map (addChild i' newcat) cats) issues i name
+          | otherwise = Category (cats ++ [newcat]) issues i name
+

Next is creating an issue. This is almost identical to creating a category.

createIssueH :: Integer -> Handler Tweedle ()
+createIssueH catid = do
+  issuename <- runFormPost $ notEmpty $ required $ input "issuename"
+  newid <- modifyDB $ createIssue catid issuename
+  ar <- getApproot
+  redirect RedirectPermanent $ ar ++ "issue/" ++ show newid ++ "/"
+
createIssue :: Integer -> String -> Category -> (Category, Integer)
+createIssue catid issuename topcat =
+  let newid = highIssueId topcat + 1
+      topcat' = addIssue catid (Issue issuename [] newid) topcat
+   in (topcat', newid)
+    where
+      highIssueId (Category cats issues _ _) =
+          maximum $ 0 : (map issueId issues) ++ map highIssueId cats
+      addIssue i' newissue (Category cats issues i name)
+          | i' /= i = Category (map (addIssue i' newissue) cats) issues i name
+          | otherwise = Category cats (issues ++ [newissue]) i name
+

Two functions to go. Now we want to show details of issues. This isn't too different from categoryDetailsH above, except for one feature: we need to know if a user is logged in. If they are logged in, we'll show an "add message" box; otherwise, we'll show a login box. Once again, we're getting the JSON representation easily.

issueDetailsH :: Integer -> Handler Tweedle RepHtmlJson
+issueDetailsH iid = do
+  Tweedle _ mvarTopCat _ <- getYesod
+  topcat <- liftIO $ readMVar mvarTopCat
+  (cat, issue) <- maybe notFound return $ findIssue iid topcat
+  let messageHelper m = Mapping $ map (second $ Scalar . Text . pack)
+        $ (maybe id (\x -> (:) ("status", x)) $ messageStatus m)
+        $ (maybe id (\x -> (:) ("priority", x)) $ messagePriority m)
+        [ ("author", messageAuthor m)
+        , ("text", messageText m)
+        , ("creation", show $ messageCreation m)
+        ]
+  let ho = Mapping
+          [ ("name", Scalar $ Text $ pack $ issueName issue)
+          , ("messages", Sequence $ map messageHelper $ issueMessages issue)
+          ]
+

Now we determine is the user is logged in via the maybeIdentifier function. Later on, we'll see how we can force a user to be logged in using authIdentifier.

ident <- maybeIdentifier
+
templateHtmlJson "issue-details" ho $ \_ -> return
+      . setHtmlAttrib "issue" ho
+      . maybe id (setHtmlAttrib "ident") ident
+      . setHtmlAttrib "cat" (Mapping
+          [ ("name", Scalar $ Text $ pack $ catName cat)
+          , ("id", Scalar $ Text $ pack $ show $ categoryId cat)
+          ])
+

And now the supporting model code. This function returns the requested Issue along with the containing category.

findIssue :: Integer -> Category -> Maybe (Category, Issue)
+findIssue iid c@(Category cats issues _ _) =
+  case filter (\issue -> issueId issue == iid) issues of
+      [] -> getFirst $ mconcat $ map (First . findIssue iid) cats
+      (issue:_) -> Just (c, issue)
+

Cool, just one function left! This should probably all make sense by now. Notice, however, the use of authIdentifier: if the user is not logged in, they will be redirected to the login page automatically.

addMessageH :: Integer -> Handler Tweedle ()
+addMessageH issueid = do
+  ident <- authIdentifier
+  (status, priority, text) <- runFormPost $
+          (,,)
+      <$> optional (input "status")
+      <*> optional (input "priority")
+      <*> required (input "text")
+  now <- liftIO getCurrentTime
+  let message = Message ident status priority text now
+  modifyDB $ addMessage issueid message
+  ar <- getApproot
+  redirect RedirectPermanent $ ar ++ "issue/" ++ show issueid ++ "/"
+
addMessage :: Integer -> Message -> Category -> (Category, ())
+addMessage issueid message (Category cats issues catid catname) =
+  (Category (map (fst . addMessage issueid message) cats) (map go issues) catid catname, ())
+    where
+      go (Issue name messages iid)
+          | iid == issueid = Issue name (messages ++ [message]) iid
+          | otherwise = Issue name messages iid
+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/04/hamlet-is-born.html b/public/blog/2010/04/hamlet-is-born.html new file mode 100644 index 00000000..0b7b2d6a --- /dev/null +++ b/public/blog/2010/04/hamlet-is-born.html @@ -0,0 +1,21 @@ + Hamlet is Born +

Hamlet is Born

April 20, 2010

GravatarBy Michael Snoyman

The past few weeks have seen a lot of changes to Yesod. As many of you know, my approach to Yesod is to split up as much functionality as possible into separate packages. Well, one of these has just matured into a first release: Hamlet 0.0.0 has been released.

+

By itself, Hamlet is a Haml-like language for writing templates. Templates are fully parsed at compile time via quasi-quotation, so you avoid a lot of the issues in dynamic templating systems. I'd like to think that this would be a natural fit for Haskell.

+

In the context of Yesod, I'm allowing Hamlet to replace two separate components: Data.Object.Html no longer exists, and there will be no HTML-like datatype. I feel good about this, since I was essentially recreating the (x)html libraries. There is an HtmlContent datatype, which is used to avoid a lot of the string escaping issues.

+

Additionally, I've dropped direct support for HStringTemplate. It will still be supported in the sense that any function can be used to generate content, but I don't intend to provide helper functions, as these would introduce a dependency on HStringTemplate.

+

I'm using Hamlet now throughout the Yesod codebase, and everything seems to be working just fine. There is a fairly comprehensive test suite, and a Hamlet section on the Yesod documentation site. I've tried to release this early as there seemed to be a fair amount of community interest, and I'd like to get the feedback sooner rather than later.

+

I'm sure there are lots of features that people would like to see in here, and I'd be happy to discuss adding them! At the same time, I want to avoid creating anything complicated here, so hopefully we'll find a good balance between features and confusion.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/04/pedantic.html b/public/blog/2010/04/pedantic.html new file mode 100644 index 00000000..cd8e8609 --- /dev/null +++ b/public/blog/2010/04/pedantic.html @@ -0,0 +1,56 @@ + Pedantic +

Pedantic

April 22, 2010

GravatarBy Michael Snoyman

I had a pedantic moment I wanted to share with you all while working on web-routes-quasi. It should also serve as a nice introduction to this (not yet released) package.

+

This package consists of essentially two components: a quasi-quoter which converts a simple syntax into a [Resource], and some template haskell code that generate a URL datatype, render, parse and dispatch functions based on this [Resource]. For now, we'll focus on just the parse function. It takes a [String] and returns either a URL (of the aforementioned datatype) or an error message; the error message is always "Invalid URL", so nothing truly interesting there.

+

.... and yes, pun intended on nothing...

+

Anyway, the template haskell code to generate the parse function basically runs through the list of Resources, generates a clause for each one, and then tacks on a catchall clause at the end to return the error message.

+

What went wrong

+

Well, there's one little corner case that I hadn't thought of. Let's say that we are writing an application where every route is valid. Then that catch-all clause will trigger an overlapping pattern warning. This seems rather benign for two reasons:

+
  1. What web application says that every possible input is a valid route?
  2. +
  3. You can always just disable the warning, or at least ignore it.
  4. +
+

However, I came up against a case that defeated both reasons:

+
  1. I'm writing a subsite for Yesod which handles static file requests. In such a situation, every request is theoretically valid and must be checked against the filesystem.
  2. +
  3. I'm pedantic, and hate all warnings.
  4. +
+

The Goal

+

So the goal is simple: determine when a list of resources covers every possible request, and if so, simply omit the catch-all clause.

+

This is where you get to see some web-routes-quasi internals. A Resource is defined as:

+
data Resource = Resource String [Piece] Handler
+data Piece = StaticPiece String
+           | StringPiece String
+           | IntPiece String
+           | SlurpPiece String
+data Handler = ByMethod [(String, String)]
+             | Single String
+             | SubSite String String String
+

The exact meanings of all of these constructors is well documented in the source code, so I will forgo it here. But basically, here's what's important for our purposes:

+
  • A slurp piece (which must be the last piece for a resource) collects all the remaining pieces in a request. A subsite does basically the same thing, passing all the remaining pieces to the subsite parser.
  • +
  • A string piece matches any single path segment.
  • +
  • Int piece matches only integral pieces; static piece matches a specific word. For purposes of determining completeness, any resource which includes a static or int piece must be thrown out. (Proof left as exercise to reader.)
  • +
+

So how do we determine whether a resource list is complete?

+

My Algorithm

+
  • Throw out resources with an int or static piece.
  • +
  • Count up the number of string pieces in each resource.
  • +
  • If the last piece is a slurp, or the handler is a subsite, consider this resource a "slurp resource".
  • +
  • Find the minimum number of string pieces for a slurp resource. If there exist non-slurp resources with string piece counts number from 0 to the minimum count for slurp resources (exclusive), then the resources are complete. Otherwise, they are incomplete.
  • +
+

Homework

+
  1. Why does my algorithm work?
  2. +
  3. I implemented it in 22 lines of code. It could easily be shorter; have fun! It's the areResourcesComplete function in Web.Routes.Quasi. Just make sure the test suite passes. (Unless someone finds a bug or makes the program clearer, I won't be including patches; this code need not be performant since it's run compile-time.)
  4. +
  5. Is there a better algorithm?
  6. +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/04/whats-cooking-with-yesod.html b/public/blog/2010/04/whats-cooking-with-yesod.html new file mode 100644 index 00000000..72bd8ab1 --- /dev/null +++ b/public/blog/2010/04/whats-cooking-with-yesod.html @@ -0,0 +1,30 @@ + What's Cooking with Yesod +

What's Cooking with Yesod

April 11, 2010

GravatarBy Michael Snoyman

I know I've been quiet on this blog for a few weeks now (though quite vocal on the web-devel mailing list...), so I just wanted to give a bit of an update on Yesod.

+

After the initial release, there has been a lot of collaboration in the community towards getting more solid tools available for Haskell web development. Most notably, Jeremy Shaw is working on the web-routes package; I'm not going to go into depth on it now, but it basically provides for type-safe URLs and pluggable web applications.

+

Yesod included in the initial release a quasi-quoted syntax for declaring routing in an application; a number of people have taken note of this. I have actually split this feature off into its own package and made it compatible with Jeremy's web-routes. Once again, I won't go into the details now, but it's a nicer syntax with more power than what's in Yesod now.

+

I'm also working on two other fronts outside of Yesod. The first is hamlet, which is a quasi-quoted templating system based (loosely) on haml. Even if you don't like the syntax of the templates, the monadic interface might allow for some very interesting features in its own right, and will most likely replace HStringTemplate as the preferred templating system in the next Yesod. I'd like community feedback on whether to maintain support for HStringTemplate.

+

The second front is persistent, a simplistic, non-relational data storage abstraction layer meant to be the basis of a model layer in Yesod. Unfortunately, this has received the least of my attention, and I'm not sure if it will be mature enough for the next release. This is another area where I'd appreciate community feedback.

+

I've just created a new branch on github for the next version of Yesod; I encourage everyone to check it out and let me know what they think! After this release, I will begin a very serious documentation effort. Due to the major changes coming up, documenting now would be a waste of time, but I hope the next release brings us much closer to a more stable API.

+

Finally, there's also progress on the next WAI release. This is more of an evolutionary than revolutionary change. So far, the only breaking change is that request and response headers are case-insensitive in the Eq instance. I've also added a buffer function to (hopefully) make enumerators more performant. I believe this will play very nicely with Hamlet to produce incredibly fast output.

+

So I believe the roadmap for the next few weeks will be:

+
  • Release some underlying libraries to work with transformers 0.2.0
  • +
  • Release hamlet
  • +
  • Release web-routes and web-routes-quasi
  • +
  • Release WAI
  • +
  • Release Yesod
  • +
  • Document like hell, replacing the current yesodweb.com site. I'll be placing documentation for all yesod-related packages on that site.
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/04/yesod-mini-tutorial.html b/public/blog/2010/04/yesod-mini-tutorial.html new file mode 100644 index 00000000..4370047d --- /dev/null +++ b/public/blog/2010/04/yesod-mini-tutorial.html @@ -0,0 +1,50 @@ + Yesod 0.2 Mini Tutorial +

Yesod 0.2 Mini Tutorial

April 24, 2010

GravatarBy Michael Snoyman

Well, I'll be flying home in a few days after a long trip to see family. As such, this will probably be my last time to do some Yesod work for a week, so I wanted to give a little status update.

+

There are two major changes coming with Yesod 0.2: Hamlet and web-routes-quasi. The former is going to be the preferred templating system for Yesod, replacing both HStringTemplate and the Data.Object.Html module. It has already been released and is fairly well documented.

+

The latter is a reworking of the former routing system for Yesod, split out and designed to work with web-routes. It features a simpler syntax, support for subsites, and (most importantly) delivers the ability to create type-safe URLs. It is not yet released, though there is a web-routes-quasi documentation page; that page may be a little out of date.

+

I'm very interested in hearing community feedback on Yesod at this point. I know most people will wait to try things out seriously when the documentation is written, but for now I thought I'd write a mini-tutorial on how things fit together.

+

Hello World

+
1 {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell #-}
+2 import Yesod
+3 data HelloWorld = HelloWorld
+4 mkYesod "HelloWorld" [$parseRoutes|/ Home GET|]
+5 instance Yesod HelloWorld where approot _ = ""
+6 getHome = return $ RepPlain $ cs "Hello World!"
+7 main = toWaiApp HelloWorld >>= basicHandler 3000
+

Line 1 shows you what language extensions Yesod requires, and line 2 imports the framework. Line 3 declares the HelloWorld datatype; every Yesod application has such a datatype which is the core of the application. In our simple HelloWorld, we don't need any information in it, but usually you store settings loaded from config files, database connections and the like in it.

+

Line 4 is magic: the end of the line is a piece of quasiquotes which creates a list of Resources. This is a feature provided by web-routes-quasi; see its documentation for syntax information. mkYesod is template haskell that does a number of things for us:

+
  • Creates a datatype for the URLs in our application. For our example, this is equivalent to data HelloWorldRoutes = Home
  • +
  • Creates an instance of YesodSite; this provided URL parse and render functions as well as a dispatch function. The dispatch function references the getHome function, which is why we define it on line 6.
  • +
+

Line 5 is a minimalistic instance of the Yesod typeclass; all applications must instantiate it. You can set various settings in it, but the only one necessary is approot. Line 6 defines the handler for the GET method of the application root. cs converts the string "Hello World!" into Content; RepPlain specifies that the content type is "text/plain".

+

And finally, line 7 creates a WAI Application and uses the basicHandler to serve the application via either simple server or CGI, based on environment variables.

+

Beyond Hello World

+

I'll just point out a few of the possibly surprising things in Yesod; everything will be covered more thoroughly in the upcoming documentation.

+
  • You can provide multiple representations of a response, and allow Yesod to choose the right one based on the HTTP "Accept" request header. This allows you to easily create applications with either an AJAX or static interface.
  • +
  • clientsession is used for handling session data.
  • +
  • There is built in support for OpenID and RPXnow authentication, but not internal site authentication. Hopefully, that will be added soon.
  • +
  • The current development version depends on a few packages not yet released, but which are all available in my github account. Obviously those will all be uploaded before release.
  • +
+

I might update this post with a few more points later, but I'm out of time for now! See you all in a week.

+

Update (April 25, 2010)

+

I've gotten all of the dependencies for Yesod uploaded onto Hackage, so all it should take to try it out is:

+
  • cabal update
  • +
  • git clone git://github.com/snoyberg/yesod.git
  • +
  • cd yesod
  • +
  • git checkout origin/ver0.2
  • +
  • cabal install
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/bigtable-benchmarks.html b/public/blog/2010/05/bigtable-benchmarks.html new file mode 100644 index 00000000..78ffc6e2 --- /dev/null +++ b/public/blog/2010/05/bigtable-benchmarks.html @@ -0,0 +1,44 @@ + Bigtable Benchmarks +

Bigtable Benchmarks

May 25, 2010

GravatarBy Michael Snoyman

Executive Overview

+

If you want the gist of it without the details:

+
  • Haskell CGI is very fast.
  • +
  • There are some major performance problems in both the fastcgi and direct-fastcgi packages.
  • +
  • hack-handler-fastcgi performs well, giving hope that we can have something similar for WAI.
  • +
  • Happstack performs very well on large loads, Snap has some room for improvement.
  • +
  • Yesod performs fairly well on CGI, but hopefully will benefit from speed improvements in wai-handler-fastcgi in the future.
  • +
+

Introduction

+

For the record, this has nothing to do with Google's database system. This is a benchmark for outputting a large amount of HTML. In particular, a table with 1000 rows, each with 50 columns, and each cell having the number of the column.

+

Normally, the benchmark just uses 10 columns, but I'm using 50 because (1) it's cooler and (2) the Snap Framework benchmark used 50 columns, and that's what got this whole ball rolling.

+

The main point of this post is to give insight into performance tradeoffs with different choices in Haskell, and in particular shed light on the best ways to serve Yesod applications.

+

Baseline

+

Let's start off by establishing some baseline results. I'll be running all tests- except for standalone servers- with lighttpd on a quad-core Ubuntu Lucid box with 4 GB of RAM. All tests are being run against localhost. I'm also using the Apache benchmarking tool (ab); it's not quite as precise as httperf, so please keep in mind a certain margin of error when looking at these numbers. All of the code for these benchmarks is available on github.

+

Anyway, the fastest result will obviously be serving a static file. The result for that was 2100+ requests/second. Graphing that number would just screw up the scale for the other results, so I've left it out.

+

For our purposes, we'll start by comparing raw CGI output. By raw, I mean simply printing to stdout without using a package. There are six different results in this benchmark: C, Perl, Python are obvious. There are then three versions of the Haskell benchmark for bytestrings, text and strings. Here's the graph, higher numbers are better.

+

The perl and python numbers aren't surprising; they need to be intrepretted for each pass. It's also not surprising that the string version is slower than bytestring. What is surprising is that text is slower than string! This will require more research, as it will have a major impact on Yesod, which uses text quite a bit through the Hamlet library.

+

Next, let's compare the performance of a few different CGI packages in Haskell.

+

Don't let the scale of the graph throw you off; these results are nearly identical to each other. This is comforting: the raw bytestring performance was fairly close to C performance, and the packages are keeping pace. Of course, CGI is not the optimal deployment method for a framework like Yesod, since there is so much initialization, so let's move onto FastCGI.

+

This is where things start getting depressing. The C library is performing incredibly well, and yet none of the Haskell libraries can keep up. WAI is merely a wrapper around direct-fastcgi, so that result isn't surprising. direct-fastcgi is a pure Haskell implementation of the FastCGI protocol; given its relative immaturity, it's not surprising that it hasn't been optimized yet.

+

What is shocking is how slow the fastcgi package is, especially since it's just wrapping around a C library. Considering the fact that FastCGI is really my main method for deploying Yesod right now, I was very concerned. I decided on an off chance to try out hack-handler-fastcgi, and amazingly, it performed very well. I did a very quick conversion to make that package work with WAI, revealing much better results:

+

There's obviously room for improvement, but at least we're in the right ballpark. This code is not yet released on Hackage; I want to clean it up a bit more first, but it will almost certainly be replacing the current version of wai-handler-fastcgi.

+

Standalone servers

+

I ran two different versions of the Snap benchmark: one from their repository, and a more optimized version that I wrote. I do this to point something out: it is very easy with the Snap API to write very poorly performing code, since you can unwittingly build up a large amount of data in memory.

+

Even so, the Snap results are much less than expected. I was in touch with Gregory Collins about this, and he found one performance bug almost immediately that gave the numbers a 40% boost. Moral of the story: Snap has a lot of potential, but it's still very young.

+

And finally, Yesod

+

I've done a very good job of not talking at all about Yesod yet. That's because the results for Yesod are very much tied in with the above results. I'm sad to say that, today, there is no truly performant base upon which to run Yesod. That doesn't mean you can't use Yesod; I'm running it in production settings without too much difficulty. It just means that I have my work cut out for me.

+

In addition to the server-side issues, Yesod itself has not been optimized at all, so there's probably plenty of room for improvement in the core library itself.

+

Due to Yesod's initialisation process, the CGI results are noticeably slower than raw CGI, though not the slowest benchmark we've seen today. fastcgi is still suffering from the direct-fastcgi package, and simpleserver is suffering from its own problems (after all, it was never intended to be given a load like this).

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/hamlet-version-0-2.html b/public/blog/2010/05/hamlet-version-0-2.html new file mode 100644 index 00000000..e5ba9ba7 --- /dev/null +++ b/public/blog/2010/05/hamlet-version-0-2.html @@ -0,0 +1,35 @@ + Hamlet Version 0.2 +

Hamlet Version 0.2

May 9, 2010

GravatarBy Michael Snoyman

This version isn't quite released yet, and I haven't had a chance to update the documentation, but I wanted to get the list of changes out there while it's still fresh.

+

The API has not changed at all, but the syntax of Hamlet templates has changed significantly. This is based on some real-world experience. Just to give a little frame of reference, I tested migrating the codebase for Search-Once.com to the new Hamlet, and the result was a net 322 reduction in lines of code.

+

Reversal of identifiers

+

The first major change is a reversal of the order of identifiers in references. In other words, you previously would write person.name, which got translated to the Haskell code name person. I originally chose that ordering to fit in with the object-oriented nature of many templating systems.

+

After much use, I've decided it just feels wrong to have identifiers in the reverse order to how Haskell naturally treats them. This is especially true when not dealing with objects; if you are just applying functions, everything is backwards! So now, to apply the function name to the value person, you simply write name.person.

+

No argument

+

Previously, a Hamlet template would generate a function which would take some value, and it would apply all the references inside the template to that value. For example: +[$hamlet|$name$|] +would roughly generate the following: +\arg -> hamletOutput (name arg) +

However, there are many templates that don't need that argument. Also, there are many times when you want to call a function without passing it any arguments. So now, arguments are gone! To get the same effect as above, you would write the following:

+\person -> [$hamlet|$name.person$|] + +

Other changes

+
  • A number of minor bug fixes. If you care to see what those are, look at the Git log.
  • +
  • Added $maybe. Very useful for error messages.
  • +
  • You can use constructors directly in your references. Very useful when constructing URLs.
  • +
  • You can now include query-string parameters along with your URLs. The syntax is @?variableName@, where variableName :: (url, [(String, String)])
  • +
  • Beginning-of-line escaping. If you begin a line with a backslash, the backslash is dropped and the rest of the line is treated as content. This makes it possible to start a line with a pound or period without it being interpretted as tag information. As a special case, a line containing only a backslash is converted to a newline.
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/laying-the-foundation.html b/public/blog/2010/05/laying-the-foundation.html new file mode 100644 index 00000000..e63395c7 --- /dev/null +++ b/public/blog/2010/05/laying-the-foundation.html @@ -0,0 +1,42 @@ + Laying the Foundation +

Laying the Foundation

May 20, 2010

GravatarBy Michael Snoyman

I'm still working on some of the underpinning libraries for Yesod, and wanted to report on some of the recently released packages. The main theme is still to reduce dependencies.

+ +

clientsession 0.4.0

+

clientsession 0.3.0 had three notable dependencies: SimpleAES, dataenc and pureMD5. I had a number of issues with this:

+
  • SimpleAES didn't work on 32-bit systems (bad).
  • +
  • All three of these packages treated data differently: strict bytestrings, lazy bytestrings and string.
  • +
+

I really disliked all the conversions, so I decided to do something truly evil: I wrote the whole thing in C. For those who don't know, clientsession takes some data, hashes it, encrypts it and base64-encodes it, so that you can stick it in a cookie and not worry about tracking session data on your server. I used a BSD checksum-inspired hash algorithm and wrote a basic base64-encoder based on some prior art. I also went with a pure C AES implementation to avoid architecture issues.

+

Upshot: library is now very fast, no dependencies, and your data doesn't get passed around a bunch.

+

wai-extra 0.1.2

+

The 0.1 series has been fun so far. 0.1.0 removed the clientsession middleware (best to just use the clientsession library directly) and thereby cut dependencies drastically. 0.1.1 switched from the zlib package to an enumerator-based wrapper around the C zlib library. 0.1.2 introduces the Network.Wai.Parse module, which handles query string, POST request, Accept header and cookie parsing. This is meant to be a replacement for web-encodings, specialized for the WAI datatypes.

+

Overall, the library should be much more efficient than web-encodings, since it uses some strict bytestring specific tricks like unfoldrN to squeeze out performance.

+

wai 0.1.0

+

This is a relatively minor change, but did introduce a breaking change in the API, so it gets a major version bump. Gregory Collins pointed out that it didn't make much sense to do a case-sensitive comparison for request and response headers, so this version fixes that. The only breaking change is that the RequestHeader and ResponseHeader constructors now take two parameters: the header itself and then a lowercase version. However, you should use the requestHeader and responseHeader functions instead, which handle the case conversion for you.

+

I also released wai-handler-fastcgi 0.0.0.2 to include this version bump. wai-extra reflects this bump in 0.1.2.

+

hamlet 0.2.3.1

+

Just a minor fix for Windows to account for carriage returns in the quasi-quoted data. However, I am beginning to think about internationalization in Hamlet; if anyone's interested in this, please let me know and I'll put up a post with my ideas.

+

Next steps

+

There was a good discussion on a previous post about web-routes-quasi, and zs had some very good ideas. I think I'll be working on web-routes-quasi 0.3.0 and include that in Yesod 0.2.0. Sorry for pushing off the release, but I think it's worth it.

+

The change will allow you to have arbitrary datatypes in your routes. For example:

+/book/lookup/#ISBN BookLookupR GET + +

And then the ISBN datatype will be able to parse the path piece appropriately. It involves a fairly substantially modification of the Template Haskell URL parse code, but I don't think it should be too much of a problem.

+

Also, I've just pushed version 0.2.0 of Yesod into the master branch; I should have done that quite a while ago.

+

The other big change is that I'm cutting ties from web-encodings. The main reason for this is specialization: web-encodings works with Strings, strict/lazy ByteStrings and strict/lazy Texts; WAI clearly defines all the datatypes, and so I can use more efficient functions. The best example is using unfoldrN for URL decoding.

+

The big hurdle was writing the multi-part parser; I aimed to avoid bytestring concatenations as much as possible, and so it should be a fairly fast, space-efficient implementation.

+

I haven't uploaded any of these packages yet, but they're all available on Github. I'm also trying to test all the packages on Windows now as well; so far found a few minor issues with missing libraries and CRLF in Hamlet, but nothing too serious.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/migrating-to-yesod-0-2.html b/public/blog/2010/05/migrating-to-yesod-0-2.html new file mode 100644 index 00000000..5028f4d1 --- /dev/null +++ b/public/blog/2010/05/migrating-to-yesod-0-2.html @@ -0,0 +1,42 @@ + Migrating to Yesod 0.2 +

Migrating to Yesod 0.2

May 3, 2010

GravatarBy Michael Snoyman

I've nearly completed the migration of my largest site from the current version of Yesod to the upcoming Yesod 0.2 release. I just wanted to share some of my experience.

+
  • In this site, the code fairly evenly splits into three parts: database interaction, logic code (site parsing to be specific) and the web interface. As would be expected, the first two required no modification at all.
  • +
  • The most time was spent migrating from HStringTemplate to Hamlet, but I'm very happy with the results over all. Overall, I'd break it down like this: +

    Advantages

    +
    • Easier to follow nesting.
    • +
    • Less typing.
    • +
    • Nicer to declare a new datatype than to create arbitrary nested-string-like data structures.
    • +
    • Type-safe URLs are a huge win.
    • +
    • When I'm lazy in a conversion, I can just copy in HTML code, and it usually works.
    • +
    +

    Disadvantages

    +
    • There's not good way to conditionally add a attribute. Most of the time, that's no big deal; but when you want to add "selected" to an option tag, it's an annoyance. I ended up writing code like this: +
      <option foo=bar
      +$if shouldBeSelected
      +   selected
      +></option>
      +This is clearly sub-optimal.
    • +
    • You can have multi-word values for attributes embedded in the template. So in order to achieve <input type="submit" value="Click me">, I ended up writing %input!type=submit!value=clickMe, and declaring a function clickMe _ = cs "Click me".
    • +
    • It can be a little tedious having all those extra datatypes just for template arguments, but I suppose that's the price to pay for type safety.
    • +
    +
  • +
  • I much prefer the new route syntax. Previously, it used Yaml; that was really too verbose. Now each resource is defined on a single line, and handler names are automatically determined from the constructor name.
  • +
  • Type-safe URLs already caught a few bugs for me. I think this will be a major cornerstone for Yesod programming. I can now easily write code such as redirect RedirectTemporary $ UserPage userId, and it just works.
  • +
  • Subsites are great, but it's a very complicated concept. It's made web-routes-quasi more complex than I would have hoped to, and necesitated added some extra information to the Handler monad. I think that this will pay off in the end; unfortunately, Yesod 0.2 subsites may still be a little unpolished.
  • +
+

For those of you playing around with Hamlet, I would definitely point out: do not be afraid to define helper functions that ignore their first argument. It's been completely necessary for me.

+

I'm still adding tutorials, refining the documentation and making some code changes, but I should be releasing very soon (latest would be next week). I encourage everyone to check out the documentation and give me any feedback you have! I'm especially interested in people's thoughts on a persistance layer, which is notably lacking right now.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/persistence-thoughts.html b/public/blog/2010/05/persistence-thoughts.html new file mode 100644 index 00000000..da83bb02 --- /dev/null +++ b/public/blog/2010/05/persistence-thoughts.html @@ -0,0 +1,96 @@ + Persistence Thoughts +

Persistence Thoughts

May 26, 2010

GravatarBy Michael Snoyman

Necessity is the mother of invention. I've been playing around with ideas for a persistence layer for a while now, but it wasn't until I got a hard deadline for a database-driven website that things really clicked. I want to share these thoughts here, both to get them down in writing, and to hopefully provoke some discussion.

+

There's some version of the code I discuss here available on github. It doesn't match the code here precisely, and is not what the final version will look like. But it does show how we can support multiple backends!

+

Pieces of the puzzle

+

There are going to be three separate components of persistent:

+
  • A high-level interface for declaring entities. There will be a set of datatypes, and perhaps either a DSL, Quasi-Quoted syntax or external files for a nicer user interface.
  • +
  • A set of type-classes for doing the actual operations on tables. The majority of this post will discuss this.
  • +
  • Each backend will have its own Template Haskell code for generating instances of the type classes based on the higher-level interface. The SQL generators will probably share a lot of code, but that's mostly irrelevant to the outside user.
  • +
+

Type families

+

My proposal makes heavy use of type families. If you want a quick introduction to type families, they're basically a method to associate two different datatypes together.

+

Let's give a pertinent example. Every record in a table will have a unique key associated with it. If we're talking about a SQL backend, this will most likely be an auto_increment (MySQL) or serial (PostgreSQL) integer, but Amazon SimpleDB prefers to use UUIDs. So I want to associate a key value based on the datatype of the value and the backend. In type families, this might look like:

+
data family Key value backend
+

However, in general we'll be defining these inside typeclasses, so it will look more like:

+
class HasTable value backend where
+    data Key value backend
+

Then you would declare an instance like so:

+
instance HasTable MyValue MyBackend where
+    data Key MyValue MyBackend = MyKey Integer
+

No relation

+

I've mentioned a number of times in previous posts the idea of no relations. Let me clarify here: persistent will not automatically load related data from other tables. There are three good reasons for this approach:

+
  1. Not all backends natively support relations. Think of a set of CSV files, or Google BigTable for that matter.
  2. +
  3. Relations will greatly complicate things, and I really want something simple.
  4. +
  5. Arguably, there's a performance edge. But I really don't want to discuss performance right now.
  6. +
+

Multiple type classes

+

So without relations, every record is really just a single row in a single table. We need to be able to do two things:

+
  1. Convert our value to a record.
  2. Convert a record to our value.
+

That's all well and good, but what's a record? If we're talking about CSV files, it's [String]. But in a database (using HDBC) it's [SqlValue].

+

So we'll need to have different typeclasses for different backends. However, since we have multiple Template Haskell generators, all is fine: the CSV TH code knows to use the IsStringList class, while the PostgreSQL TH uses IsSqlValueList... or whatever it's called.

+

However, some database operations require more than just this. What if you want to update a record? What if you want to search? Or sort?

+

I propose a set of classes akin to the following:

+
class HasField a where
+    data Field a
+    applyField :: Field a -> a -> a
+
+class HasFilter a where
+    data Filter a
+    applyFilter :: Filter a -> a -> Bool
+
+class HasOrder a where
+    data Order a
+    applyOrder :: Order a -> a -> a -> Ordering
+

Let's see a sample set of instances.

+
data Person = Person String Int
+instance HasField Person where
+    data Field Person = PersonName String | PersonAge Int -- type safe!
+    applyField (PersonName name) (Person _ age) = Person name age
+    applyField (PersonAge age) (Person name _)  = Person name age
+
+instance HasFilter Person where
+    data Filter Person = PersonNameEq String -- name equality
+                       | PersonAgeLt Int     -- age is less than parameter
+    -- we could add as many filters here as we like
+    applyFilter (PersonNameEq x) (Person y _) = x == y
+    applyFilter (PersonAgeLt x) (Person _ y) = y < x="" instance="" HasOrder="" a="" where="" data="" Order="" Person="NameAsc" --="" name,="" ascending="" |="" AgeDesc="" --="" age,="" descending="" applyOrder="" NameAsc="" (Person="" x="" _)="" (Person="" y="" _)="compare" x="" y="" applyOrder="" AgeDesc="" (Person="" _="" x)="" (Person="" _="" y)="compare" y="" x="" --="" reverse<="" /code="">
+

Notice how the names of the constructors in there don't clash. Once you have these data families, you get to do stuff like:

+
class HasField v => Updateable v backend where
+    update :: Key v backend -> [Field v] -> IO ()
+class HasFilter v => Searchable v backend where
+    select :: [Filter v] -> IO [(Key v backend, v)]
+

In case it's not apparent, let me point out the huge win here: everything is completely type checked. We're not passing around SqlValues or something like that. Every single field requires a matching datatype, or it simply won't compile.

+

Of course, this won't be enough information for SQL. It would want something additional such as:

+
class HasFilter v => SqlFilter v where
+    toWhereClause :: Filter v -> String
+    toWhereData :: Filter v -> SqlValue
+instance SqlFilter Person where
+    toWhereClause (PersonNameEq _) = "name=?"
+    toWhereClause (PersonAgeLt _) = "age<?"
+    toWhereData (PersonNameEq x) = SqlString x
+    toWhereData (PersonAgeLt x) = SqlInteger x
+

The beauty is that Template Haskell will address all these problems for us. You'll be left with being able to just use the select function, without writing any boilerplate.

+

Coming up

+

I'm actually actively using code very similar to this in developing a production website. There's a hard deadline for it of less than a week away. You might be wondering why I'm blogging when there's a deadline... good question.

+

Anyway, the actual type classes are slightly more complex than what I've laid out here, but I think this gives a good overview of the approach. I haven't started working on the high-level code or the Template Haskell yet. One idea I have in mind is putting enough information in the high-level design to be able to automatically generate web forms from them. In fact, I envision a Yesod subsite capable of letting you fully interact with a table in a database.

+

Questions for the audience

+
  • MySQL support in HDBC does not seem too strong. HDBC also has some annoying tendencies: it doesn't let you manually finalize statements, for example. Is it worth coding the SQL backends straight against the C libraries?
  • +
  • SimpleDB sounds like a good candidate for a non-SQL backend. CSV files good be a fun one as well. Any other thoughts?
  • +
  • Where's Waldo?
  • +
  • What would you like the high-level interface to look like?
  • +
  • There will almost certainly be an enumerator/iteratee interface for this library. What style of enumerators are people interested in? I know the iteratee package is all the rage, but the new 0.4 version will be completely changing the internals.
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/persistent-blog-example.html b/public/blog/2010/05/persistent-blog-example.html new file mode 100644 index 00000000..6600fcc4 --- /dev/null +++ b/public/blog/2010/05/persistent-blog-example.html @@ -0,0 +1,33 @@ + Persistent Blog Example- with Redis support! +

Persistent Blog Example- with Redis support!

May 31, 2010

GravatarBy Michael Snoyman

One of the typical examples applications given by web frameworks is the blog: it shows basic CRUD operations, templating a dispatch- in other words, Model, View and Controller. The current Yesod blog tutorial includes no persistence support, so one of the goals of persistent is to make it possible to have a very light-weight blog tutorial.

+

Well, it's not light-weight yet, but I've included the blog tutorial in the persistent repo. And here's the really cool part: there's a sqlite version and a Redis version!

+

Redis support is far from complete (and I mean far), but I think this is a very nice proof-of-concept. And to my knowledge, this might make Yesod the first web framework to have support for the new generation of NoSQL, key-value datastores.

+

New high-level syntax

+

I've also scrapped the ugly YAML-based syntax that I'd cobbled together for something which, in my opinion, in much nicer. I'd appreciate feedback on it. The basic idea is:

+
  • Any non-indented line should contain the name of the table. Table name must be capitalized.
  • +
  • All indented lines (ie, anything beginning with a space) are attribute lines, and are applied to the most recent table name.
  • +
  • An attribute line beginning with a lowercase defines a column. The first word is the column name, the second is the datatype of the column, and the remaining words are attributes of the column. For example, null means the field is nullable, Eq means the field can be filtered by equality, etc.
  • +
  • An attribute line beginning with an uppercase defines a unique key. The first word is the constructor name, and the following words are the column names in the unique key.
  • +
+

Here's the definition taken straight from the blog example:

+
Entry
+    slug String
+    date Day Desc
+    title String
+    content HtmlContent
+    UniqueSlug slug
+

Here, we can sort in descending order by date, and the slug must be unique.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/persistent-progress.html b/public/blog/2010/05/persistent-progress.html new file mode 100644 index 00000000..818feac6 --- /dev/null +++ b/public/blog/2010/05/persistent-progress.html @@ -0,0 +1,86 @@ + Persistent Progress +

Persistent Progress

May 28, 2010

GravatarBy Michael Snoyman

I just switched over a 2900-loc Yesod web application to use persistent, so I think now's a good time to give a bit of an update on the way things are going. For those interested, here's the most recent Haddocks.

+

Status

+

Amazingly, persistent is fully usable right now for certain functionalities. There's a high-level interface for defining tables, Template Haskell code to generate SQLite code, and the Persist typeclass. You can try it now if you like, though be warned that it will be changing.

+

Nothing is NULL

+

Pop quiz: does the following code return all the rows in a table?

+
SELECT * FROM MyTable WHERE my_column=5
+UNION ALL
+SELECT * FROM MyTable WHERE my_column<>5
+

It seems like the answer should be yes, but in fact, it's no. That's because both NULL=5 and NULL<>5 evaluate to FALSE. Technically, they evaluate to NULL, which gets interpretted in a boolean context as FALSE, but it's irrelevant to our point.

+

This makes it difficult to model a Maybe value as a nullable field. The solution isn't too difficult; if you want to filter for all rows where column my_column is equal to some Nothing value, you specify IS NULL instead of =? and binding the Nothing.

+

However, when dealing with unique keys and the getBy function, it gets a bit more complicated. So for now, I have a simple compromise: nullable columns cannot be included in unique keys.

+

Less type classes

+

I've decided to backtrack a bit on my multi-type-class idea in the previous post. Instead, I'm having the TH code generate a single type class instance for each table.

+

I've also decided to simplify the data families a bit. Here's the motiviation: I want to be able to have a PersonId datatype to go along with the Person table. The previous definition of Key was data family Key value m, where m is the monad for the backend we're dealing with (such as Sqlite).

+

Unfortunately, that means the definition of PersonId because type PersonId m = Key Person m, which is really irritating. So instead, Key is now simply data family Key value.

+

The upshot of this is that there can only be one instance for each value in scope at a time. However, this really makes perfect sense: the TH code generates the datatypes! So in order to switch backends, you'll need to change your TH deriving line.

+

YAML entity declarations

+

I'll be honest: I really don't like this solution, and find it ugly. However, it was the fastest thing to implement, and I needed something to work with immediately for my current project. Hopefully I can put some real thought into a better syntax later. In fact, I think an EDSL would be better here than quasi-quoted syntax.

+

Here's an example that would declare two datatypes and create appropriate instances for the Sqlite3 backend.

+
persistSqlite3 [$persist|
+Author:
+    columns:
+        - name: name
+          type: String
+          order: [Asc] # can be sorted in ascending order
+        - name: tagline
+          type: String
+          nullable: True
+    uniques:
+        UniqueAuthorName: [name]
+Entry:
+    columns:
+        - name: author
+          type: AuthorId # this is declared automatically
+          filter: [Eq]   # you can select all entries by an author
+        - name: date
+          type: Day
+          order: [Asc, Desc]
+        - name: slug
+          type: String
+        - name: title
+          type: String
+        - name: content
+          type: HtmlContent
+    uniques:
+        UniqueDateSlug: [date, slug]
+|]
+

Using HDBC... for now

+

To get started quickly, I'm using HDBC. The obvious advantages are:

+
  • Automatic support for all HDBC backends.
  • +
  • No need to write against the FFI.
  • +
  • Well tested library.
  • +
  • SqlValue and Convertible are defined for us.
  • +
+

The downsides are:

+
  • Even with automatic support for the backends, we still need to be careful of writing compliant SQL. For example, INTEGER PRIMARY KEY versus INTEGER AUTO_INCREMENT versus SERIAL.
  • +
  • MySQL support is pretty spotty on HDBC if I'm not mistaken.
  • +
  • It still doesn't help us with the non-SQL backends at all.
  • +
  • We're probably incurring some some performance overhead.
  • +
  • HDBC isn't very good at deterministic operation. It's difficult to tell when a statement will be finalized, and if you use the lazy I/O, there's lots of opportunities for bugs.
  • +
  • It's not quite as bug-free as I'd like. Details available upon request.
  • +
  • Ideally I'd like a PersistValue instead of SqlValue datatype that takes into account non-SQL backends.
  • +
  • I don't like the design of Convertible: there's no distinction made between conversions which are guaranteed to succeed and those which might fail. This encourages errors ocurring in pure code. I proposed a different interface to John, and he seemed interested at the time, but nothing has come of it since.
  • +
+

I have a lot of experience with the Sqlite C API, and it's very easy to program against. I'm not so certain of MySQL and PostgreSQL, but frankly the hardest work in this project is the TH code, not the database code itself.

+

Of course, if we stick with HDBC it will be easier for people to drop down to straight SQL queries when persistent is too constraining.

+

Next steps

+

It would be really nice to be able to automatically generate web forms from this tables, especially on my current project. This isn't very difficult in theory, I just have to figure it out.

+

I'm not sure if for this first release I should focus more on a number of backends, or on perfecting a single one. If I do multiple backends, it might point out places where the API favors a certain backend too much, but I'd really like to get something working out there quickly so that Yesod can have a persistence layer already.

+

And I still have to choose an enumerator interface for the select, filter and order functions. For those afraid of enumerators, I intend to offer a strict list variant as well. I'm tempted to join everyone else with the iteratee package... I'd appreciate community feedback on this one.

+

If you want to get a better feel for this library, start by looking at the Persist typeclass. You can also look at a simple test program. Hopefully I'll put up a simple Yesod app that uses this soon.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/really-type-safe-urls.html b/public/blog/2010/05/really-type-safe-urls.html new file mode 100644 index 00000000..850db277 --- /dev/null +++ b/public/blog/2010/05/really-type-safe-urls.html @@ -0,0 +1,46 @@ + Really type-safe URLs +

Really type-safe URLs

May 13, 2010

GravatarBy Michael Snoyman

It's my son's second birthday. Yay!

+

Anyway, on to the real stuff. Every once in a while, you get those "eureka" moments, where something that's been bothering you finally clicks. In this case, it was killing two birds with one stone. The stone is a small changes to web-routes-quasi which makes the generated URL datatypes even more typesafe. But before we talk about the stone anymore, let's discuss those birds.

+

Bird #1: Useless data

+

I've been collecting some data in web-routes-quasi which I've been completely ignoring. For those of you not familiar, web-routes-quasi defines a nice, simple, quasi-quoted syntax for declaring URL dispatch. It converts something like this: +

/            RootR  GET
+/entry/$slug EntryR GET
+into something like this: +
data BlogRoutes = RootR | EntryR String
+It also generates a parse function, dispatch function, and notably here a dispatch function which requires the user to define the following two functions: +
getRootR :: Handler
+getEntryR :: String -> Handler
+

+

This is all very nice, but there's one thing bothering me: in the second line of the quasi-quoted data, what the hell does the word slug do? It's just sitting there, having no influence on the code! Well, it does give the programmer an idea of the purpose of that piece of the URL, but the computer completely ignores it.

+

Maybe this isn't the worst thing in the world, but it was always lurking in the back of my mind, waiting...

+

Bird #2: Not completely type-safe URLs

+

We've come a long way from the horrors which is PHP. Jeremy Shaw's web-routes package encourages us to never, ever use direct string concatenation to generate URLs. Instead of: "/foo/" ++ bar ++ "/" ++ show baz ++ "/" we can write a beautiful render $ Foo bar baz, and the compiler checks all of our errors for us.

+

Wait... all of our errors? Maybe not. Let's say I'm writing an application to calculate your BMI. I want to construct URLs with the datatype data BmiRoute = BmiRoute Integer Integer, where the first integer is your height in centimeters and the second is your weight in kilograms.

+

I'm sure you can see where this is going: One day I make a little mistake and type in BmiRoute weight height, and suddenly the entire population is obese.

+

Damn you web-routes! You failed me! I exclaim in fury as 2000 people e-mail me to let me know how fat they are. On the bright side, I end up getting a very nice check in the mail from Weight Watchers.

+

I'm sure most Haskellers already know the solution to this little predicament: I defined my types badly. Instead, I should have done: +

newtype Height = Height Integer
+newtype Weight = Weight Integer
+data BmiRoute = BmiRoute Height Weight
+Then, when I stupidly type in BmiRoute (Weight weight) (Height height), the compiler automatically catches the mistake.

+

That may have been a silly example, but imagine instead we were dealing with database keys; do you want to get your user_id confused with your email_id? I didn't think so.

+

The problem is, that web-routes-quasi only handles Integers, Strings and [String]s. So what's a man to do?

+

The stone

+

Many of you have probably already figured out what I'm about to say, but I'll spell it out anyway, cause I'm just too excited to keep quiet (it is my son's birthday after all). That useless syntax in the quasi-quoted routes will now define the datatype of the parameter. To use the BMI example, I could write /bmi/#Height/#Weight Bmi GET, and the resulting constructor will be Bmi Height Weight.

+

The pound sign here will still mean only to accept integers, but now the fromInteger method will be called to convert the plain Integer into a Weight and Height. For strings and slurps, you use the ToString and IsSlurp typeclasses, respectively. ToString is a subclass on IsString, adding the toString function, and IsSlurp defines the toSlurp and fromSlurp functions.

+

Epilogue

+

I e-mailed the web-devel list just yesterday saying I thought Yesod 0.2 was feature complete; well, this change is definitely going into Yesod 0.2. The change in web-routes-quasi is done, and will be released as version 0.2 shortly (I'll do a little more testing first).

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/status-update.html b/public/blog/2010/05/status-update.html new file mode 100644 index 00000000..5350800a --- /dev/null +++ b/public/blog/2010/05/status-update.html @@ -0,0 +1,44 @@ + Status Update +

Status Update

May 16, 2010

GravatarBy Michael Snoyman

I announced on the web-devel list that Yesod 0.2 is basically feature complete, and almost ready for release. I decided to finally go and do some maintenance on some of the underlying libraries, so I wanted to give some updates. Some of this is stuff I've been itching to do for a while, so I'm pretty excited it will be included for Yesod 0.2.

+

Enumerator-based compression

+

The one big weak link in the enumerator chain for WAI has been the GZIP middleware; it was originally based on the standard zlib package on Hackage, which uses lazy I/O. In essence, we were losing a lot of the benefits of the WAI whenever we compressed output.

+

But not anymore! I've released wai-extra 0.1.1, which includes an enumerator-based binding to the zlib library.

+

The new wai-extra 0.1 series is designed to minimize external dependencies. I dropped the clientsession and methodoverride middlewares (I don't think they'll be missed), and drop dependencies on a few other packages. I believe the only non-Haskell Platform packages are now sendfile and wai, and clearly the latter has to stay in.

+

As a future roadmap, I'm considering porting over the encoding functions in web-encodings and specializing them for datatypes in WAI. For example, decodeUrlPairs would only work on strict bytestrings, since that's how WAI provides the query string.

+

wai-handler-fastcgi

+

I originally wrote hack-handler-fastcgi as a port of the fastcgi package on Hackage, which in turn wraps the C library. I was very excited when Dan Knapp released direct-fastcgi, and recently I wrote a wrapper around it that allows you to run your WAI applications with it. I'm using it in production with lighttpd for my most recent client site.

+

One little annoyance: there's a debugging line that outputs "fromList []" for every request. I emailed Dan about this, and hopefully he'll remove it; in the meanwhile, for version 1.0.1.1, you can just remove line 1704 in Network/FastCGI.hs.

+

yaml

+

With some help from Anton Ageev, yaml, data-object-yaml and data-object-json all got a bit of an update, including some iteratee goodness. I won't repeat myself: you can read the release announcement.

+

failure et al

+

The failure package got bumped to 0.1.0. The only real change was renaming MonadFailure to Failure. This created a cascade of releases for control-monad-failure, attempt, safe-failure, control-monad-attempt, etc.

+

This package makes failure handling very nice, I encourage people to look into it.

+

clientsession

+

And now a bit of a wart. I really didn't like having the Crypto dependency, since it takes forever to build all the tests, especially on a low-memory server. I e-mailed them a patch to make the tests optional, but it hasn't been applied. On top of that, I have a feeling that the AES implementation in Crypto is very inefficient, since it deals directly with lists of Word8s.

+

I tried switching over to SimpleAES. In fact, I even released clientsession 0.3.0 based on SimpleAES. However, it seems there's some bug for 32-bit systems (my server again). I traced it down to something in assembly, at which point I gave up.

+

My next step was to try pulling the AES code into the clientsession repo to avoid the test building, but that still runs very slowly.

+

So I just wrote some code in a separate branch that uses incredibly fast XOR encryption. Combined with some compression to avoid frequency analysis, this may be enough, but I definitely want a more robust solution in place by release.

+

bloggy

+

And by popular demand, I've ported bloggy to Yesod 0.2. I personally don't think it's a great way to learn Yesod- I believe the tutorials give a better view without clouding the scene with data serialization issues- but for those who want it, it's available.

+

What's next

+

I won't even consider releasing Yesod until I can go a few days without making a breaking API change. I'm still finding minor things all over the place where a change is nice, and I'd rather have a solid release than an earlier one.

+

Also, my next project is massively database-driven, so I'm already thinking hard about the next Yesod release. Here's the thoughts I've been tossing around, very much unpolished:

+
  • Define entities but not entity relationships. Allow entities to hold references to other entities (ie, keys). And the layer will use no joining at all.
  • +
  • Create a Query datatype somehow, and values will be created using combinators like filterBy someField myValue or sortBy someField.
  • +
  • Have some typeclass for converting a Query into various monads.
  • +
+

As I said, very unpolished... and it's late... hopefully better stuff tomorrow.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/05/yesod-0-2-0-released.html b/public/blog/2010/05/yesod-0-2-0-released.html new file mode 100644 index 00000000..076cf748 --- /dev/null +++ b/public/blog/2010/05/yesod-0-2-0-released.html @@ -0,0 +1,29 @@ + Yesod 0.2.0 Released +

Yesod 0.2.0 Released

May 24, 2010

GravatarBy Michael Snoyman

I'm very happy to announce the release of Yesod 0.2.0. For those who don't know, Yesod is a web framework for Haskell that promotes static type-safety, RESTful design, conciseness and efficiency.

+

You can find tutorials and other documentation on the Yesod documentation site. This site is also the home to documentation on some Yesod subprojects.

+

This release introduces some major changes from the previous 0.0 series. The largest of these include:

+
  • Type-safe URLs via the web-routes-quasi package. You no longer splice together strings to create URLs; each application has a datatype which only allows encoding of valid URLs.
  • +
  • Compile-time checked templates via the hamlet package. Hamlet has a Haml-like syntax and can be embedded directly in Haskell source code via quasi-quoting. Yesod ties Hamlet and web-routes-quasi together to ensure type-safe URLs in your templates as well. Hamlet has many nice features for creating HTML output, I will not enumerate them here.
  • +
  • Subsites are a new feature which allows you to create web components that can be plugged into multiple sites. Yesod includes two subsites: authentication and static files.
  • +
+

Yesod is still based on the Web Application Interface (wai), allowing it to be served from multiple backends. I will be releasing benchmarks in a few days as I wait for some feedback on one more backend which will hopefully be released soon.

+

Various other sources of Yesod information:

+
  • I gave a talk to the Southern California Functional Programmers group. There are slides and audio.
  • +
  • I run my blog with a little bit of Yesod code I endearingly call bloggy.
  • +
  • If you have questions, please consider joining the Haskell web devel mailing list.
  • +
+

I'm using this code in production, so it is fairly well tested. However, if you find a bug, or even think you've found a bug, please let me know. And stay tuned to this blog to hear plans for the next Yesod release.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/first-persistent-new-yesod.html b/public/blog/2010/06/first-persistent-new-yesod.html new file mode 100644 index 00000000..3412a69b --- /dev/null +++ b/public/blog/2010/06/first-persistent-new-yesod.html @@ -0,0 +1,33 @@ + First Persistent release, new Yesod release +

First Persistent release, new Yesod release

June 22, 2010

GravatarBy Michael Snoyman

I'm happy to announce the first release of Persistent, and a new release of Yesod adding basic Persistent support.

+

This release of Persistent includes backends for sqlite and postgresql. The former has no external library dependencies, as it includes the SQLite C library in the distribution. The latter is built on top of HDBC-postgresql. Please see the documentation site for more details.

+

Yesod 0.3.0 adds some rudimentary support for Persistent, most interestingly in the Yesod.Helpers.Crud module. With this addition, Yesod is now what I'd consider a "full featured web framework", providing Model, View and Controller functionality. However, not all features have been finalized. In particular, the Yesod.Formable module is highly subject to change, and will be merged together with the Yesod.Form module in upcoming releases.

+

A fairly complete changelog for this release of Yesod:

+
  • Added the isAuthorized function to the Yesod typeclass for easing and centralizing authorization.
  • +
  • Migration to Hamlet 0.3.1, which offers huge performance gains by being built on top of BlazeHtml.
  • +
  • Migration to web-routes-quasi 0.4.0, allowing much more powerful subsites to be constructed.
  • +
  • Yesod.Formable as a rudimentary formlets library. See below for where this is heading.
  • +
  • ContentType is now simply a type synonym for String.
  • +
  • Removed dependency on the convertible-text library.
  • +
  • Converted the Handler monad to be a monad transformer stack using standard monads from the transformers library.
  • +
  • Fixed a very stupid performance bug, result in a 10x speedup in the BigTable benchmark.
  • +
+

Future plans

+

I wanted to get this release of Yesod out quickly so that people could start playing around with Persistent immediately. However, as noted, some aspects are as yet unpolished. That's because I'm intending on adding a very significant feature in the next release: widgets.

+

Let's give a very simple motivating example: web forms. Let's say you want a form that provides client-side validation and shows your error messages in red. Currently, you need to write your Yesod code to generate HTML, write Javascript separately, write the CSS separately, and explicitly link in the JS and CSS files.

+

The widget concept is an attempt to combine all of that into a single place, within your Yesod code. For example, you'll be able to specify some Javascript validation code, and specify dependencies for that Javascript, such as jQuery. If you have other Javascript code on your page that depends on jQuery, jQuery will only be included once.

+

This will hopefully encourage the creation of composable web components. This looks to me to be a very exciting direction for Yesod to take, and I look forward to sharing my progress on it in the future.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/formlets-meet-hamlet.html b/public/blog/2010/06/formlets-meet-hamlet.html new file mode 100644 index 00000000..9278fe0d --- /dev/null +++ b/public/blog/2010/06/formlets-meet-hamlet.html @@ -0,0 +1,51 @@ + Formlets meet Hamlet +

Formlets meet Hamlet

June 4, 2010

GravatarBy Michael Snoyman

I just scratched a whole bunch of itches at once. I've been wanting to integrate formlets with Yesod, I've wanted Django-style generics, and somehow combining form generation with model definitions.

+

Well, the generics could be a little more generic, and I'm not sold 100% on formlets, but there's some definite progress. I also want to discuss some future directions for Hamlet.

+

Type-based form definitions

+

A major driving force behind the persistent package is having the types define a lot of characteristics. For example: in Django, you would define a string field and then specify that you want it to be non-empty (non-null? I don't remember their terminology). I believe the equivalent functionality for persistent should be through types. So, we would have a newtype NonEmptyString = NonEmptyString String. Then, the Persistable instance can do any validation that it wants.

+

By having all of these specifications in the type, we can easily extend this to deal with fields. Let's have a typeclass:

+
class Fieldable a where
+    fieldable :: String -> Formlet (Hamlet url IO ()) m a
+

Then when we define the NonEmptyString instance, we have it verify that strings aren't empty. (If you're wondering, that first String argument is simply for defining the label of the field.) Then we'll have another class:

+
class Formable a where
+    formable :: Formlet (Hamlet url IO ()) m a
+

And we can automatically generate Formable instances (via template haskell) for any datatype that is fully made up of Fieldable instances.

+

Taking it a step further: if we also have a Persist instance for this datatype, then we can automatically create, read, update and delete these types. By having a Fieldable instance, we can ask the user for input on the data. And voila! we have a generic CRUD system.

+

The code in question is available on github. I'd like to eventually turn CRUD into its own subsite, but I believe the subsite system will need a little more work to make this happen. Looks like a good project for Yesod 0.3.0.

+

Access Control Lists

+

In the project I'm contracting on right now, I'm going to need to implement permissions management. I'm considering a very simple extension to yesod to allow this:

+
data PermissionResult = Permitted | RequiredAuthentication | NotAuthorized String
+
+-- in the Yesod typeclass
+authenticateRoute :: y -> Maybe (Routes y)
+authenticateRoute _ = Nothing
+isPermitted :: Routes y -> Handler y PermissionResult
+isPermitted _ = return Permitted
+

What's really cool about this approach is:

+
  • You can completely separate the authorization logic from the rest of the application.
  • +
  • You can't accidently forget to specify the settings for a route.
  • +
  • Before showing the user a link, you can call isPermitted yourself and decide if they should see it.
  • +
+ +

Hamlet modifications

+

The first one I'm considering is migrating away from the text package. I think it's massively slowing down hamlet (based on my bigtable benchmarks). Instead, I think Hamlet will work on UTF8-encoded bytestrings. Additionally, we can do a lot of the UTF8-encoding of strings at compile-time instead of runtime, hopefully speeding things up more. And if someone really wants a different encoding, they can re-encode the bytestrings.

+

I'm also considering removing interleaved IO support. There's three main reasons for this:

+
  • Most reasons for interleaved IO are not a good idea anyway. For example, it's probably best not to interleave database calls, as that will hog a database connection longer than is necessary.
  • +
  • It adds a lot of complexity to the system. Based on some profiling, I believe the enumerators in particular are giving serious slow-downs versus pure, lazy lists.
  • +
  • The Hamlet datatype could be greatly simplified from a Monad to... well, maybe all the way to (url -> String) -> Lazy.ByteString. I'd probably keep a newtype wrapper, and allow some instances like Monoid.
  • +
+

Please let me know if anyone is relying on any of the Hamlet features I'm considering removing.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/fringe-benefits-typesafe-urls.html b/public/blog/2010/06/fringe-benefits-typesafe-urls.html new file mode 100644 index 00000000..df5ea1a0 --- /dev/null +++ b/public/blog/2010/06/fringe-benefits-typesafe-urls.html @@ -0,0 +1,46 @@ + Fringe benefits of type-safe URLs +

Fringe benefits of type-safe URLs

June 24, 2010

GravatarBy Michael Snoyman

I speak a lot about how awesome type-safe URLs are. They give you a huge level of confidence in the validity of all your links, let you change your URL structure at will and let you pass type-safe parameters to your handler functions. That's all well and good, but let's talk about some of the minor benefits of these things. Consider this the first in a mini-series focusing on some of the lesser known features of Yesod.

+

Breadcrumbs

+

Let me describe a project from hell. Imagine a site that has a navigational structure sometimes 8 levels deep. Imagine a client that can't write a spec, but knows exactly what he wants. And imagine that the "exactly what he wants" changes on a daily basis.

+

Breadcrumbs was a major annoyance on this project. Let's say we suddenly need to move one branch of the sitemap somewhere else. Using a normal, manual breadcrumbs approach, you have to change every single child page in the hierarchy to reflect things. Let's say the client decides you need to suddenly rename the root of the tree: you have to change every single page.

+

But if you think about it, breadcrumbs should really be very simple. For any page on your site, you need to know 1) the title of the page and 2) the parent page, if any. Turns out that type-safe URLs make this really easy to express as:

+
breadcrumb :: url -> (String, Maybe url)
+

If this is a top-level page, return Nothing in the second part of the tuple. Otherwise, return the parent. The first part in the tuple is the page title. I went ahead and packaged this into a typeclass, made the type signature a little more complex to run inside the Handler monad, and you have:

+
class YesodBreadcrumbs y where
+    breadcrumb :: Routes y -> Handler y (String, Maybe (Routes y))
+breadcrumbs :: YesodBreadcrumbs y => Handler y (String, [(Routes y, String)])
+

The breadcrumbs function returns the title of the current page and the chain of parents along with their titles. You can easily turn that into some HTML using Hamlet:

+
1 (title, parents) <- breadcrumbs
+2 return [$hamlet|
+3 #breadcrumbs
+4    $forall parents parent
+5        %a!href=@fst.parent@ $string.snd.parent$
+6        &gt; $
+7    $string.title$
+8 |]
+

Just to point out some of the Hamlet syntax: the trailing dollar sign on line 6 is simply to demarcate the extra space at the end of the line; Hamlet ignores it. On line 5, the at signs denote a type-safe URL, whereas the dollar signs represent HTML content. string is a function from BlazeHtml to convert a String into the Html datatype.

Static files host

+

As fast as Yesod is, it's not as fast as a static file server. It makes a lot of sense to server the static content from a different server (not to mention you avoid the cookie-transport cost). However, when developing the application, we want to be able to simply use the simple server and not set up a dedicated static file server.

+

That's where urlRenderOverride comes into play. Given a type-safe URL, it might return a replacement absolute URL. urlRenderOverride is part of the Yesod typeclass, where we keep a lot of optional settings. By default, the function always returns Nothing, which allows normal URL rendering to take place. However, you can set up your production code to intercept all of your static file requests and send them to a different server.

+

For an example of how to do static file server, see the Ajax tutorial.

+

One of the more recent additions to the Yesod typeclass is the isAuthorized function. Unfortunately, it was not done properly in 0.3.0, but I intend to fix that in 0.4.0. In any event, here's the idea: Given a type-safe URL, it returns Nothing if the request is authorized, or Just with a message if unauthorized. If you want the user to authenticate first, or provide some other information, you can use a redirect to send them to a different page.

+

The problem is that I wrote this to live in the IO monad instead of the Handler monad, so it doesn't have access to request information or the ability to send a redirect. Once this is fixed, it should provide a very light-weight access-control-list mechanism.

+

Conclusion

+

All three of these methods offer two serious benefits from a program safety standpoint:

+
  • It puts all of the code for a specific concept into a single location.
  • +
  • The compiler can check to make sure you've considered all possibilities.
  • +
+

I'm sure a lot of this could be implemented in a different way than type-safe URLs, but I think that we see here a natural result of a good design decision.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/hamlet-and-persistent.html b/public/blog/2010/06/hamlet-and-persistent.html new file mode 100644 index 00000000..785206b4 --- /dev/null +++ b/public/blog/2010/06/hamlet-and-persistent.html @@ -0,0 +1,18 @@ + Hamlet 0.3.0 and Persistent documentation +

Hamlet 0.3.0 and Persistent documentation

June 21, 2010

GravatarBy Michael Snoyman

Just two quick announcements: Hamlet 0.3.0 (and 0.3.1) have been released. 0.3.0 switches over to using BlazeHtml as its backend, resulting in much better performance. It does so at the cost of the monadic interleaving abilities of previous versions. Version 0.3.1 simply adds a "trailing dollar sign" feature: see the discussions and the documentation (bottom of page).

+

Also, I've just written the documentation for Persistent. I think the API is pretty stable at this point, and frankly the Postgresql backend is probably ready for a release. However, I'm holding off a little bit as I'm trying to solve an issue affecting the sqlite backend.

+

And once that's done, I think I'll be releasing Yesod 0.3.0. This release will most likely not be quite as polished as previous releases: there's currently a confusing redundancy in the API (both Yesod.Form and Yesod.Formable) which I'll most likely leave in for the moment. The reason is that I have plans for a widget system for the following release which will supercede these issues. Stay tuned for more details.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/handler-monad.html b/public/blog/2010/06/handler-monad.html new file mode 100644 index 00000000..18c40b0b --- /dev/null +++ b/public/blog/2010/06/handler-monad.html @@ -0,0 +1,84 @@ + The Handler Monad +

The Handler Monad

June 15, 2010

GravatarBy Michael Snoyman

Quick Status Update

+

For those of you wondering what's happening with Yesod: there's two major changes in store for the 0.3.0 release. Firstly, Hamlet 0.3.0 is going to be released, which will now be built on top of BlazeHtml. The performance of this combination is awesome. Also, persistent is currently fully functional, if a little young.

+

Topic at hand

+

Instead of giving more high-level views of what's going on in the Yesod ecosystem, I thought I'd show some of the more low-level work that's going on, and hopefully give some insight into how Yesod works. This time, I'll be addressing the Handler monad.

+

As you might guess, the Handler function is how you write handlers. You might use it like so:

+
getHome :: Handler MySite RepHtml
+

MySite here is your site argument, which is really the center of any Yesod application. RepHtml simply wraps some Content (a discussion for another time) so that it gets a text/html content type. And getHome is the function name: it's what gets called when a user requests the Home URL with the GET request method.

+

But what is a Handler?

+

Firstly, it's actually a specification of a GHandler. Here, the G stands for General or Generic. In particular:

+
type Handler y = GHandler y y
+

So what's this GHandler about? Remember that Yesod has a really cool, not-quite-polished feature called subsites. A subsite has its own argument type and its own set of routes. So when writing subsite handler code, you'll sometimes need access to a function to render subsite URLs to Strings, or sometimes you'll want access to the default layout of the master site. However, a normal Yesod application only has one site argument, which doubles as the master and subsite. Thus the type synonym.

+

Fine, what's a GHandler

+

Glad you asked. I'm going to present three virtually identical versions of a GHandler. Let's start off by defining what we want this monad to do:

+
  • Clearly, it must be a transformer on top of IO (you do want to be able to use a database, right?)
  • +
  • You need to be able to read various status information, like the request information (GET params, cookies, etc), the master and subsite arguments, and so on.
  • +
  • It needs to allow you to write out headers. I won't get into the details of how this works, but suffice it to say we have a Header datatype.
  • +
  • You'll need to write modifications to the user session. In particular, you need to be able to set a clear session variables.
  • +
  • And finally, you need to be able to short-circuit for certain exceptional responses. Exceptional here does not just mean error: sending static files and redirecting are both exceptional as well.
+

In Yesod 0.2.0, I wrote a custom monad without using anything from transformers. I manually declare all the correct instances. This worked out just fine, but I really do prefer when possible to use pre-defined monads. One reason is that it gives you automatic access to some really awesome libraries.

+

However, there is (in my opinion) a huge wart with both transformers and mtl: the Error monad. They each declare an orphan instance for Either, and don't even get me started on the Error class. That orphan instance can really bite you sometimes: you import a library that uses mtl and you're using transformers, and now your code won't compile.

+

So instead, here's the GHandler definition from Yesod 0.2.0:

+
newtype GHandler sub master a = Handler {
+    unHandler :: HandlerData sub master
+              -> IO ([Header], [(String, Maybe String)], HandlerContents a)
+}
+

HandlerData contains the read-only data we want access to, that second bit in the return tuple is modifications to the user session, and HandlerContents is a fairly trivial datatype:

+
data HandlerContents a =
+      HCContent a
+    | HCError ErrorResponse
+    | HCSendFile ContentType FilePath
+    | HCRedirect RedirectType String
+

If you want to, go ahead and define all the monad, application, etc instances for it.

+

The mother of all monads

+

I'm sure many of you have heard the claim that the continuation monad is the mother of all monads. This may be true (or may not, I don't really care), but it does give us some nice control structures. Such as an alternative to the Error monad. I've also seen lots of claims that it's significantly faster, though my tests haven't shown that yet. Best of all: we can use the standard definition from transformers without any orphans!

+

When building a complicated transformer stack, I like to write it out the long way first to make sure we have the right behavior. In this case, I want to make sure that the headers and session updates up until a short-circuit call (such as a redirect) are retained. So let's have a look at a second implementation of GHandler:

+
newtype GHandler sub master a = Handler {
+     unHandler :: HandlerData sub master
+               -> (a -> IO Helper)
+               -> IO Helper
+}
+
+type Helper = ([Header], [(String, Maybe String)], HandlerContents)
+

You can take a look at the code for the above-linked commit to see exactly how the instances work, but there's nothing too special going on here: it's just a combination of a Reader and a Cont.

+

Migrating to transformers

+

Now that we know what the result should be under the hood, let's look at the high-level approach we get to take. I'm also switching from straight lists for the writer to list endomorphisms ([a] versus [a] -> [a]). This is more efficient for appending, which happens to be the activity we do most often.

+
type GHandler sub master =
+    ReaderT (HandlerData sub master) (
+    ContT HandlerContents (
+    WriterT (Endo [Header]) (
+    WriterT (Endo [(String, Maybe String)]) (
+    IO
+    ))))
+ 
+type Endo a = a -> a
+

That's all there is to it! Let's see an example of how we short-circuit a send file request:

+
sendFile :: ContentType -> FilePath -> GHandler sub master a
+sendFile ct fp = lift $ ContT $ const $ return $ HCSendFile ct fp
+

For those of you unfamiliar with how to use a Cont monad (like me 24 hours ago), the const in there is ignoring the continuation argument. That argument tells you how to finish the computation. However, the whole point of short-circuiting is that we're going to finish it our way, so we drop that argument. The return is necessary since we're dealing with a ContT transformer over another monad, and the lift deals with the fact that our ContT monad is wrapped in a ReaderT.

+

Setting a session variable is also trivial:

+
setSession :: String -- ^ key
+            -> String -- ^ value
+            -> GHandler sub master ()
+setSession k v = lift . lift . lift . tell $ (:) (k, Just v)
+

That's a lot of lifts, but otherwise pure simplicity.

+

Results

+

I didn't measure any speedups from this conversion, but I also didn't try very hard to benchmark properly. I actually found a much more major performance bug that was causing a huge slowdown in the bigtable benchmark (a poorly written buffer function), so any gains from this wouldn't have shown up. However, the really nice feature is that we get to use functionality built for the transformers library for free.

+

The biggest example of this is the MonadCatchIO package. I'm using it extensively in the Sqlite backend for persistent to deal with exceptions, and now we have a properly tested instance of MonadCatchIO for our Handler monad.

+

Coming up

+

I'll probably put up a post soon describing the Content datatype and the HasReps typeclass, which define Yesod's output system. Stay tuned!

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/optimizing-hamlet-minor-correction.html b/public/blog/2010/06/optimizing-hamlet-minor-correction.html new file mode 100644 index 00000000..5e117b10 --- /dev/null +++ b/public/blog/2010/06/optimizing-hamlet-minor-correction.html @@ -0,0 +1,22 @@ + Optimizing Hamlet (a minor correction) +

Optimizing Hamlet (a minor correction)

June 7, 2010

GravatarBy Michael Snoyman

Note: this is in relation to my previous blog post, which showed a steep speed advantage for Hamlet versus Blaze on the BigTable benchmark.

+

Jasper pointed out that the benchmark I was using introduced unnecessary overhead by converting the final lazy bytestring to a String, when simply taking the length would have been enough strictness. I re-ran the benchmark that way, which gave the advantage back to Blaze over Hamlet 0.3.

+

I decided to run one more benchmark: the CGI benchmark. In this one, Blaze also has an advantage over Hamlet, this time of about 10%. I don't have time at the moment to do an in-depth investigation of this, but here is my theory on this strange behavior:

+

Hamlet currently performs no bytestring concatenation. As a result, it produces a lot of small bytestrings. These are very ammenable to being converted to Strings and being concatenated together. However, it will take longer to sum up the lengths of all the bytestrings, since length of a strict bytestring is O(1). Blaze, on the other hand, does concatenate bytestrings together, therefore giving it the exact opposite characteristics.

+

In practice, clearly the behavior of Blaze will be preferable. Given that Jasper is doing such hard work on optimizing Blaze, it's unlikely that Hamlet will ever get those specific attributes of its backend to be as fast as Blaze is. On the other hand, Hamlet still seems to have an edge over Blaze by being able to perform compile-time character encoding, and at least in theory concatenating large blocks of static HTML into a single string. The BigTable benchmark, however, utilized neither of these features.

+

Jasper had mentioned to me the possibility of using Blaze as a backend for Hamlet before, but I thought it didn't make much sense, given the heavy compile-time nature of Hamlet. However, the benchmarks are beginning to prove me wrong. Most likely, the 0.3 release of Hamlet will not be based on Blaze (especially since it's not yet released), but if the numbers indicate gains by using Blaze as a backend, most likely a later release of Hamlet will do so.

+

Thanks to Jasper for the great work!

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/optimizing-hamlet.html b/public/blog/2010/06/optimizing-hamlet.html new file mode 100644 index 00000000..2481e053 --- /dev/null +++ b/public/blog/2010/06/optimizing-hamlet.html @@ -0,0 +1,42 @@ + Optimizing Hamlet +

Optimizing Hamlet

June 6, 2010

GravatarBy Michael Snoyman

Overview

+

You can find the original benchmarks by Jeremy Shaw for Blaze and Hsp. The Hamlet-3 benchmark is now part of the Hamlet repository. The final results are as follows:

+Haskell Templating Systems BigTable Benchmark +

Introduction

+

It seems like the new cool thing in the Haskell web ecosystem is benchmarking: we all want to use the fastest libraries out there. Personally, I'm much more interested in type-safe, concise code than having the fastest library... but sometimes it's fun to optimize things.

+

When I ran the last set of bigtable benchmarks, I was trying to determine the best backends for serving Yesod applications. However, I stumbled upon some strange performance results: namely, the text package was slower than either bytestrings or plain Strings by a significant margin. Given that Hamlet is based on the text package, this worried me a little bit. After some profiling, I noticed that there was also significant overhead from the enumerator interface.

+

So when Jeremy Shaw posted some Criterion-based benchmarking code for Blaze and HSP, I was curious to see how Hamlet stacked up. The first set of results (based on Hamlet 0.2.3.1) were encouraging: Blaze took 105ms while Hamlet took 121ms. Hsp meanwhile pulled in at 140ms. So I was already in the ballpark of Blaze, and beating Hsp.

+

However, I wasn't very happy with this result: Hamlet does most of its work at compile time. It's able to concatenate adjacent pieces of static text into a single string. So I began working on Hamlet 0.3, and in particular 2 optimizations.

+

Replace text with UTF-8 bytestrings

+

How many of you still write web applications with a non-UTF8 encoding? Not many I suppose. The text package, however, uses UTF-16 internally. Thus, there's a lot of extra encoding going on when using the text package as the backend.

+

Swapping in bytestrings allowed me to completely avoid runtime-encoding of static text, and ensure that dynamic text is encoded only once. We now have 3 basic functions for creating Hamlet values:

+
output :: ByteString -> Hamlet ...
+outputOctets :: String -> Hamlet ...
+outputString :: String -> Hamlet ...
+

I put an ellipsis at the end there because the type of Hamlet is going to change throughout this post. The important thing here is the difference between outputOctets and outputString: the former converts the String to a ByteString via Data.ByteString.Char8.pack. In other words, it performs no UTF-8 encoding. The Hamlet quasi-quoter UTF-8 encodes all compile-time strings and then generates code which calls outputOctets. outputString, on the other hand, does perform UTF-8 encoding, and is used for runtime-generated strings.

+

This optimization showed great results: the bigtable benchmark dropped from 120ms to 70ms.

+

Remove monadic interleaving

+

Hamlet was originally designed to support monadic action interleaving. I thought this would be a great feature: you could interleave file access with template execution, pull in a database result via an enumerator, and then have an enumerator interface for generating template results.

+

After writing a hell of a lot of Hamlet-based code, I have not used this feature once. I'm not aware of anyone using this feature. My profiling also indicated that this was another source of a performance hit. So I decided to remove it entirely.

+

The first step was to redefine the Hamlet datatype. Previously, it was a newtype wrapper around an enumerator interface. Now, it's much simpler: newtype Hamlet url = Hamlet ((url -> String) -> [ByteString] -> [ByteString]). The first argument in there is the URL render function, and then we return a bytestring-list endomorphism. This allows very efficiecient appending of Hamlets. The main typeclass instance we use is the Monoid one:

+
instance Monoid Hamlet where
+    mempty = Hamlet $ const id
+    mappend (Hamlet x) (Hamlet y) = Hamlet $ \r -> (x r) . (y r)
+

The internals of the quasi-quote also became much simpler. Another advantage I had not anticipated is much friendlier error messages.

+

Final result: bigtable runs in only 40ms. This is a 66% improvement upon Hamlet 0.2, and a 60% advantage over BlazeHtml.

+

Conclusion

+

The code for Hamlet 0.3 is available on Github, but not yet released. Since it introduces breaking changes, it requires a major version jump for dependent packages, in particular Yesod. I'm planning on doing some more testing, perhaps a little more profiling, and releasing it fairly soon. The Yesod 0.3 release will most likely wait for a release of persistent, which should also come out soon. I'm in the process now of integrating forms and generics with persistent values; hopefully we can have a first release within the next two weeks.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/restful-content.html b/public/blog/2010/06/restful-content.html new file mode 100644 index 00000000..19261a9f --- /dev/null +++ b/public/blog/2010/06/restful-content.html @@ -0,0 +1,53 @@ + RESTful Content +

RESTful Content

June 18, 2010

GravatarBy Michael Snoyman

I've said many times that Yesod is based on RESTful principles. One example is the 1 resource == 1 URL design. Another is multiple representations. In my last post I described the Handler monad; here I hope to explain why the return type of handler functions usually looks like Handler MyApp RepHtml.

+

Files and Enumerators

+

Yesod is built on top of WAI, so we need to look down at that level a bit to get an understanding of what's going on. WAI is designed for performance, and in particular offers two ways of giving a response body:

+
  • A file path. This allows the web server to use a sendfile system call if it so desires. It can be a massive performance win since the data doesn't need to be copied at all.
  • +
  • An enumerator. Enumerators are simulatenously the cool new kid on the block, not well understood, and completely non-standard. I'm guessing there's easily a dozen enumerator definitions floating around. WAI uses one of the simplest definitions around. However, I won't really be discussing that design in this post.
  • +
+

Yesod therefore also allows both files and enumerators for the output; this is the Content data type. Yesod also has a ToContent typeclass (as of 0.3.0; it used to just be a more general ConvertSuccess) for converting the "usual suspects" like lazy bytestrings or text into Content.

+

Representations

+

A representation of data then really consists of two pieces of information: the Content and the mime-type. In Yesod 0.3.0, we use a simple String to represent mime-type: type ContentType = String. So how do we allow multiple representations? Let's start off with the simplest approach: [(ContentType, Content)]. Seems perfect: if a handler could return either HTML or JSON content, it would return something like:

+
return
+    [ ("text/html", toContent "<p>Hi there!</p>")
+    , ("application/json", toContent "{\"msg\":\"Hi there!\"}")
+    ]
+

So how would Yesod know which representation to serve? RESTfully of course! We parse the Accept HTTP request header, determine the prioritized list of expected mime-types, and then select the appropriate representation based on that list. If none of our representations match that list, we just serve the first one.

+

ChooseRep and RepHtml

+

This is all well and good, and earlier versions of Yesod worked this way. However, you end up losing type information: I can't look at the return type of a handler and know what type of content it has. So instead, let's look at this approach:

+
type ChooseRep = [ContentType] -> IO (ContentType, Content)
+class HasReps a where
+   chooseRep :: a -> ChooseRep
+

This first thing to notice is that ChooseRep is more powerful than our simple list. It's able to perform IO actions to produce the appropriate representation. This is very useful: perhaps we showing the HTML representation of data, you need to do some expensive database lookups, whereas the JSON version doesn't need that data. You can make sure you only run the IO operations when the user actually wants HTML.

+

The HasReps typeclass is the real winner here. It's trivial to now define instances of HasReps that specify which mime-type they return. Some real-life examples from Yesod:

+
newtype RepHtml = RepHtml Content
+instance HasReps RepHtml where
+    chooseRep (RepHtml c) _ = return (typeHtml, c)
+
+newtype RepJson = RepJson Content
+instance HasReps RepJson where
+    chooseRep (RepJson c) _ = return (typeJson, c)
+
+data RepHtmlJson = RepHtmlJson Content Content
+instance HasReps RepHtmlJson where
+    chooseRep (RepHtmlJson html json) = chooseRep
+        [ (typeHtml, html)
+        , (typeJson, json)
+        ]
+

Notice how that last datatype actually supports two different mime-types. You could create a type that supports XML as well if you like, or anything else. Yesod tries to only offer the most common types, so we've stuck with the HTML+JSON combination.

+

Coming up

+

In this mini-series on Yesod under-the-hood stuff, I think I'll attack user sessions next, and some of the built-in functions to help you (ab)use them properly.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/06/some-plumbing-fixes.html b/public/blog/2010/06/some-plumbing-fixes.html new file mode 100644 index 00000000..2a749a28 --- /dev/null +++ b/public/blog/2010/06/some-plumbing-fixes.html @@ -0,0 +1,49 @@ + Some Plumbing Fixes +

Some Plumbing Fixes

June 30, 2010

GravatarBy Michael Snoyman

As I've said, the main feature goal for the 0.4.0 release of Yesod is widgets. However, before I get started on that feature, there's a number of issues that I wanted to address. These are improvements that introduce breaking changes in the API, and so must wait for a major release.

+

persistent: split up PersistEntity

+

Version 0.0.0 of persistent had a typeclass called PersistEntity. Without getting into details, this typeclass essentially defined exactly how a datatype could be persisted to a particular database. Some advantages to this approach are:

+
  • You could have any datatype for the key that the backend required.
  • +
  • In theory, any backend could be written; it just required some template haskell code.
  • +
+

However, there are some major disadvantages to this approach:

+
  • It requires a large amount of template haskell code.
  • +
  • A datatype could only have a single PersistEntity instance, meaning it could only be persisted to a single backend.
  • +
  • Type signatures could become appallingly difficult.
  • +
+

So instead, persistent 0.1.0 will split up the work into two type classes: PersistEntity and PersistBackend. The former gives information on a datatype, such as what columns exist, how it can be sorted, unique keys, etc. It contains nothing backend specific. The latter typeclass is for each backend- like Sqlite and Postgresql. It is implemented without any TH code.

+

One downside is that a new backend must deal with the information provided by the PersistEntity typeclass. If it needs extra information that's not there, it won't work. I'm not so worried about this one, since I'm currently the only backend writer, and if someone else wants to write a backend that requires more information than is currently available in PersistEntity, I can just add it later.

+

The other downside is that database keys are all required to be Int64s. In practice, I don't think this is a problem at all: the only backend I know of that would really prefer a different key is Amazon SimpleDB, and even there I'm sure an Int64 would work.

+

web-routes-quasi: major overhaul

+

When Jeremy Shaw put together web-routes, I thought that web-routes-quasi would interact with it very well. Unfortunately, as the complexity of the package began to grow, I realized that it was really just becoming tailor-made for Yesod. This was a sub-optimal situation. And each new release of web-routes-quasi just added more Yesod-specific features.

+

This release (0.5.0) will be different. It no longer defines a QuasiSite datatype; we reuse the Site datatype from web-routes. It also cleanly exposes different TH functions for the four relevant declarations it provides for: creating the type-safe URL datatype, the parse function, the render function and the dispatch function.

+

The main reason I decided to start on this rewrite is to allow the dispatch function to return values still inside the Handler monad. Previously, web-routes-quasi would unwrap that monad itself, using a provided explode function. The plus was this provided a nice way to deal with the difference in type signature to subsite handlers (GHandler sub master) and master site handlers (GHandler master master); unfortunately, it made it difficult to provide features like authorization.

+

So now web-routes-quasi simply promotes the subsite handler functions into master site handler function. It needs three pieces of information to do this:

+
  • The master site argument to subsite argument conversion function.
  • +
  • The subsite route to master site route constructor.
  • +
  • The subsite route that was created.
  • +
+

The first of these is provided by the user when declaring routes; the last two are handled automatically by web-routes-quasi.

+

yesod: No more ContT

+

It seems now that migrating over to ContT for the Handler monad was a mistake. In particular, it has a broken MonadCatchIO instance, which leads to a number of annoying bugs (seg faults in sqlite because it double-frees memory). Of course, I still hate the ErrorT transformer, both for the super-class requirement and the orphan Monad instance.

+

So I've created a new package: neither. I originally thought of the name as a joke; in fact, my wife (a Greek/Latin major) helped me come up with a great datatype:

+
data Neither a b = Sinister a | Dexter b
+

However, it now provides three useful datatypes: AEither for an applicative-style either type, which combines Lefts via mappend; MEither for a monad instance; and MEitherT for a monad transformer. And you can use them all without any orphan instances. It also provides both mtl and transformers instances of MonadIO, MonadTrans and MonadCatchIO.

+

Also, as a result of really hideous error messages, I've switched GHandler to be a newtype instead of a type.

+

yesod TODO: merge Yesod.Form and Yesod.Formable

+

These two modules currently provide related functionality: the former is for getting GET and POST parameters without generating HTML forms. The latter is for generating HTML forms and automated parsing of POST parameters. It shouldn't be too difficult to combine the two together.

+

Other changes

+

I've added Facebook support to the authenticate repo, and passed that support up to the Yesod.Helpers.Auth module. It keeps track of the access token for you, so you can easily make requests against the Facebook graph API.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/07/database-migrations.html b/public/blog/2010/07/database-migrations.html new file mode 100644 index 00000000..997c4841 --- /dev/null +++ b/public/blog/2010/07/database-migrations.html @@ -0,0 +1,47 @@ + Recent update: database migrations and more +

Recent update: database migrations and more

July 27, 2010

GravatarBy Michael Snoyman

I usually blog much less about Yesod when the changes going on aren't near finalization. But I wanted to get out at least one post letting people know what's in the works. Just remember: we're early in the cycle for the next release, so a lot of these features are really wild guesses.

+

Database migrations

+

Thanks to some motivation from Greg Weber, I've finally gotten around to working on database migrations. The code is currently only functional for PostgreSQL, but it's already being heavily battle tested: my main project relies very heavily on this feature to deal with the ever-changing specs that are thrown at me.

+

What I think is a relatively unique feature in persistent is database introspection. The migration code looks at your table structure, including default values, NULL/NOT NULL and type, and compares that to your entity definitions. In addition, some pretty cool features:

+
  • There's a separation between "find the differences" and "apply changes." This allows you do store the migration information in a separate file, or use some external tool (such as dbmigrations) to manage the actual migrations.
  • +
  • There are two different functions for applying the changes: the "safe" version never drops columns, whereas the unsafe one does. If you use the safe version (I do) you'll have to manually drop columns.
  • +
+

migrate will replace the initialize function in persistent 0.2. I still have to implement the code for Sqlite. Unfortunately, sqlite does not provide the same level of introspection and alteration abilities that postgresql does. Most likely, I will need to parse the CREATE TABLE statements and copy table contents for any migration, but it should be manageable. And when it comes to schema-less databases like Redis, migrations shouldn't be necessary.

+

Improved select function

+

Rehno Lindeque pointed out that we were missing two important features in persistent: getting the number of records and limiting/offsetting a result set. For the former feature, I was hoping to write something more generic, allowing any aggregate functions. Unfortunately, I haven't come up with a type-safe approach to this yet, so we'll just have a count function for the moment. If anyone has a brilliant insight, let me know.

+

The select function is now a bit more complicated. In addition to a list of filters and a list of orders, it now also takes an offset and limit. If the limit is 0, then no limit is imposed.

+

Finally, select no longer returns a list of records. For this usage, use the selectList function. select now has an enumerator interface; you provide it with a seed value and an iteratee. I'm debating whether to use the iteratee package for this interface; input is welcome.

+

Foreign keys and deleteCascade

+

For postgresql, we now support foreign key references. persistent figures these out in one of two ways:

+
  • If you provide a references=tblName attribute, then we create such a reference to that table's primary key. If you provide a noreference attribute, then no reference is created.
  • +
  • If neither of the above attributes exist, and the data type for a field is SomeNameId, than a reference is created to the primary key of tblsomename.
  • +
+

This makes it exceedingly difficult to actually delete data from a database; having any references to data you try deleting will result in an exception. So I've also added a DeleteCascade typeclass and a TH helper function to automatically create "deep deletes".

+

Some forms cleanup

+

Back to Greg Weber, we've been working together on cleaning up the Yesod.Form module a bit. Here are some notable changes:

+
  • You can supply id=someId and name=someName attributes in the entity definition and mkToForm will set HTML attributes accordingly. This can be useful for creating separate CSS files.
  • +
  • We're making the locations of Javascript and CSS files customizable. Now there is a YesodJquery typeclass that has functions you can override. This makes it much easier to use a local copy of the jQuery code, or switch a jQuery UI theme. This change precipitated some major reworkings of the ToForm typeclass, in particular it is now a MPTC.
  • +
  • There are no longer special datatypes for javascript-enabled fields. For example, in order to create a jQuery UI date picker field, you would now specify in your entity definition day Day toFormField=YesodJquery.jqueryDayField. The YesodJquery prefix specifies that this function requires a YesodJquery instance.
  • +
+

nginx

+

I've added an nginx section to the deploying page of the Yesod docs site.

+

Questions for the community

+
  • What kind of changes would people like to the layout of the yesod docs site?
  • +
  • Do people prefer screencasts or text tutorials?
  • +
  • Do you have any Yesod projects that are not listed in the powered by Yesod section?
  • +
+

One final note: I'm in the process of some big changes over here (my wife's expecting any day now, and we're probably moving apartments in the next few weeks), and I need to make sure I don't slip too far behind in my day job. Don't be surprised if I suddenly stop answering my email for a few days; it's probably because I'm either in the hospital with a new baby or between apartments without internet access.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/07/first-stab-at-widgets.html b/public/blog/2010/07/first-stab-at-widgets.html new file mode 100644 index 00000000..58f3a74c --- /dev/null +++ b/public/blog/2010/07/first-stab-at-widgets.html @@ -0,0 +1,27 @@ + First Stab at Widgets +

First Stab at Widgets

July 1, 2010

GravatarBy Michael Snoyman

Having finished a bunch of minor maintenance on Yesod (more consistent API names, that kind of stuff), I've begun to focus in on the big new feature brewing for Yesod 0.4.0: widgets. I'm really not a big fan of designing awesome technology out in the middle of nowhere; instead, I like to think of real-world problems I want to solve, and then build the tools around it. So let's start with a motivating example: forms.

+

We all know how annoying it is to deal with web forms. Back in the good old days of raw CGI, you'd have to write your form HTML, write parse code separately, probably write another version of the HTML to create sticky fields, etc. We've come much farther than that, especially with libraries like formlet. However, there's still one piece of the puzzle which is tedious to set up: the Javascript and CSS.

+

For example, I want to have a day field. Of course, like any sane person, I use YYYY-MM-DD format. But I live in a country where the standard format is DD/MM/YYYY, and many of the user's of my sites live in a country where the standard is MM/DD/YYYY. So ideally I'd like to use Javascript to both do client-side checking and provide a nice date picker. I also would like error messages to be displayed in some shade of red.

+

So with Yesod 0.3.0 I would need to include the necessary Javascrip libraries in my template, include the datepicker CSS file in the template, write a local Javascript file to initiate the datepicker and the validation code, and write a local stylesheet for styling the error messages. Now that's tedious.

+

Instead, we're going to create composable widgets. They keep track of not just HTML to be placed in the body of a document, but also scripts and stylesheets to load, extra HTML to throw in the head, CSS for <style> tags, and the title of the page. It also should supply me with unique identifiers to use.

+

I've implemented this as a monad transformer stack; you can see the implementation on Github. The Yesod.Widget module also contains a bunch of functions for creating widgets. For example, addStylesheet adds a stylesheet, addBody adds some content to the body of the page, etc. There's also a cool function applyLayoutW which is able to display a widget with the default page layout.

+

Since a widget is just a monad stack, it composes very easily. There's also a Monoid instance to make life simpler. The trickiest part of it right now is when you want to wrap the body HTML code up somehow (for example, sticking everything in a div id=wrapper tag). For this, check out the wrapWidget and extractBody functions in the Yesod.Widget module.

+

Forms

+

The next big step is converting Yesod.Form over to use widgets. The original inspiration for this module is formlets; however, I'm veering farther and farther from the API, as my goals are quite different than the formlets API. As an example, formlets does not support inline error messages; all validation messages must be shown together. I don't find that an acceptable tradeoff.

+

The API is in flux right now, so I don't want to spend too much time talking about it. However, here's a high-level approach: there is a GForm monad which takes an "xml" type parameter. There are then two type synonyms: Form sets that xml type parameter to be a widget. This is what you'll usually want to embed in a page.

+

The other type synonym is FormField, which instead sets that xml type to FieldInfo. A FieldInfo contains information on the label, error messages, and various other things in a field. Of course, you can't display this directly; there will be various helper functions to convert a FieldInfo into a widget. The reason to split this up is so users have more control of how to layout their forms. For example, there's a function currently to display a FieldInfo as a row in a table; we could also provide a list and paragraph version.

+

And I still intend to have all this tie in with some typeclass for automatic creation of forms for PersistEntity instances. Just imagine: you could declare your entity structure once, and Yesod will create a database schema, generate SQL code, and create HTML forms for you automatically. I have some examples of this actually working with various versions of Yesod, and most likely the release of Yesod 0.4.0 will be accompanied by a screencast showing how to use all these features together. Most likely it will be the inevitable blog example ;).

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/07/sessions.html b/public/blog/2010/07/sessions.html new file mode 100644 index 00000000..573c9cee --- /dev/null +++ b/public/blog/2010/07/sessions.html @@ -0,0 +1,47 @@ + Sessions +

Sessions

July 6, 2010

GravatarBy Michael Snoyman

This post discusses the session API as present in Yesod 0.4. This version has not been released yet, but you can see the Haddocks online. The API is not much different from previous versions, but the names have changed a bit.

+

Sessions

+

Most web frameworks provide some notion of user sessions, where you can store small bits of information for each user. In the case of Yesod, we use the clientsession package (also written by yours truly) to store data in an HTTP cookie. While this might sound insecure and inefficient at first, it's not:

+
  • All data is hashed, encrypted and base64-encoded before being placed in the HTTP cookie.
  • +
  • You shouldn't be storing large amounts of data in the cookie, so the bandwidth overhead shouldn't be large.
  • +
  • Production sites should be serving their static data from a separate domain name, meaning the cookie bandwidth hit won't apply to them.
  • +
  • By storing the data in a client session, we avoid a database lookup for each request, which can be very expensive.
  • +
  • Currently, sessions are not compressed, though this is a feature easily added in the future if it turns out to be beneficial.
  • +
+

Like most frameworks, the sessions are simple key-value stores. There are three basic operations on a session:

+
  • lookupSession gets a value for a given key.
  • +
  • setSession sets a new value, or replaces an existing value.
  • +
  • deleteSession clears the value in a session, so that it does not appear in the next request.
  • +
+

Please note that the setSession and deleteSession functions do not affect the outcome of a call to lookupSession within a single request. In other words:

+
do
+    val1 <- lookupSession "key"
+    setSession "key" "newvalue"
+    val2 <- lookupSession "key" -- val1 == val2, not "newvalue"
+

Messages

+

Messages are built on top of sessions. The situation is a common one in web development: the user performs a POST request, you make a change, and want to simultaneously tell the user that the request suceeded and redirect them to a new page. Yesod provides a pair of functions to make this very easy:

+
  • setMessage stores a value in the session.
  • +
  • getMessage both reads the value most recently put into the session, and also clears that old value.
  • +
+

In my projects, I will often put a call to getMessage in the defaultLayout function and know that any messages I set with setMessage will be displayed on the next page the user views.

+

Ultimate Destination

+

Not to be confused with a horror film, this concept is used internally in the Yesod authentication module. Simply put, let's say a user requests a page that requires authentication. Clearly, you need to send them to the login page. A well-designed web app will then send them back to the first page they requested. That's what we call the ultimate destination.

+
  • setUltDest sets the ultimate destination to the given route.
  • +
  • setUltDest' is a variant of setUltDest that sets the ultimate destination to the current route.
  • +
  • setUltDestString is the same thing, but uses a string instead of a type-safe URL. Can be useful if you want to send a user to another site after authentication.
  • +
  • redirectUltDest sends the user to the ultimate destination currently in his/her session and clears that value from the session.
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/07/wai-0-2-ideas.html b/public/blog/2010/07/wai-0-2-ideas.html new file mode 100644 index 00000000..c3ae5c83 --- /dev/null +++ b/public/blog/2010/07/wai-0-2-ideas.html @@ -0,0 +1,61 @@ + Web Application Interface 0.2 Ideas +

Web Application Interface 0.2 Ideas

July 9, 2010

GravatarBy Michael Snoyman

There's a number of ideas I've had for fixing up the Web Application Interface, and finally decided to put them all together. I've created new branches for wai, wai-extra and yesod.

+

In no particular order, here are the items I've changed. I'm very happy to hear any critiques of these, so don't hold back.

+

Removed the buffer function

+

I wrote a buffer function which performs badly, and I want to remove it. Nuf said.

+

Less data types, more type synonyms

+

An obvious definition for HTTP request methods is:

+
data Method = GET | POST | PUT | DELETE
+

Obvious... except I forgot OPTIONS. No problem, I'll add it. And I forgot TRACE. OK, add that too. Oh wait: you can have arbitrary HTTP methods if you like. Doh!

+

WAI 0.0 and 0.1 therefore had an extra constructor Method B.ByteString, which allowed you to specify any request method you wanted. All seemed well and good to me, until Jeremy Shaw commented on an entirely different topic: "The use of a type ... implies that you want to be pattern matching on the constructors in your code".

+

And I realized he was absolutely right. It's a bad idea to create a data type with constructors that can't be pattern matched on, and that's exactly what I'd created. After all, Method "GET" == GET, so pattern matching is out of the question. Sure enough, I've received a few patches for WAI code that mistakenly attempts to pattern match. I'd created a horrible monster which thwarted all that is well and good in type-safe programming! </drama>

+

So instead, the definition is now simply type Method = B.ByteString. And all is good and right in the world.

+

This same change was applied to the other relevant datatypes in Network.Wai: HttpVersion, RequestHeader, ResponseHeader and Status.

+

Functions are the new constructors

+

I'm on the fence for this one, so please let me know what you think. I've replaced the GET constructor with a methodGET function. This is simply defined as methodGET = B8.pack "GET"; and the same is done for all other constructors.

+

But frankly, I think this just litters the namespace. I have not implemented these functions for RequestHeader and ResponseHeader, and I don't think I will. I'm fairly certain I dislike the functions for Method, but will probably keep the definitions for HttpVersion and Status.

+

Added CIByteString

+

Gregory Collins pointed out that request and response headers should be case-insensitive. As a result, WAI 0.1 made sure their Eq instance handled this properly. Now, due to the switch to type synonyms, we define a new datatype named CIByteString (which looks strangely familiar) and use that for RequestHeader and ResponseHeader.

+

RequestBody datatype

+

WAI 0.0/0.1 define a response body as Either FilePath Enumerator. We all know how wonderful enumerators are versus lazy I/O... but how good are they versus lazy evaluation. In other words, which is faster: an enumerator, or a purely created lazy bytestring? I don't have specific benchmarks, but my work on Hamlet indicated to me that lazy bytestrings win.

+

The vast majority of web applications will not often need an enumerator interface. Mostly, static files and lazy bytestrings will be sufficient. Hack has done a very good job with only providing a lazy bytestring interface. So I would like to add a lazy bytestring response body to WAI; not to replace enumerators, but to augment them. So I've created a new datatype:

+
data ResponseBody = ResponseFile FilePath
+                  | ResponseEnumerator Enumerator
+                  | ResponseLBS L.ByteString
+

This adds a bit of complexity to handler code, but I think it's worth it. I've also replaced the fromEitherFile function with fromResponseBody. As a bonus, I think it's nicer using a specific datatype than an Either.

+

What hasn't changed

+

There are three decisions in particular that I've decided to stick with, even though there's been some pressure to change this. I'd like to state these out loud to re-open the debate, because I want to make sure this is the right decision.

+

The first is fairly trivial: whether the Request datatype has a record for script name. Hack and the Hyena WAI both have it, but I've removed it for two reasons:

+
  1. It has no meaning for many backends, particularly any stand-alone server.
  2. +
  3. Backends where is does have meaning (CGI, FastCGI) do not provide reliable data on it. Whenever you use URL rewriting (which I hope you're using), it's nearly impossible to get a meaningful script name.
  4. +
+

The second issue is the Source datatype as a request body. People have said it should simply be an Enumerator. I have this to say:

+
  1. Any Source can be converted to an Enumerator without issue, but the reverse is not true.
  2. +
  3. A Source is easier to parse than an Enumerator.
  4. +
  5. A Source allows you to implement backtracking parsing of request bodies without reading the entire request body into memory.
  6. +
  7. I don't think there is significant difficulty placed on backend implementations to provide the more powerful Source type versus an Enumerator.
  8. +
+

The final issue is the definition of Enumerator. There's lots of work in the iteratee package, and it seems to be blazing the trail for this domain. However:

+
  1. WAI should be low-dependency, and introducing a dependency on the iteratee package would be too high a burden. (I realize this isn't a full argument, since I could just copy-paste the definitions from iteratee.)
  2. +
  3. The iteratee approach is still evolving, now migrating to a continuations-based approach. It seems too early to try and jump on that band-wagon; let's let the dust settle first.
  4. +
  5. Much of the complexity introduced by iteratee is totally unnecessary in WAI. For example, we can't have an arbitrary monad, we need IO. We can't have arbitrary chunk data types, we use strict bytestrings.

    +

    However, at a more fundamental level, iteratee is trying to solve problems WAI doesn't have. WAI just needs a way to allow efficient outputting of data with interleaved IO actions. It will always just send one chunk at a time, and either the entire chunk will succeed, or there will be a failure. iteratee deals with complex problems like partial input consumption.

    +

    In other words: I have not seen a single argument made why switching to iteratee is actually a win for WAI.

    +
  6. +
  7. And let's face it: enumerators are complicated beasts. People are afraid of them. If the Enumerator type in WAI is as scary as someone holding a knife to you, iteratee is a B2 Bomber.
  8. +
+

That said, I'm happy to hear any explanations and see some examples of why I'm making The Wrong Decision here. For that matter, please tell me if you think any of the changes I'm proposing for WAI are a bad idea.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/07/wai-handler-snap.html b/public/blog/2010/07/wai-handler-snap.html new file mode 100644 index 00000000..03149c58 --- /dev/null +++ b/public/blog/2010/07/wai-handler-snap.html @@ -0,0 +1,20 @@ + wai-handler-snap: Run Yesod apps on the Snap server +

wai-handler-snap: Run Yesod apps on the Snap server

July 18, 2010

GravatarBy Michael Snoyman

One area that WAI has been lacking in up until now is a production-quality standalone server for running your applications. We've had CGI and FastCGI when you want to pair up with lightppd or its ilk, and we've had simpleserver for testing purposes. We can now welcome a new member to this family: Snap.

+

A big thanks to Gregory Collins to making some modifications to Snap so that this shim would be possible. Everything should be installable from cabal without any issues. I've added a sample to the yesod-hello repository, but it should all be pretty straight-forward.

+

The code itself is very simple: the only slightly complicated part was converting a WAI enumerator to a Snap enumerator. Also, since a Snap enumerator cannot be converted directly to a Source, the request body uses lazy I/O under the surface.

+

Below are the obligatory benchmarks for a very simple Sqlite-powered blog. The example is simply loading up a page 1000 times with 20 concurrent connections. The numbers do not necessarily reflect any performance issues with Snap itself; most likely, the performance difference between FastCGI and Snap are due to either issues in the wai-handler-snap layer or, most likely, the runtime arguments I passed to the executable. I'd appreciate feedback on this from people in the know; the code is all available on github.

+

Yesod Blog Benchmark

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/07/yesod-0-4.html b/public/blog/2010/07/yesod-0-4.html new file mode 100644 index 00000000..0fc60f71 --- /dev/null +++ b/public/blog/2010/07/yesod-0-4.html @@ -0,0 +1,25 @@ + Yesod 0.4.0 Released +

Yesod 0.4.0 Released

July 13, 2010

GravatarBy Michael Snoyman

I'm very pleased to announce the release of Yesod 0.4.0. This release includes new major versions for WAI, Hamlet, persistent and web-routes-quasi, pulling in a large number of new features. As an introduction to this release, I've begun a multi-part screencast of a blog tutorial.

+

The major feature in this release is widgets. This allows you to create modular pieces of HTML/CSS/JavaScript and plug them together with ease. As an example, the widgets tutorial demonstrates having a jQuery UI datepicker and Nic HTML editor without explicitly calling any Javascript.

+

Other major changes: include a large architectural overhaul of persistent, which does not affect user code very much, but gives much more flexibility in using the library; an updated parser for Hamlet allowing some nice features like quoted attributes; a more sane subsite syntax hopefully allowing interoperation with other frameworks; and much more.

+

I keep the changelogs separate by project, you can see each of them here:

+ +

Unlike other releases, I don't have any major features planned next, so I'm very interested in what the community has to say at this point. I'm hoping to get a few more WAI backends working soon (waiting on some feedback from Snap before releasing a Snap-powered handler), but otherwise I'm open to suggestions.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/08/orange-roster.html b/public/blog/2010/08/orange-roster.html new file mode 100644 index 00000000..43763349 --- /dev/null +++ b/public/blog/2010/08/orange-roster.html @@ -0,0 +1,32 @@ + Orange Roster +

Orange Roster

August 20, 2010

GravatarBy Michael Snoyman

For the past few months, I've been writing a lot of code using Yesod. The problem is, this has all been on a proprietary project. Not only can I not open source the code for the site, I can't even show you the site. (I'm sure a little detective work would be enough to discover it, but even so you can't get past the homepage without an account.)

+

So this presents a little bit of a predicament for learning Yesod. There's a documentation site that has some screencasts, and I've started to work on writing a more detailed book, but sometimes you just need to see the code for a real live site to get a feel for things.

+

So I started putting together a site called Orange Roster, both to scratch a personal itch and to get out a real-life Yesod example project. The source code is available on Github. I know that I put the Facebook secret key in the repo; if anyone decides to abuse my Facebook application test account... well, you suck ;).

+

This project demonstrates the most recent "best practices" for laying out a large Yesod project, splitting up the code into multiple files, separating the Hamlet templates into separate files, and using Cassius and Julius. It depends on Yesod 0.5, which hasn't been released quite yet. This was in fact done on purpose: I'm using this project to smoke out the last few changes to be made to Yesod before releasing.

+

You don't really need to know what the point of the site is to benefit from the source code, but I'll give a brief description. I'm always irked that I need to log into Facebook to get someone's phone number. I actually keep a spreadsheet with a lot of people's phone numbers, but numbers change, and you don't always remember to put everyone's number in your address book/spreadsheet. All together, here are the annoyances:

+
  • I hate logging into Facebook. It's a privacy trap, changes its structure often enough that I always have trouble finding the information I want, and just in general isn't geared for this usage.
  • +
  • It's annoying having to keep information in two places: if I want to store people's phone numbers, I put it in my spreadsheet. If I want someone else's contact information, I look it up on Facebook.
  • +
  • And I really don't like spreadsheets that much anyway. That's probably a bit of post-traumatic stress from working in the insurance industry, but when I see those rows and columns staring at me I start getting images of horrible Visual Basic code manipulating cell formats.
  • +
+

So Orange Roster is a very simplistic website: you log in, create a profile, share your profile with whomever you want, and add entries to an address book. You share your profile via email addresses. If the user is in the system already, then you immediately show up in their "sharing" tab. Otherwise, they get an email invitation. And here's another great feature: sharing is unidirectional. I can share with you and you don't have to share with me.

+

The site is not done, and I'm not giving any guarantees on holding onto your data yet, but you can play around with it, experiment, and give me feedback. Here are some of the features I haven't yet implemented:

+
  • Edit/delete profile data after you've entered it.
  • +
  • Connecting a user to an address book entry.
  • +
  • Importing your list of friends from Facebook. This would obviously be an optional feature, I have no intention of invading privacy here.
  • +
+

As stated, Yesod 0.5 is almost ready for release. I'm playing around with a few last-minute changes, such as persistent using the newly released enumerator package.

+

PS: My appologies if the site runs a bit slowly, I'm running it on an (apparently) very low-end VPS. I'll probably be switching providers before bumping this site up to beta status.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/08/static-file-optimizations.html b/public/blog/2010/08/static-file-optimizations.html new file mode 100644 index 00000000..c2daa1be --- /dev/null +++ b/public/blog/2010/08/static-file-optimizations.html @@ -0,0 +1,39 @@ + Static File Optimizations +

Static File Optimizations

August 11, 2010

GravatarBy Michael Snoyman

Things are coming into place very nicely for the Yesod 0.5 release. I'd originally thought this release would basically just be to match the release of persistent 0.2; instead, there are some major features making their way in. I've already mentioned the changes in Hamlet; here I want to address some issues revolving around static files.

+

sendfile system call

+

Probably the most powerful optimization available for serving static files is the sendfile system call. The WAI has support for this feature, and Yesod inherits that. So if you want to send a file, you should always use sendFile.

+

One other thing: a number of web servers provide access to this optimization for (Fast)CGI scripts via an HTTP header. wai-handler-fastcgi 0.2.1 added support for this feature. And on a complete tangent, I'm testing wai-handler-fastcgi 0.2.2 which introduces multi-threaded support, so you don't need to run your app as multiple processes.

+

Separate domain

+

Another technique to speed up static files is serving from a cookie-less domain. The Yesod approach to this is to still create the route for serving static content with the static subsite, but then use the urlRenderOverride function to point this at a separate domain name in your production server.

+

The yesod scaffolding tool for Yesod 0.5 will generate a site template that does all of this for you automatically, so you don't need to guess at how to approach it. If you want to see it in action, there's an example on github.

+

Another advantage of this approach is that we can bypass the Yesod application entirely for serving these static files.

+

Type-safety anybody?

+

Let's take a little tangent- we'll get back to optimizations in a second, trust me. Have you ever been annoyed that you're hardcoding these strings in your source code? You know, path names: what if I mistyped it, what if I move the file, and so on. Yesod's got your back here too: staticFiles is a little bit of template haskell code that gets a list of all of your static files at compile time and creates identifiers for all of them.

+

Hashes

+

And here comes the awesome new feature that's holding up the Yesod 0.5 release: hashes. When the staticFiles function mentioned above gets the list of files in your static folder, it also gets a hash value for each file. When you render that identifier to a URL, it includes that hash value in the query string of the URL.

+

So here's a corny line: what rhymes with hashing? Caching. You can now set your expiration headers into the distant future with impunity. If the file changes, the hash will change, and therefore the URL will change, and therefore the browser will download a new version. There is no runtime penalty for this, since the hashes are all calculated at compile time.

+

And if you follow my advice and serve your static files from a separate domain name, you can simply have your server set an expiration header in the far future for that domain. In nginx, for example, I'm now able to use "header: max;".

+

Cassius and Julius

+

I had a little poll recently to determine the names for the CSS and Javascript templating languages to be including with Hamlet, and the winners were Cassius and Julius. Due to the cassiusFileDebug and juliusFileDebug functions, you no longer need to recompile your application to see changes to styles and javascript added to widgets. By using these template languages, you get to have variable interpolation and type-safe URLs in your CSS and JS as well. Seems like a great feature.

+

The downside is that styles and javascript in Yesod 0.4 get embedded in the head tag, when what we'd really like to do is have them stored separately so we can get the benefits of caching. In fact, if we could do some hashing as well, that would be amazing, right?

+

Yesod 0.5 is introducing the function addStaticContent; by default, the function does nothing. However, it can also be used to store contents of a file somewhere and return a URL from which the contents can be accessed. Yesod uses this function internally to reference CSS and JS code from a URL instead of embedding it.

+

The default site template will simply store the contents in a filename based on the hash of the contents; once again, if you follow the advice given above, these files will be served on a separate domain name without ever touching your Yesod application, with an expiration date far in the future, reaping maximum performance.

+

However, for sites that want to get even better performance, the sky's the limit. For example, you could write an addStaticContent function that stores the contents in a memcached database, or distributes the contents to a CDN.

+

Conclusion

+

I still think the major feature of the next release will be database migrations in persistent. However, the debug mode for Hamlet is also very compellling, as are these modifications to Yesod. The nice thing about this release is that it's introducing very few breaking changes, so it should be an easy migration from Yesod 0.4.

+

I was on a train for a long time this week, so I finally started a project I've been itching to do for a while: a Yesod book. It's going to be served on the Yesod documentation website, but will be going into much more depth than I do elsewhere. I assume my blog readers have ADD (no offense), and that people on the main docs site just want a quick example of how to get something done. The book, on the other hand, will try to approach both the why and the how.

+

Since I was on the train when writing, there are still a lot of typos, and it's very incomplete, but I'm hoping to do a bit of writing every day. Stay tuned.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/08/typesafe-runtime-hamlet.html b/public/blog/2010/08/typesafe-runtime-hamlet.html new file mode 100644 index 00000000..80a04761 --- /dev/null +++ b/public/blog/2010/08/typesafe-runtime-hamlet.html @@ -0,0 +1,92 @@ + Type-safe runtime Hamlet +

Type-safe runtime Hamlet

August 8, 2010

GravatarBy Michael Snoyman

Current Hamlet inclusion options

+

I recently released Hamlet version 0.4.2, which added an often-requested feature: runtime Hamlet templates. This allows some really cool possibilities, such as using Hamlet templates in Hakyll. There are now three different ways to include a Hamlet template in your code; let's look at the options and their relatives strengths and weaknesses.

+
  • Quasi-quoted
    • Advantages
      • Fully type-checked at compile time.
      • +
      • Changing the template automatically forces a recompile.
      • +
      • Does as much work as possible at compile-time; faster runtime execution.
      • +
    • +
    • Disadvantages
      • Some people like to keep their templates and code separate.
      • +
      • The only way to see the result of a template change is a recompile- slows down development cycle.
      • +
    • +
  • +
  • Template Haskell external file
    • Advantages
      • Fully type-checked at compile time.
      • +
      • Does as much work as possible at compile-time; faster runtime execution.
      • +
      • Keeps templates and code separate.
      • +
    • +
    • Disadvantages
      • Changing the template does not automatically forces a recompile; you might see stale content if you're not careful.
      • +
      • The only way to see the result of a template change is a recompile- slows down development cycle.
      • +
    • +
  • +
  • Runtime templates
    • Advantages
      • Immediately see results of template changes, without a recompile.
      • +
      • Keeps templates and code separate.
      • +
    • +
    • Disadvantages
      • Not type-checked at all.
      • +
      • Parse errors only appear at runtime.
      • +
    • +
  • +
+

In addition, it's very difficult to get runtime templates to interact well with the first two options. In the case of Yesod, there is basically no machinery in place to help you out; you'll have to write it all yourself.

+

The Fourth Option

+

However, Hamlet 0.5 is going to include a fourth method which will have the following characteristics:

+
  • Advantages
    • Fully type-checked at compile time.
    • +
    • Is code-wise identical to the external Template Haskell method.
    • +
    • You can view changes to your template without a recompile. If you have made changes the break the type-safety of your template, you will get an error message and be informed you must recompile.
    • +
    • You can use this method during testing and easily switch all of your code to the external TH version for production, thereby avoiding all runtime costs.
    • +
  • +
  • Disadvantages
    • Since you are avoiding a compilation step, sometimes you'll need to manually initiate a recompile.
    • +
    • Some of the more advanced Hamlet tricks available to quasi-quoted and TH templates are not available. This shouldn't affect most people.
    • +
  • +
+

Half runtime, half Template Haskell, and a little unsafe

+

Let's say that I have the simple Hamlet template Hello $name$. If we use either quasi-quoted or TH Hamlet, name will be converted into an Exp and will reference the variable in scope called name. If we use runtime Hamlet, we would need an appropriate HamletData value, something looking like HDMap [("name", HDHtml $ string name)].

+

Now let's say that we want to change that template to Hello $name$!. All I've done is added an exclamation point, so I know the type safety of the template has not been affected. Here are the results for the three inclusion options.

+
Quasi-quoted
+
Fully recompile the entire module containing the template.
+
Template Haskell
+
The change won't appear until you manually force the module to be recompiled.
+
Runtime
+
Change appears immediately without a recompile.
+
+

However, let's say that I change the template now to:

+
%ul
+    $forall names name
+        %li Hello $name$
+

I also immediately fix up my Haskell code so that instead of name = "Michael" I have names = words "Michael Miriam Eliezer Gavriella". Now, the results are:

+
Quasi-quoted
+
Fully recompile the entire module containing the template.
+
Template Haskell
+
Fully recompile the entire module containing the template.
+
Runtime
+
Code won't compile until you also modify the HamletData value to something like HDMap [("names", HDList $ map (HDHtml . string) names)].
+
+

So with options 1 and 2, we get an unnecessary recompile step in case 1. In case 2, we require Haskell code changes, so the recompile is unavoidable. However, it's much more tedious to make the code changes for option 3. Also, let's say that we made the Haskell code change (name to names) before the template changes. Options 1 and 2 would give us a compile-time error message, while option 3 would only complain at runtime.

+

What we really want is to have that HamletData value constructed for us by Hamlet, by reading the template file at compile time. However, we also want it to read the template file at runtime and apply the HamletData value to it. This is exactly what option 4 does.

+

How it works

+

There's a new function in Hamlet 0.5: hamletFileDebug. (Don't get attached to any names, I expect a lot of renamings before it's released.) It has an identical signature to hamletFile (the external Template Haskell version): FilePath -> Q Exp. hamletFile pulls in the template from the given file at compile time, parses it and converts it into Haskell code that gets compiled. Template Haskell changes the Q Exp into a value Hamlet url.

+

hamletFileDebug is slightly different. It also pulls in the template from the file at compile time and parses it. Next, it scans through the parsed template and finds all references to variables and how they are used. Using the example above, it would notice that the template refers to name and expects it to be a String. It then creates the appropriate HamletData value based on all these references.

+

Like hamletFile, hamletFileDebug will also return a value of type Hamlet url. However, this value will in fact read the specified file at runtime and render it against the HamletData value each time. In order to achieve this, it has to use unsafePerformIO; since this is intended to enhance your development, and should not be used in production, I think it's a fair use.

+

How it plays out

+

So how does this interact with a normal Hamlet development workflow? Let's go back to the name to names example: if you change your Haskell code first, hamletFileDebug will get called again during your recompile and will inspect the template. It will notice that the name variable is no longer in scope, and you will get a compile-time error.

+

On the other hand, if you change the Hamlet template file first, you'll end up with a runtime error. However, I think it's a fair trade-off: you're skipping a compile cycle here, so the only way to get an error message is at runtime.

+

Looking at the exclamation mark example, no code changes are required, only template changes. In this case, your changes will become immediately visible.

+

Next steps

+

I'm very excited about how this change will affect my development cycle; it will make it much easier to play around with HTML changes to see the results. This also converges with some other work I've been doing: Stylish. I created a github repo for Stylish a few days ago, which is meant to be Hamlet for CSS. It is based on the recently released blaze-builder, which will also be the core of blaze-html 0.2.

+

Even though Yesod offers an addStyle function for including CSS declarations, I almost always use static CSS files for this purpose, simply because I can't afford to go through a whole compile cycle to test out changing the border from 2px to 3px. (I know about firebug, don't worry.) However, Hamlet 0.5 should hopefully solve this problem: by using the same technique of hamletFileDebug, I'll be able to test out style changes immediately.

+

So now I'm announcing the inclusion of two more pieces in Hamlet 0.5: Camlet and Jamlet. They are to CSS and Javascript, respectively, what Hamlet is to HTML. Camlet is going to be what Stylish is right now: white-space sensitive CSS allowing you to embed variables from Haskell and nest CSS declarations. I think Jamlet will simply be a text pass-through for the moment, but may eventually support Javascript compression.

+

By using these two libraries with Hamlet, you won't need to worry about deploying your static files separately. And here's the really cool feature I'm hoping to add to Yesod 0.5: caching. When you use addStyle in Yesod, it will concatenate all of the CSS declarations for a page together. It will then take a hash of that value, cache the value based on the hash, and send a link tag referencing that content via the hash. When that content is served, it will have an expiration date set long in the future; since the hash value will automatically change whenever you change the CSS, you won't need to worry about users getting stale CSS files. The same will be true for Javascript.

+

This will also give you a number of minor benefits. For example, instead of hard-coding CSS names in your HTML, CSS and JavaScript separately, you could declare the name once in Haskell and reference that variable throughout. You'll be alerted at compile time if you've made a typo in a CSS class name. You're also guaranteed to have well-formed CSS files.

+

I'm very excited about all of these changes, and hope to be making these releases soon. Let me know your thoughts on this earlier, rather than later, if possible. Along with the database migration additions, I think Yesod 0.5 is going to be a very fun release.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/08/whats-in-a-hamlet.html b/public/blog/2010/08/whats-in-a-hamlet.html new file mode 100644 index 00000000..c8f8dd47 --- /dev/null +++ b/public/blog/2010/08/whats-in-a-hamlet.html @@ -0,0 +1,62 @@ + What's in a Hamlet? +

What's in a Hamlet?

August 1, 2010

GravatarBy Michael Snoyman

Quick message from our sponsors: I just wanted to inform everyone that I'm nearing a new release of Yesod, version 0.5. This release introduces very few changes versus 0.4, notably some enhancements to forms. The main reason for this release is to upgrade to the (not yet released) persistent 0.2, which adds some major features, most notably database migrations. I encourage everyone to check out the code on github.

+

What's in a Hamlet?

+

Hamlet- for those not familiar- is an HTML templating system for Haskell with a Haml-inspired syntax. It is quasi-quoted, meaning it is fully parsed and converted to Haskell code and checked at compile time with the rest of your code. (I'm currently toying with the idea of adding support for runtime-parsed templates, but that is quite orthogonal to this discussion.)

+

Hamlet is built on top of BlazeHtml, and can be called in Haskell code as follows:

+
myTemplate = [$hamlet|
+%h1 Hello World!
+%p This is some static stuff.
+%ul
+    $forall people person
+        %li $person$
+|]
+

Now, given everything I've just said, you might guess that the type signature for myTemplate is myTemplate :: Html (); however, you'd be wrong. The actual signature is in fact myTemplate :: (a -> String) -> Html (). There's a function (hamlet') in the Hamlet repo which will most likely be included in the next release which would create the former type signature, but for now I want to focus on the latter signature.

+

Coding PHP in Haskell

+

I'm not trying to pick on PHP here (well, maybe a little...). Let's say that I want to create a link in a PHP web app. I'm sure you're used to seeing code like <a href="person.php?person_id=<? echo $person_id;?>">Check out this guy</a>. I'm sure many web developers can remember getting 404 errors because they forgot to update all their URLs when they renamed person.php to people.php.

+

I was serious: I don't want to pick on PHP here. This was a problem back in the days of Perl CGI scripts, or if you were aggressive enough, C CGI scripts as well. For that matter, it's a bane when writing static HTML as well.

+

A partial solution

+

Many (most?) web frameworks try to deal with this problem. My largest pre-Haskell web experience was with Django, so I'll speak to their approach. Caveat emptor: I stopped using Django before they hit 1.0, so my information and terminology is probably dated. Over there, when you declare a route, you can also give a name to that route. You declare routes in Django via regular expressions; you then have utility functions that take a named route and pieces to "fill the gap" in the regular expressions, and thus URLs are born.

+

With this approach, it doesn't matter if you change "/person/(\d+)/" to "/people/(\d+)/"; as long as the name stays the same, your code will still point to the right URL. Thus a whole class of annoying 404 bugs are eliminated, and the gods were pleased.

+

But it's still dynamic

+

I'll assume I'm preaching to the choir here: dynamic languages don't give you a lot of safety assurances. The "compiler" will happily accept Print instead of print and then complain at runtime. Many people find this to be an acceptable situation; I assume if you're already using Haskell, you prefer to let the compiler do a lot of the heavy lifting for you.

+

So I ask you fellow Haskell web developers: why do you demand such safety in function calls, but not in URL construction? The problem with both the URL-splicing approach and the Django (et al) approach is a lack of compile-time safety!

+

I'll admit that original versions of Yesod had the same flaw; fortunately, I was pointed in the right direction and now type-safe URLs are a cornerstone of Yesod. The concept is unbelievably simple, and very obvious once stated: we should have a datatype represent URLs. Every URL our application accepts should correlate to some value for that datatype.

+

Here's a very basic example: we want to have a blog, which has two types of pages: the homepage, and a page for each entry. URLs look like this:

+
http://www.myblog.com/
Homepage
+
http://www.myblog.com/2010-01-01/happy-new-year/
Blog post on January 1, 2010.
+
+

In other words, the homepage requires no extra parameters, and an entry page needs to know the date and the slug of the entry. This smells a lot like an algebraic data type; let's represent URLs as:

+
data BlogRoute = Homepage | Entry Day String
+

Along with this datatype, we need some way to parse an incoming URL to a datatype and some way to render a datatype back to a URL. In general, we would want some functions that basically follow these rules:

+
parse :: String -> Maybe BlogRoute
+render :: BlogRoute -> String
+-- parse . render = Just
+

To avoid the boilerplate of creating the datatypes and proper parse/render functions, Yesod uses web-routes-quasi.

+

Back to Hamlet

+

OK, so the majority of this post was really just speaking the praises of type-safe URLs, my appologies ;). Let's get back to the topic at hand: why does a hamlet have such a strange type signature? As a reminder:

+
[$hamlet|%h1 My Blog|] :: (a -> String) -> Html ()
+

Well, using our blog example, let's try to make that type signature a little bit clearer now:

+
[$hamlet|%a!href=@Homepage@ Go to blog homepage|] :: (BlogRoute -> String) -> Html ()
+

at-sign interpolation is used to include a type-safe URL; in this case, a value of type BlogRoute. Suddenly, that mysterious a in the type signature turned into BlogRotue.

+

Does anyone notice that we've seen the BlogRoute -> String type signature before? It's in fact the type of render. This is no coincidence: a hamlet takes a URL rendering function. This is a huge boon for creating safe applications: during the entire construction of your template, you can deal with the URL datatypes directly. For the most part, every value for a href or src attribute will be surrounded by at-signs. Finally, as you're ready to render your Hamlet template, you pass it your URL rendering function.

+

Note for Yesod users

+

For those of you using Hamlet in the context of Yesod, you are mostly sheltered from this whole discussion. Yesod provides the hamletToRepHtml and applyLayout functions which automatically apply the Hamlet template to the appropriate URL rendering function. When you want to embed one Hamlet template within another, you use carrot-interpolation (^otherTemplate^) and needn't be concerned about how it works. However, I think it is very important to have this knowledge in the back of your mind.

+

A plea to fellow Haskell web developers

+

I strongly believe that type-safe URLs are one of the most powerful features we Haskell web developers have at our disposal. I'd go so far as to say it should be one of our main advertisements of the power of Haskell.

+

Unfortunately, I think that the concept has had a lukewarm reception. Many people seem not to think the effort pays off. I think this is short-sighted; you've already admitted by using Haskell that you think spending a little extra time declaring types correctly is worth the payoff in the end from a more easily maintained codebase. Type-safe URLs are simply an extension of that decision.

+

I'll speak a bit from personal experience here: I have a project that's over 10,000 LOC and over 150 separate routes. (I would give more information, but I'm not allowed currently to discuss the project publicly.) At some point, the spec changed, and I suddenly needed to add an extra piece of information to the URL of almost every route. I was able to make the transition in about an hour: I simply changed the routes declaration, hit compile, and the compiler caught every single place that needed to be adjusted.

+

Perhaps you think that's an anomalous case. Then again, perhaps you've worked with clients and can empathize.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/08/yesod-0-5.html b/public/blog/2010/08/yesod-0-5.html new file mode 100644 index 00000000..4793cd9e --- /dev/null +++ b/public/blog/2010/08/yesod-0-5.html @@ -0,0 +1,32 @@ + Yesod 0.5.0 Released +

Yesod 0.5.0 Released

August 29, 2010

GravatarBy Michael Snoyman

I'm very happy to announce a new release of the Yesod Web Framework, along with new releases of Hamlet, Persistent and web-routes-quasi. A big thanks again on this release to Greg Weber who has given lots of API feedback, written some test cases for persistent and started work on a MongoDB backend. It's not yet fully tested, but is very promising. It's also the proving ground that persistent is not tied to SQL.

+

As usual, there are a lot of new features in this release, so I'll try to highlight some of the major ones:

+
  • Hamlet now includes Cassius and Julius templating systems for CSS and Javascript, respectively. All three templating systems have a "debug" mode which allows you to see changes to your templates without recompiling. The debug mode has the same API as production mode, so you can easily lose any runtime penalty when compiling your production executable.
  • +
  • Persistent has seen a lot of cleanup, notably the split between PersistEntity and PersistBackend. The SQL specific code has also been cleaned up significantly, making it easier to add more backends in the future (such as MySQL).
  • +
  • Database migration code has been added, making it very simple to deal with schema changes. By default, it will only perform safe migrations, meaning migrations which don't lose data.
  • +
  • Static file serving can now include a hash of the contents to ensure that the contents haven't changed. This allows you to set expiration dates on your static files far in the future without concerns of users getting stale contents.
  • +
  • And tying together a few different pieces, Cassius and Julius templates can easily be added using widgets, and the concatenated content can now be served as a static file with a name based on the hash. This allows you to have a single stylesheet/javascript file for each page and have the caching hanlded properly.
  • +
  • The Yesod scaffolding tool has been updated to provide a much nicer file structure, as well as include some more advanced features by default, such as user authentication.
  • +
+

I've put up a screencast introducing Yesod 0.5, and will be following it up with some more hands-on coding examples. Also, I'm reworking the Yesod documentation site to make things more streamlined. In particular, the focus for documentation now will be in a book format, which will hopefully make it more accessible and easier to locate the correct information.

+

The four main targets I have for next steps are:

+
  • A comprehensive test suite. Greg has already started laying the foundations for this.
  • +
  • Improving Orange Roster, which should hopefully give a good example program for people to learn from.
  • +
  • Write more of the book.
  • +
  • Improve forms. I've been having some discussions via email, and I definitely think we can do better than where we're at right now.
  • +
+

Happy hacking!

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/adding-varnish.html b/public/blog/2010/09/adding-varnish.html new file mode 100644 index 00000000..f3e6c9f5 --- /dev/null +++ b/public/blog/2010/09/adding-varnish.html @@ -0,0 +1,35 @@ + Adding a little varnish +

Adding a little varnish

September 6, 2010

GravatarBy Michael Snoyman

Since the latest release of Yesod I've been working on consolidating Yesod information onto my new VPS system. I'd previously kept this blog on my personal domain name and the docs site separate, and generated both of them as static sites (the former via a custom script, the latter via Hakyll).

+

However, I decided to combine both sites into a single Yesod site; you are reading the result. In the process, I've put the majority of the old documentation into the book and created separate examples and screencasts sections. Hopefully, this should make it much easier to find information.

+

Testing examples

+

All of the examples on the site double as executables in the yesod-examples package on hackage. This provides a simple means to test that the examples are written correctly and still compile with new versions of Yesod. However, up until recently, examples in the rest of the documentation and the book had no such testing involved.

+

So I devised a very simple solution: there's a snippets folder that contains various Haskell files, each one a stand-alone executable. The book simply embeds pieces from those files for the code snippets. Also, I finally added in syntax highlighting via hscolour.

+

Varnish

+

Anyway, the part that I thought people would actually be interested in: loading up all of these various static files for each request and performing all the syntax highlighting and markdown rendering is a fairly heavy performance price. At first, I'd thought about playing with caching the results in temporary files and checking file modification times.

+

Instead, a much simpler solution presented itself: Varnish, an HTTP accelerator that performs caching. I'd read about it a while ago, but never found it necessary, since most of my Yesod sites are purely dynamic sites, which are not amenable to caching.

+

Setting up Varnish was incredibly simple: I already run my nginx server on port 8080 and have port forwarding redirect traffic from port 80. Now, Varnish simply listens on port 8081 and port forwarding redirects 80 to 8081. And I get to have that warm, fuzzy feeling of using some new technology I read about on Reddit. Yeah!

+

The only code change required to take advantage of Varnish is an HTTP cache-control response header, which is already easy enough with the setHeader Yesod function. I'll probably make a minor release of Yesod (0.5.1?) to include some helper functions to make this easier.

+

Which reminds me...

+

It's a real pain getting a VPS set up. Now that I'm hosting a number of different Yesod apps from my VPS now, it's fairly easy and straightforward to add more Yesod apps to it. Each Yesod app requires a very small amount of resources (< 20M of RAM and not too much CPU), but still requires a VPS with enough RAM to actually pull off the compiles itself.

+

This makes me think Yesod apps are quite amenable to some kind of managed hosting. I've thought about this a little bit, and here's the idea I've come up with:

+
  • We would need some kind of standard format for applications. I think a cabalized tarball is the perfect fit.
  • +
  • Have a requirement that each cabal file have a rule to build an executable called fastcgi, and build this file with the cabal flag production. This is what the current scaffolder does. (A future idea would be to design a file that could use any WAI backend, but I wanted to start simple.)
  • +
  • Since Template Haskell code can be run at compile time, for security the build of the process should occur in a chroot jail.
  • +
  • Running the application would also occur in a chroot jail. All of the source code from the tarball would be present in the chroot jail, so extra resources (settings files, etc) would be present. Static files should follow the standard layout: in the static folder.
  • +
  • As per my deploying advice, use spawn-fcgi and bind to a named socket, and have an nginx config block automatically generated that talks to that socket. Perhaps some information on domain name should be included in the tarball, of perhaps that would be part of a web-based admin util.
  • +
+

I'm kind of tempted to set up an automated system like this, if only to automate the management of my current sites. If anyone would be interested in this kind of hosting, and would like to help me test this out, let me know. I still have a little bit of extra room on my VPS to play around with.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/announcing-http-enumerator.html b/public/blog/2010/09/announcing-http-enumerator.html new file mode 100644 index 00000000..67003f73 --- /dev/null +++ b/public/blog/2010/09/announcing-http-enumerator.html @@ -0,0 +1,21 @@ + Announcing http-enumerator +

Announcing http-enumerator

September 21, 2010

GravatarBy Michael Snoyman

My first task when working on Haskellers.com was to set up OpenID authentication. My authenticate package only supports OpenID 1, so I looked into the openid package. Unfortunately, it didn't work out of the box for me. As I started digging, the reason appeared to be the HTTPS connections.

+

It's really unfortunate that while so many of our tools in Haskell are incredibly advanced, our HTTP client libraries are lackluster. When originally writing the authenticate package, I stumbled upon this lack of HTTPS support. I say stumble because the library not once advertises its lack of support, nor does it complain when you try to download something from an HTTPS URL. Instead, it simply connects over vanilla HTTP. I've also been less than satisfied with the complexities involved in the HTTP library's API.

+

The other mainstream solution is the curl package. This also suffers from some warts: the API is basically a mirror of the C library. I've coded against that library before, and I don't feel like doing it again in Haskell. It also introduces a dependency on a system library, which is something I'm trying to avoid whenever possible for Yesod.

+

So I wrote a package called http-wget which simply wraps around the wget executable. This is an ugly hack, but at least it worked, and I could make a nice, simple API. However, this isn't a real solution, and as I was trying to get the openid package to work, I realized we finally just need a working package capable of making HTTPS connections.

+

So I'd like to announce the first release of http-enumerator. It uses attoparsec, enumerator and OpenSSL to try and deliver a simple, efficient API. This release is definitely an experimental release, so don't start using it in your production code. I have tested it with the authenticate package, and I have no trouble accessing Facebook's graph API, which is a promising sign.

+

I am intending on making changes to the API still, and I'd appreciate feedback from others. If you have a chance, take a look and tell me what you think. Also, since it depends on the hsopenssl package it still introduces a system library dependency. I'd like to experiment at some point with providing an alternate package that embeds a mini SSL library such as MatrixSSL.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/enumerators-tutorial-part-1.html b/public/blog/2010/09/enumerators-tutorial-part-1.html new file mode 100644 index 00000000..7de3b933 --- /dev/null +++ b/public/blog/2010/09/enumerators-tutorial-part-1.html @@ -0,0 +1,227 @@ + Enumerators Tutorial Part 1: Iteratee +

Enumerators Tutorial Part 1: Iteratee

September 30, 2010

GravatarBy Michael Snoyman

This content is now part of the Yesod book. It is recommended to read there, since the content is more up-to-date.

+

Introduction

+

One of the upcoming patterns in Haskell is the enumerators. Unfortunately, it's very difficult to get started with them since:

+
  • There are multiple implementations, all with slightly different approaches.

  • +
  • Some of the implementations (in my opinion) use incredibly confusing naming.

  • +
  • The tutorials that get written usually don't directly target an existing implementation, and work more on building up intuition than giving instructions on how to use the library.

  • +
+

I'm hoping that this tutorial will fill the gap a bit. I'm going to be basing this on the enumerator package. I'm using version 0.4.0.2, but this should be applicable to older and hopefully newer versions as well. This package is newer and less used than the iteratee package, but I've chosen it for three reasons:

+
  • It has a much smaller dependency list.

  • +
  • It's a smaller package, and therefore easier to wrap your mind around.

  • +
  • I think the naming is better.

  • +
+

That said, both packages are built around the same basic principles, so learning one will definitely help you with the other.

+

Three Parts

+

The title of this post says this is part 1. In theory, there will be three parts (though I may do more or less, I'm not certain yet). There are really three main concepts to learn to use the enumerator package: iteratees, enumerators and enumeratees. A basic definition would be:

+
  • Iteratees are consumers: they are fed data and do something with it.

  • +
  • Enumerators are producers: they feed data to an iteratee.

  • +
  • Enumeratees are pipes: they are fed data from an enumerator and then feed it to an iteratee.

  • +
+

What good are enumerators?

+

But before you really get into this library, let's give some motivation for why we would want to use it. Here's some real life examples I use the enumerator package for:

+
  • When reading values from a database, I don't necessarily want to pull all records into memory at once. Instead, I would like to have them fed to a function which will consume them bit by bit.

  • +
  • When processing a YAML file, instead of reading in the whole structure, maybe you only need to grab the value of one or two records.

  • +
  • If you want to download a file via HTTP and save the results in a file, it would be a waste of RAM to store the whole file in memory and then write it out. Enumerators let you perform interleaved IO actions easily.

  • +
+

A lot of these problems can also be solved using lazy I/O. However, lazy I/O is not necessarily a panacea: you might want to read some of Oleg's stuff on the pitfalls of lazy I/O.

+

Intuition

+

Note: you can see the code in this post as a github gist.

+

Let's say we want to write a function that sums the numbers in a list. Forgetting uninteresting details like space leaks, a perfectly good implementation could be:

+
sum1 :: [Int] -> Int
+sum1 [] = 0
+sum1 (x:xs) = x + sum1 xs
+
+

But let's say that we don't have a list of numbers. Instead, the user is typing numbers on the command line, and hitting "q" when done. In other words, we have a function like:

+
getNumber :: IO (Maybe Int)
+getNumber = do
+    x <- readLine
+    if x == "q"
+        then return Nothing
+        else return $ Just $ read x
+
+

We could write our new sum function as:

+
sum2 :: IO Int
+sum2 = do
+    maybeNum <- getNumber
+    case maybeNum of
+        Nothing -> return 0
+        Just num -> do
+            rest <- sum2
+            return $ num + rest
+
+

It's fairly annoying to have to write two completely separate sum functions just because our data source changed. Ideally, we would like to generalize things a bit. Let's start by noticing a similarity between these two functions: they both only yield a value when they are informed that there are no more numbers. In the case of sum1, we check for an empty list; in sum2, we check for Nothing.

+

The Stream datatype.

+

The first datatype defined in the enumerator package is:

+
data Stream a = Chunks [a] | EOF
+
+

The EOF constructor indicates that no more data is available. The Chunks constructor simply allows us to put multiple pieces of data together for efficiency. We could now rewrite sum2 to use this Stream datatype:

+
getNumber2 :: IO (Stream Int)
+getNumber2 = do
+    maybeNum <- getNumber -- using the original getNumber function
+    case maybeNum of
+        Nothing -> return EOF
+        Just num -> return $ Chunks [num]
+
+sum3 :: IO Int
+sum3 = do
+    stream <- getNumber2
+    case stream of
+        EOF -> return 0
+        Chunks nums -> do
+            let nums' = sum nums
+            rest <- sum3
+            return $ nums' + rest
+
+

Not that it's much better than sum2, but at least it shows how to use the Stream datatype. The problem here is that we still refer explicitly to the getNumber2 function, hard-coding the data source.

+

One possible solution is to make the data source an argument to the sum function, ie:

+
sum4 :: IO (Stream Int) -> IO Int
+sum4 getNum = do
+    stream <- getNum
+    case stream of
+        EOF -> return 0
+        Chunks nums -> do
+            let nums' = sum nums
+            rest <- sum4 getNum
+            return $ nums' + rest
+
+

That's all well and good, but let's pretend we want to have two datasources to sum over: values the user enters on the command line, and some numbers we read over an HTTP connection, perhaps. The problem here is one of control: sum4 is running the show here by calling getNum. This is a pull data model. Enumerators have an inversion of control/push model, putting the enumerator in charge. This allows cool things like feeding in multiple data sources, and also makes it easier to write enumerators that properly deal with resource allocation.

+

The Step datatype

+

So we need a new datatype that will represent the state of our summing operation. We're going to allow our operations to be in one of three states:

+
  • Waiting for more data.

  • +
  • Already calculated a result.

  • +
  • For convenience, we also have an error state. This isn't strictly necessary (it could be modeled by choosing an EitherT kind of monad, for example), but it's simpler.

  • +
+

As you could guess, these states will correspond to three constructors for the Step datatype. The error state is modeled by Error SomeException, building on top of Haskell's extensible exception system. The already calculated constructor is:

+
Yield b (Stream a)
+
+

Here, a is the input to our iteratee and b is the output. This constructor allows us to simultaneously produce a result and save any "leftover" input for another iteratee that may run after us. (This won't be the case with the sum function, which always consumes all its input, but we'll see some other examples that do no such thing.)

+

Now the question is how to represent the state of an iteratee that's waiting for more data. You might at first want to declare some datatype to represent the internal state and pass that around somehow. That's not how it works: instead, we simply use a function (very Haskell of us, right?):

+
Continue (Stream a -> Iteratee a m b)
+
+

Euerka! We've finally seen the Iteratee datatype! Actually, Iteratee is a very boring datatype that is only present to allow us to declare cool instances (eg, Monad) for our functions. Iteratee is defined as:

+
newtype Iteratee a m b = Iteratee (m (Step a m b))
+
+

This is important: Iteratee is just a newtype wrapper around a Step inside a monad. Just keep that in mind as you look at definitions in the enumerator package. So knowing this, we can think of the Continue constructor as:

+
Continue (Stream a -> m (Step a m b))
+
+

That's much easier to approach: that function takes some input data and returns a new state of the iteratee. Let's see what our sum function would look like using this Step datatype:

+
sum5 :: Monad m => Step Int m Int -- Int input, any monad, Int output
+sum5 =
+    Continue $ go 0 -- a common pattern, you always start with a Continue
+  where
+    go :: Monad m => Int -> Stream Int -> Iteratee Int m Int
+    -- Add the new input to the running sum and create a new Continue
+    go runningSum (Chunks nums) = do
+        let runningSum' = runningSum + sum nums
+        -- This next line is *ugly*, good thing there are some helper
+        -- functions to clean it up. More on that below.
+        Iteratee $ return $ Continue $ go runningSum'
+    -- Produce the final result
+    go runningSum EOF = Iteratee $ return $ Yield runningSum EOF
+
+

The first real line (Continue $ go 0) initializes our iteratee to its starting state. Just like every other sum function, we need to explicitly state that we are starting from 0 somewhere. The real workhorse is the go function. Notice how we are really passing the state of the iteratee around as the first argument to go: this is also a very common pattern in iteratees.

+

We need to handle two different cases: when handed an EOF, the go function must Yield a value. (Well, it could also produce an Error value, but it definitely cannot Continue.) In that case, we simply yield the running sum and say there was no data left over. When we receive some input data via Chunks, we simply add it to the running sum and create a new Continue based on the same go function.

+

Now let's work on making that function a little bit prettier by using some built-in helper functions. The pattern Iteratee . return is common enough to warrant a helper function, namely:

+
returnI :: Monad m => Step a m b -> Iteratee a m b
+returnI = Iteratee . return
+
+

So for example,

+
go runningSum EOF = Iteratee $ return $ Yield runningSum EOF
+
+

becomes

+
go runningSum EOF = returnI $ Yield runningSum EOF
+
+

But even that is common enough to warrant a helper function:

+
yield :: Monad m => b -> Stream a -> Iteratee a m b
+yield x chunk = returnI $ Yield x chunk
+
+

so our line becomes

+
go runningSum EOF = yield runningSum EOF
+
+

Similarly,

+
Iteratee $ return $ Continue $ go runningSum'
+
+

becomes

+
continue $ go runningSum'
+
+

Monad instance for Iteratee

+

This is all very nice: we now have an iteratee that can be fed numbers from any monad and sum them. It can even take input from different sources and sum them together. (By the way, I haven't actually shown you how to feed those numbers in: that is in part 2 about enumerators.) But let's be honest: sum5 is an ugly function. Isn't there something easier?

+

In fact, there is. Remember how I said Iteratee really just existed to facilitate typeclass instances? This includes a monad instance. Feel free to look at the code to see how that instance is defined, but here we'll just look at how to use it:

+
sum6 :: Monad m => Iteratee Int m Int
+sum6 = do
+    maybeNum <- head -- not head from Prelude!
+    case maybeNum of
+        Nothing -> return 0
+        Just i -> do
+            rest <- sum6
+            return $ i + rest
+
+

That head function is not from Prelude, it's from the Data.Enumerator module. Its type signature is:

+
head :: Monad m => Iteratee a m (Maybe a)
+
+

which basically means give me the next piece of input if it's there. We'll look at this function in more depth in a bit.

+

Go compare the code for sum6 with sum2: they are amazingly similar. You can often build up more complicated iteratees by using some simple iteratees and the Monad instance of Iteratee.

+

Interleaved I/O

+

Alright, let's look at a totally different problem. We want to be fed some strings and print them to the screen one line at a time. One approach would be to use lazy I/O:

+
lazyIO :: IO ()
+lazyIO = do
+    s <- lines `fmap` getContents
+    mapM_ putStrLn s
+
+

But this has two drawbacks:

+
  • It's tied down to a single input source, stdin. This could be worked around with an argument giving a datasource.

  • +
  • But let's say the data source is some scarce resource (think: file handles on a very busy web server). We have no guarantees with lazy I/O of when those file handles will be released.

  • +
+

Let's look at how to write this in our new high-level monadic iteratee approach:

+
interleaved :: MonadIO m => Iteratee String m ()
+interleaved = do
+    maybeLine <- head
+    case maybeLine of
+        Nothing -> return ()
+        Just line -> do
+            liftIO $ putStrLn line
+            interleaved
+
+

The liftIO function comes from the transformers package, and simply promotes an action in the IO monad to any arbitrary MonadIO action. Notice how we don't really track any state with this iteratee: we don't care about its result, only its side effects.

+

Implementing head

+

As a last example, let's actually implement the head function.

+
head' :: Monad m => Iteratee a m (Maybe a)
+head' =
+    continue go
+  where
+    go (Chunks []) = continue go
+    go (Chunks (x:xs)) = yield (Just x) (Chunks xs)
+    go EOF = yield Nothing EOF
+
+

Like our sum6 function, this also wraps an inner "go" function with a continue. However, we now have three clauses for our go function. The first handles the case of Chunks []. To quote the enumerator docs:

+

(Chunks []) is used to indicate that a stream is still active, but currently has no available data. Iteratees should ignore empty chunks.

+
+

The second clause handles the case where we are given some data. In this case, we yield the first element in the list, and return the rest as leftover data. The third clause handles the end of input by returning Nothing.

+

Exercises

+
  • Rewrite sum6 using liftFoldL'.

  • +
  • Implement the consume function using first the high-level functions like head, and then using only low-level stuff.

  • +
  • Write a modified version of consume that only keeps every other value, once again using high-level functions and then low-level constructors.

  • +
+

Next time

+

Well, now you can write iteratees, but they're not very useful if you can't actually use them. Next time we'll cover what an enumerator is, some basic enumerators included with the package, how to run these things and how to write your own enumerator.

+

Summary

+

Here's what I consider the most important things to glean from this tutorial:

+
  • Iteratee is a simple wrapper around the Step datatype to allow for cool typeclass instances.

  • +
  • Using the Monad instance of Iteratee can allow you to build up complicated iteratees from simpler ones.

  • +
  • The three states an enumerator can be in are Continue (still processing data), Yield (a result is ready) and Error (duh).

  • +
  • Well behaved iteratees will never return a Continue after receiving an EOF.

  • +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/modular-authentication.html b/public/blog/2010/09/modular-authentication.html new file mode 100644 index 00000000..1bdff320 --- /dev/null +++ b/public/blog/2010/09/modular-authentication.html @@ -0,0 +1,27 @@ + Modular Authentication +

Modular Authentication

September 20, 2010

GravatarBy Michael Snoyman

Dependencies are often a bit of a bind. On the one hand, I want Yesod to be as fully-featured as possible. On the other hand, adding dependencies makes it more difficult to install, especially when those dependencies are system C libraries.

+

Case in point: Yesod currently has support for OpenID version 1. There is a package providing OpenID 2 support on Hackage, but it introduces a dependency on OpenSSL, which could be difficult for some users to install. (Side point: I can't actually get the package to work; has anyone else had any luck?)

+

The current approach to authentication hard-codes in all of the possible authentication backends. For the moment, this is OpenID 1, Rpxnow, Facebook and email address. But now I want to support OpenID 2, without changing the Yesod package at all. As it is, this is not possible.

+

So I'm experimenting with a new, modular authentication framework based upon authentication plugins. The system is very simple: a plugin consists of three pieces:

+
  • A name.
  • +
  • A widget to display on the login screen. For Facebook, this is a "click here to login" link. For OpenID, it's a typical OpenID input field.
  • +
  • A dispatch function for handling user requests.
  • +
+

There's a datatype called Creds, as in credentials, and a function called setCreds provided by the new auth module. Plugins simply call setCreds when a user logs in. The application developer determines how to map credentials onto his/her database, and the auth module itself handles the logout page, a status page (useful for Ajax programs) and a login page.

+

The downside of this new approach is a loss of some type safety: individual plugins cannot have type-safe URLs, but instead must use a [String] to create links. This is definitely a downside, but I think it is a necessary evil to provide the modularity necessary here.

+

I've created a new branch on github for this code. If you want to take a look, I'd recommend looking at the sample app and the Yesod.Helpers.Auth2 module.

+

One question: I'm considering separating the entire authentication system into its own package so that its API can change without affecting Yesod version numbers. Any thoughts on that approach? Obviously plugins will be released as their own packages under this new scheme.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/wai-handler-devel.html b/public/blog/2010/09/wai-handler-devel.html new file mode 100644 index 00000000..fbab99cc --- /dev/null +++ b/public/blog/2010/09/wai-handler-devel.html @@ -0,0 +1,22 @@ + WAI development server (aka, skip the compiles) +

WAI development server (aka, skip the compiles)

September 14, 2010

GravatarBy Michael Snoyman

One of the most alluring advantages of dynamic languages is the rapid development cycle. When writing a web app in PHP, for example, you save your changes and see the results immediately. Us poor Haskellers are stuck compiling all the time.

+

Of course, it's not really true that PHP results are immediate. It's simply that PHP reinterprets your code automatically. But this perceived immediacy is very important. Having to switch back to a console to stop a process and start it up again is tedious.

+

So I've just released version 0.0.0 of wai-handler-devel. What it does is fairly simple: load up your code at runtime via hint, check for code changes every second and otherwise simply respond like a plain old simpleserver. This release has not been extensively tested, and has one known bug: it doesn't respond to a Ctrl-C on the console properly. Patches welcome; I just wanted to get this out the door quickly.

+

This package comes with a command line tool and a library. The library exports a single function, run, which happens to take the exact same arguments as the command line tool. These are the port number, the module name containing the application function, and the name of the application function. The only tricky thing is the type signature for the application function. Instead of simply using the WAI Application datatype, the function must have a type signature of:

+
withYourApp :: (Application -> IO ()) -> IO ()
+

By having this funny signature, it allows applications to be written in the with-style, making automatic resource management simpler. The Yesod scaffolding tool, for example, creates a function like this by default.

+

This code is very young, so please abuse it and make it break. This should hopefully make it much easier to test out WAI applications, and Yesod applications in particular.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/yammer-screencast.html b/public/blog/2010/09/yammer-screencast.html new file mode 100644 index 00000000..1ee870ae --- /dev/null +++ b/public/blog/2010/09/yammer-screencast.html @@ -0,0 +1,21 @@ + Yammer Screencast +

Yammer Screencast

September 13, 2010

GravatarBy Michael Snoyman

I've finally gotten around to making the Yammer screencast. This does a fair job of showing real-life usage of Persistent and Hamlet. You can see it on the screencasts page or directly on Vimeo.

+

Just for a little status update on Yesod itself: there has been very little activity on the code side. I've added support for both 32 and 64 bit integers in the persistent SQL backends, but have otherwise been focused on documentation.

+

I believe we're at the point where the APIs are ready to stabilize. I'm beginning to think that the next Yesod release will be a version 1.0. As such, if anyone has any API changes or feature requests they would like to see, I recommend you let me know as soon as possible. I don't have any timetable on a 1.0 release, but I would like to start getting a basic roadmap down soon.

+

Right now, I think the two biggest areas to help out with are documentation and testing. If you notice things in the Yesod book that could be clarified, please leave a comment. I'm also planning on adding a wiki section to the site, which should make it easier to contribute. As far as testing: the Hamlet, Persistent and Yesod packages all have their own test suites: if you see something that could be added, send me a patch.

+

One thing that jumps to mind as a feature for me would be adding more authentication support, in particular OpenID 2 and OAuth (for Twitter). The one thing I'm trying to avoid is adding C-library dependencies, as these can make it much more difficult to get started with Yesod. I believe that currently available implementations of those two protocols rely on a system OpenSSL library, so this will require more thought.

+

A 1.0 release is important since it marks a level of maturity and stability in the project, but it does not represent any kind of finish line. Once Yesod is considered more stable, it should be easier to build on more support tools. Some examples would be externally available subsites (such as a wiki or blog engine), automatic memcached interaction for caching static content and database queries, and more persistent backends. And I'm sure the best ideas are the ones that I haven't even thought of yet.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/yesod-0-5-1.html b/public/blog/2010/09/yesod-0-5-1.html new file mode 100644 index 00000000..34fa4a33 --- /dev/null +++ b/public/blog/2010/09/yesod-0-5-1.html @@ -0,0 +1,62 @@ + Yesod 0.5.1: HTML sanitization, generalized Hamlet and pluggable authentication +

Yesod 0.5.1: HTML sanitization, generalized Hamlet and pluggable authentication

September 28, 2010

GravatarBy Michael Snoyman

For the past week or so, most of my programming focus has been going into projects tangential to Yesod development, mostly http-enumerator. As an aside: I think the enumerator package is wonderful; does anyone want me to write up a tutorial on how to use it?

+

Anyway, there has been some activity on the Yesod side as well. If you remember, I'd previously mentioned that I thought Yesod was approaching a 1.0 release, marking it as more API-stable. I asked for some input on things people would like to see, and got some great responses. Here's a quick run-down of what's new. Fortunately, this release does not break any APIs, so you can easily upgrade.

+

More comments

+

Normally I wouldn't mention updating documentation, but this is slightly different. I've now fully commented the site created by the yesod executable scaffolder. It should be easier to understand what's going on in there and why certain design decisions were made.

+

HTML Sanitization

+

Greg Weber wrote the xss-sanitize package to remove dangerous bits from HTML. The HTML and NicHTML form fields now use this function to avoid any nefarious user input.

+

Generalized Hamlet/Easier Widgets

+

Widgets make it easy to have modular components consisting of HTML, CSS and JavaScript. The downside is that it can be a bit tedious to use them. For example:

+
myInnerWidget = do
+    addBody [$hamlet|...|]
+    addStyle [$cassius|...|]
+
+myOuterWidget = do
+    inner <- extractBody myInnerWidget
+    addBody [$hamlet|
+        %h1 This is the inner widget
+        ^inner^
+    |]
+
+

Having to use the extractBody is a bit of an annoyance. Fortunately, this is no longer necessary. With the release of Hamlet 0.5.1, Hamlet templates are now generalized using the HamletValue typeclass. This essentially means we can use Hamlet to produce things besides Hamlet values.

+

On the Yesod side, we now have an instance of HamletValue for Widget. This means the code above can be written as:

+
myInnerWidget = do
+    addBody [$hamlet|...|]
+    addStyle [$cassius|...|]
+
+myOuterWidget = [$hamlet|
+    %h1 This is the inner widget
+    inner <- extractBody myInnerWidget
+    ^myInnerWidget^
+|]
+
+

Notice that we no longer prepend a call to addBody; the value of the quasi-quoted template is itself a widget. This also means that if we want to embed plain Hamlet templates, we'd need to do something like this:

+
myPlainTemplate :: Hamlet MyRoute
+myPlainTemplate = [$hamlet|...|]
+
+myWidget = [$hamlet|
+    %h1 Embed another widget
+    ^myInnerWidget^
+    %h1 Embed a Hamlet
+    ^addBody.myPlainTemplate^
+|]
+
+

This may not seem like a big deal, but I think it will make people's coding much easier and allow us to write widget code in a more modular fashion.

+

Upcoming features/Breaking changes

+

I mentioned previously that I was dissatisfied with the authentication subsite. In Yesod 0.6 I will be removing the Yesod.Helpers.Auth. Instead, I'm creating a separate yesod-auth package. This will allow us to add dependencies to yesod-auth without affecting yesod, as well as make breaking changes without influencing yesod version numbers.

+

As I said at the beginning of this post, I've been working mainly on http-enumerator recently. The impetus to get started on that was to get good OpenID 2 support going for the haskellers.com website (nothing there yet). Currently, the Yesod authentication system only works with OpenID 1. I'm hoping that by Yesod 0.6, we'll have support for both of them.

+

For some bright-eyed Haskeller out there...

+

One last feature I'd love to see implemented is a Javascript minifier to attach to Julius. Anyone out there want to take a swing at it?

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/09/yo-dawg-template-haskell.html b/public/blog/2010/09/yo-dawg-template-haskell.html new file mode 100644 index 00000000..022dc183 --- /dev/null +++ b/public/blog/2010/09/yo-dawg-template-haskell.html @@ -0,0 +1,19 @@ + Yo dawg, I heard you like Template Haskell +

Yo dawg, I heard you like Template Haskell

September 7, 2010

GravatarBy Michael Snoyman

Yo dawg, I heard you like Template Haskell, so I put some TH in your TH so you can meta program while you meta program.

+ +

This is just a little bit of fun I've been meaning to write and post for a while now. Enjoy.

+

In totally unrelated news, I just set up the Gold linker on my systems and my Haskell builds are much faster. Especially nice when compiling on my VPS.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/announcing-zlib-bindings.html b/public/blog/2010/10/announcing-zlib-bindings.html new file mode 100644 index 00000000..191853d3 --- /dev/null +++ b/public/blog/2010/10/announcing-zlib-bindings.html @@ -0,0 +1,55 @@ + Announcing zlib-bindings +

Announcing zlib-bindings

October 9, 2010

GravatarBy Michael Snoyman

I just released version 0.0.0 of the zlib-bindings package, which I think fills a nice gap in the compression realm for Haskell. Up until now, we've had the zlib package, which provides a nice, high-level interface to compression based on lazy bytestrings. Unfortunately, there are two problems:

+
  1. This depends on lazy IO, which many people try to avoid.

  2. +
  3. There is no way to use this library when working with streams.

  4. +
+

The second point forced me to write some FFI code into the wai-extra package to implement the Gzip middleware. More recently, I decided to add compressed HTTP support to http-enumerator, and was facing the same challenge. Instead, I decided to simply pull all of that code into a single package.

+

The package uses the zlib terminology of deflate for compression and inflate for decompression. There are API docs, but I think the easiest way to get started is to simply read the unit tests. Here are two samples: the first performs deflation/compression, the second inflation/decompression:

+
deflateTest = do
+    license <- S.readFile "LICENSE"
+    def <- initDeflate 7 $ WindowBits 31
+    gziped <- withDeflateInput def license $ go id
+    gziped' <- finishDeflate def $ go gziped
+    let raw' = L.fromChunks [license]
+    assertEqual raw' $ Gzip.decompress $ L.fromChunks $ gziped' []
+  where
+    go front x = do
+        y <- x
+        case y of
+            Nothing -> return front
+            Just z -> go (front . (:) z) x
+
+inflateTest = do
+    license <- S.readFile "LICENSE"
+    gziped <- S.readFile "LICENSE.gz"
+    inf <- initInflate $ WindowBits 31
+    ungziped <- withInflateInput inf gziped $ go id
+    final <- finishInflate inf
+    assertEqual license $ S.concat $ ungziped [final]
+  where
+    go front x = do
+        y <- x
+        case y of
+            Nothing -> return front
+            Just z -> go (front . (:) z) x
+
+

The package is tested on both Linux and Windows, and since it uses the zlib package internally, it does not introduce a system library dependency on Windows. Hopefully this package will be of use to some people.

+

I also simulataneously released a new version of wai-extra which uses this library internally, and http-enumerator 0.2.0 which includes gzip compression support. This new version of http-enumerator includes some minor fixes, and also consolidates all exceptions into a single type (HttpException).

+

Following the dominos along, I've also released authenticate 0.7.0; the only changes there are a bump to http-enumerator 0.2.* and also combining all exceptions into AuthenticateException. This is the library underlying the login mechanism for haskellers.com, and so has seen a good amount of testing recently.

+

And finally there's new releases of yesod (0.5.2) and yesod-auth (0.1.2) which accounts for the new authenticate version. There are also a few minor additions to yesod; I won't bore you with details here, you can check out the log on github if you're interested.

+

Yesod 0.6

+

I'm still confident that we are fast approaching a 1.0 release of Yesod. My guess is that 0.6 will be the last release beforehand. The goal for that release is to separate off some tangential aspects into separate packages. I've already mentioned yesod-auth; I'll probably also release Yesod.Mail as a separate yesod-mail package. I'm also considering adding support for markdown emails and a few other features that could be nifty.

+

I've said it before, but I'll throw it out there again: if you have any feature requests for yesod, it'll be easier to add them in now versus later. You can post your ideas in the comments here, on the web-devel list or just email me directly.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/custom-forms.html b/public/blog/2010/10/custom-forms.html new file mode 100644 index 00000000..84de42e4 --- /dev/null +++ b/public/blog/2010/10/custom-forms.html @@ -0,0 +1,77 @@ + Custom Forms and the Form Monad +

Custom Forms and the Form Monad

October 20, 2010

GravatarBy Michael Snoyman

I think it's fair to say that forms are one of the most complicated parts of web development. A huge part of this complication simply comes from the tedious, boilerplate style code we need to write. A library such as formlets can do wonders to clean this up. Yesod 0.5 follows the philosophy behind formlets.

+

formlets is great because it lets you forget about so many of the boring details of your form: names, ids, layout. This can all be handled in an automated manner. For example (using Yesod):

+
data Person = Person { name :: String, age :: Int }
+personForm = fieldsToTable $ Person
+    <$> stringField "Name" Nothing
+    <*> intField "Age" Nothing
+myHandler = do
+    (res, form, enctype) <- runFormPost personForm
+    ...
+    defaultLayout [$hamlet|^form^|]
+
+

Sometimes, however, you do want to deal with some of those pesky details. Yesod already allows overriding the automatically generated ids and names, eg:

+
<$> stringField "Name" { ffsId = Just "name-field" } Nothing
+
+

But Yesod 0.5 doesn't really have a simply way to produce arbitrarily laid out forms. For example, we might want to get fancy with our Person form and produce:

+
Hi, my name is <input type="text" name="name"> and I am <input type="number" name="age"> years old.
+
+

You see, the forms library keeps all of the information on the view of the form locked up, and only returns the whole thing once you run the form. So in theory, with Yesod 0.5, we could do something like:

+
personForm = Person -- notice no fieldsToTable
+    <$> stringField "Name" Nothing
+    <*> intField "Age" Nothing
+myHandler = do
+    (res, [nameFieldInfo, ageFieldInfo], enctype) <- runFormPost personForm
+    ...
+    defaultLayout [$hamlet|
+    Hi, my name is ^fiInput.nameFieldInfo^ and I am ^ageFieldInfo^ years old.
+    |]
+
+

But this is an absolute recipe for disaster: we've completely lost so many of our type-safety benefits by forcing a pattern match on a specific size of a list, if you change the order of our fields the fields will be backwards, we need to remember to update multiple places when the Person datatype changes, and so on. What we'd like is to have to stringField and intField functions give us the HTML directly, something like:

+
personForm = do
+    (name, nameHtml) <- stringField "Name" Nothing
+    (age, ageHtml) <- intField "Age" Nothing
+    return (Person name age, [$hamlet|
+    Hi, my name is ^nameHtml^ and I am ^ageHtml^ years old.
+    |]
+
+

This doesn't work. The GForm datatype doesn't have a monadic instance. It's easy enough to add one, but that would result in one of two things:

+
  • We would have an inconsistent definition of our Monad and Applicative instances. You see, Applicatives cannot use the result from a previous action in determining the next course of action, which allows them to collect multiple error values. (I know this is vague, please forgive me, a fully fleshed out explanation would not fit here well.)

  • +
  • We would need to cut out the true power of the forms library, but defining a weaker Applicative instance which can't collect multiple failures. Imagine a form validation that only tells you the first validation error.

  • +
+

Additionally, the code above will only really return HTML if the stringField and intField functions succeed. It turns out that we need a different approach to handle this problem.

+

GFormMonad

+

Yesod 0.6 will be adding a new datatype, GFormMonad, to complement the GForm datatype. As a high-level overview: GForm automatically handles keeping track of validation failures and HTML for you; GFormMonad gives you direct access to these. As an example:

+
personForm = do
+    (name, nameField) <- stringField "Name" Nothing
+    (age, ageField) <- intField "Age" Nothing
+    return (Person <$> name <*> age, [$hamlet|
+    Hi, my name is ^fiInput.nameField^ and I am ^fiInput.ageField^ years old.
+    |])
+
+

This is almost identical to what we wrote as our ideal code above, but not quite. The name variable above has a datatype of FormResult String. In other words, when using GFormMonad, you have to deal with the possibility of validation errors directly. Fortunately, you are still free to use the Applicative instance of FormResult, as we did here.

+

Also notice how I'm still using stringField and intField; field functions are all now polymorphic, using the IsForm typeclass. In order to unwrap a GFormMonad, we use runFormMonadGet and runFormMonadPost, like so:

+
myHandler = do
+    ((personResult, form), enctype) <- runFormMonadPost personForm
+    case personResult of
+        FormSuccess person -> ...
+    defaultLayout [$hamlet|^form^|]
+
+

Making the run functions non-polymorphic helps the type system figure out what you're trying to do.

+

Other changes

+

As I had anticipated, there are not going to be many other changes in Yesod 0.6. Haskellers.com discovered a bug in MonadCatchIO which screwed up the database connection pool, so I had to yank all of the MonadCatchIO code out and replace it with something else. I've moved the Yesod.Mail code to a separate package, as well as Yesod.Helpers.Auth.

+

I'm going to spend a few more days going through the Haddocks and make sure names are consistent and the docs are comprehendable, and will probably make the Yesod 0.6 and Persistent 0.3 release some time early next week. It's already powering Haskellers.com and the Hackage dependency monitor, so I think it's solid enough. This is a last call for change requests!

+

Given the API stability between 0.5 and 0.6, I feel pretty confident that the next release of Yesod after this will be 1.0. My goal for that release is all about documentation: I want to get as much content as possible into the book, and have it polished. If you see something in the book you don't understand, please ask, and if you see a topic I've skipped, bring it up. We're really building a great community for Yesod, and I need everyone's help to make it the best it can be.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/enumerators-tutorial-part-2.html b/public/blog/2010/10/enumerators-tutorial-part-2.html new file mode 100644 index 00000000..b96561a2 --- /dev/null +++ b/public/blog/2010/10/enumerators-tutorial-part-2.html @@ -0,0 +1,184 @@ + Enumerators Tutorial Part 2: Enumerator +

Enumerators Tutorial Part 2: Enumerator

October 2, 2010

GravatarBy Michael Snoyman

This content is now part of the Yesod book. It is recommended to read there, since the content is more up-to-date.

+

Note: code for this tutorial is available as a github gist.

+

Extracting a value

+

When we finished the last part of our tutorial, we had written a few iteratees, but we still didn't know how to extract values from them. To start, let's remember that Iteratee is just a newtype wrapper around Step:

+
newtype Iteratee a m b = Iteratee { runIteratee :: m (Step a m b) }
+
+

First we need to unwrap the Iteratee and deal with the Step value inside. Remember also that Step has three constructors: Continue, Yield and Error. We'll handle the Error constructor by returning our result in an Either. Yield already provides the data we're looking for.

+

The tricky case is Continue: here, we have an iteratee that is still expecting more data. This is where the EOF constructor comes in handy: it's our little way to tell the iteratee to finish what it's doing and get on with things. If you remember from the last part, I said a well-behaving iteratee will never return a Continue after receiving an EOF; now we'll see why:

+
extract :: Monad m => Iteratee a m b -> m (Either SomeException b)
+extract (Iteratee mstep) = do
+    step <- mstep
+    case step of
+        Continue k -> do
+            let Iteratee mstep' = k EOF
+            step' <- mstep'
+            case step' of
+                Continue _ -> error "Misbehaving iteratee"
+                Yield b _ -> return $ Right b
+                Error e -> return $ Left e
+        Yield b _ -> return $ Right b
+        Error e -> return $ Left e
+
+

Fortunately, you don't need to redefine this yourself: enumerator includes both a run and run_ function. Let's go ahead and use it on our sum6 function:

+
main = run_ sum6 >>= print
+
+

If you run this, the result will be 0. This emphasizes an important point: an iteratee is not just how to process incoming data, it is the state of the processing. In this case, we haven't done anything to change the initial state of sum6, so we still have the initial value of 0.

+

To give an analogy: think of an iteratee as a machine. When you feed it data, you modify the internal state but you can't see any of those changes on the outside. When you are done feeding the data, you press a button and it spits out the result. If you don't feed in any data, your result is the initial state.

+

Adding data

+

Let's say that we actually want to sum some numbers. For example, the numbers 1 to 10. We need some way to feed that into our sum6 iteratee. In order to approach this, we'll once again need to unwrap our Iteratee and deal with the Step value directly.

+

In our case, we know with certainty that the Step constructor we used is Continue, so it's safe to write our function as:

+
sum7 :: Monad m => Iteratee Int m Int
+sum7 = Iteratee $ do
+    Continue k <- runIteratee sum6
+    runIteratee $ k $ Chunks [1..10]
+
+

But in general, we won't know what constructor will be lying in wait for us. We need to properly deal with Continue, Yield and Error. We've seen what to do with Continue: feed it the data. With Yield and Error, the right action in general is to do nothing, since we've already arrived at our final result (either a successful Yield or an Error). So the "proper" way to write the above function is:

+
sum8 :: Monad m => Iteratee Int m Int
+sum8 = Iteratee $ do
+    step <- runIteratee sum6
+    case step of
+        Continue k -> runIteratee $ k $ Chunks [1..10]
+        _ -> return step
+
+

Enumerator type synonym

+

What we've done with sum7 and sum8 is perform a transformation on the Iteratee. But we've done this in a very limited way: we've hard-coded in the original Iteratee function (sum6). We could just make this an argument to the function:

+
sum9 :: Monad m => Iteratee Int m Int -> Iteratee Int m Int
+sum9 orig = Iteratee $ do
+    step <- runIteratee orig
+    case step of
+        Continue k -> runIteratee $ k $ Chunks [1..10]
+        _ -> return step
+
+

But since we always just want to unwrap the Iteratee value anyway, it turns out that it's more natural to make the argument of type Step, ie:

+
sum10 :: Monad m => Step Int m Int -> Iteratee Int m Int
+sum10 (Continue k) = k $ Chunks [1..10]
+sum10 step = returnI step
+
+

This type signature (take a Step, return an Iteratee) turns out to be very common:

+
type Enumerator a m b = Step a m b -> Iteratee a m b
+
+

Meaning sum10's type signature could also be expressed as:

+
sum10 :: Monad m => Enumerator Int m Int
+
+

Of course, we need some helper function to connect an Enumerator and an Iteratee:

+
applyEnum :: Monad m => Enumerator a m b -> Iteratee a m b -> Iteratee a m b
+applyEnum enum iter = Iteratee $ do
+    step <- runIteratee iter
+    runIteratee $ enum step
+
+

Let me repeat the intuition here: the Enumerator is transforming the Iteratee from its initial state to a new state by feeding it more data. In order to use this function, we could write:

+
run_ (applyEnum sum10 sum6) >>= print
+
+

This results in 55, exactly as we'd expect. But now we can see one of the benefits of enumerators: we can use multiple data sources. Let's say we have another enumerator:

+
sum11 :: Monad m => Enumerator Int m Int
+sum11 (Continue k) = k $ Chunks [11..20]
+sum11 step = returnI step
+
+

Then we could simply apply both enumerators:

+
run_ (applyEnum sum11 $ applyEnum sum10 sum6) >>= print
+
+

And we would get the result 210. (Yes, (1 + 20) * 10 = 210.) But don't worry, you don't need to write this applyEnum function yourself: enumerator provides a $$ operator which does the same thing. Its type signature is a bit scarier, since it's a generalization of applyEnum, but it works the same, and even makes code more readable:

+
run_ (sum11 $$ sum10 $$ sum6) >>= print
+
+

$$ is a synonym for ==<<, which is simply flip >>==. I find $$ the most readable, but YMMV.

+

Some built-in enumerators

+

Of course, writing a whole function just to pass some numbers to our sum function seems a bit tedious. We could easily make the list an argument to the function:

+
sum12 :: Monad m => [Int] -> Enumerator Int m Int
+sum12 nums (Continue k) = k $ Chunks nums
+sum12 _ step = returnI step
+
+

But now there's not even anything Int-specific in our function. We could easily generalize this to:

+
genericSum12 :: Monad m => [a] -> Enumerator a m b
+genericSum12 nums (Continue k) = k $ Chunks nums
+genericSum12 _ step = returnI step
+
+

And in fact, enumerator comes built in with the enumList function which does this. enumList also takes an Integer argument to indicate the maximum number of elements to stick in a chunk. For example, we could write:

+
run_ (enumList 5 [1..30] $$ sum6) >>= print
+
+

(That produces 465 if you're counting.) The first argument to enumList should never affect the result, though it may have some performance impact.

+

Data.Enumerator includes two other enumerators: enumEOF simply passes an EOF to the iteratee. concatEnums is slightly more interesting; it combines multiple enumerators together. For example:

+
run_ (concatEnums
+        [ enumList 1 [1..10]
+        , enumList 1 [11..20]
+        , enumList 1 [21..30]
+        ] $$ sum6) >>= print
+
+

This also produces 465.

+

Some non-pure input

+

Enumerators are much more interesting when they aren't simply dealing with pure values. In the first part of this tutorial, we gave the example of the user entering numbers on the command line:

+
getNumber :: IO (Maybe Int)
+getNumber = do
+    x <- getLine
+    if x == "q"
+        then return Nothing
+        else return $ Just $ read x
+
+sum2 :: IO Int
+sum2 = do
+    maybeNum <- getNumber
+    case maybeNum of
+        Nothing -> return 0
+        Just num -> do
+            rest <- sum2
+            return $ num + rest
+
+

We referred to this as the pull-model: sum2 pulled each value from getNumber. Let's see if we can rewrite getNumber to be a pusher instead of a pullee.

+
getNumberEnum :: MonadIO m => Enumerator Int m b
+getNumberEnum (Continue k) = do
+    x <- liftIO getLine
+    if x == "q"
+        then continue k
+        else k (Chunks [read x]) >>== getNumberEnum
+getNumberEnum step = returnI step
+
+

First, notice that we check which constructor was passed, and only perform any actions if it was Continue. If it was Continue, we get the line of input from the user. If the line is "q" (our indication to stop feeding in values), we do nothing. You might have thought that we should pass an EOF. But if we did that, we'd be preventing other data from being sent to this iteratee. Instead, we simply return the original Step value.

+

If the line was not "q", we convert it to an Int via read, create a Stream value with the Chunks datatype, and pass it to k. (If we wanted to do things properly, we'd check if x is really an Int and use the Error constructor; I leave that as an exercise to the reader.) At this point, let's look at type signatures:

+
k (Chunks [read x]) :: Iteratee Int m b
+
+

If we simply left off the rest of the line, our program would typecheck. However, it would only ever read one value from the command line; the >>== getNumberEnum causes our enumerator to loop.

+

One last thing to note about our function: notice the b in our type signature.

+
getNumberEnum :: MonadIO m => Enumerator Int m b
+
+

This is saying that our Enumerator can feed Ints to any Iteratee accepting Ints, and it doesn't matter what the final output type will be. This is in general the way enumerators work. This allows us to create drastically different iteratees that work with the same enumerators:

+
intsToStrings :: (Show a, Monad m) => Iteratee a m String
+intsToStrings = (unlines . map show) `fmap` consume
+
+

And then both of these lines work:

+
run_ (getNumberEnum $$ sum6) >>= print
+run_ (getNumberEnum $$ intsToStrings) >>= print
+
+

Exercises

+
  • Write an enumerator that reads lines from stdin (as Strings). Make sure it works with this iteratee:

    +
    printStrings :: Iteratee String IO ()
    +printStrings = do
    +    mstring <- head="" case="" mstring="" of="" Nothing="" -="">return ()
    +        Just string -> do
    +            liftIO $ putStrLn string
    +            printStrings
    +
  • +
  • Write an enumerator that does the same as above with words (ie, delimit on any whitespace). It should work with the same Iteratee as above.

  • +
  • Do proper error handling in the getNumberEnum function above when the string is not a proper integer.

  • +
  • Modify getNumberEnum to pull its input from a file instead of stdin.

  • +
  • Use your modified getNumberEnum to sum up the values in two different files.

  • +
+

Summary

+
  • An enumerator is a step transformer: it feeds data into an iteratee to produce a new iteratee with an updated state.

  • +
  • Multiple enumerators can be fed into a single iteratee, and we finally use the run and run_ functions to extract results.

  • +
  • We can use the $$, >>== and ==<< operators to apply an enumerator to an iteratee.

  • +
  • When writing an enumerator, we only feed data to an iteratee in the Continue state; Yield and Error already represent final values.

  • +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/enumerators-tutorial-part-3.html b/public/blog/2010/10/enumerators-tutorial-part-3.html new file mode 100644 index 00000000..05fd387b --- /dev/null +++ b/public/blog/2010/10/enumerators-tutorial-part-3.html @@ -0,0 +1,152 @@ + Enumerators Tutorial Part 3: Enumeratee +

Enumerators Tutorial Part 3: Enumeratee

October 7, 2010

GravatarBy Michael Snoyman

This content is now part of the Yesod book. It is recommended to read there, since the content is more up-to-date.

+

This is part 3 of a series of tutorials on the enumerator package. As usual, the code for this part of the tutorial is available as a github gist.

+

Generalizing getNumberEnum

+

In part 2 of this series, we created a getNumberEnum function with a type signature:

+
getNumberEnum :: MonadIO m => Enumerator Int m b
+
+

If you don't remember, this means getNumberEnum produces a stream of Ints. In particular, our getNumberEnum function read lines from stdin, converted them to ints and fed them into an iteratee. It stopped reading lines when it saw a "q".

+

But this functionality seems like it could be useful outside the realm of Ints. We may like to deal with the original Strings, for example, or Bools, or a bunch of other things. We could easily define a more generalized function which simply doesn't do the String to Int conversion:

+
lineEnum :: MonadIO m => Enumerator String m b
+lineEnum (Continue k) = do
+    x <- liftIO getLine
+    if x == "q"
+        then continue k
+        else k (Chunks [x]) >>== lineEnum
+lineEnum step = returnI step
+
+

Cool, let's plug this into our sumIter function (I've renamed the sum6 function from the previous two parts):

+
lineEnum $$ sumIter
+
+

Actually, that doesn't type check: lineEnum produces Strings, and sumIter takes Ints. We need to modify one of them somehow.

+
sumIterString :: Monad m => Iteratee String m Int
+sumIterString = Iteratee $ do
+    innerStep <- runIteratee sumIter
+    return $ go innerStep
+  where
+    go :: Monad m => Step Int m Int -> Step String m Int
+    go (Yield res _) = Yield res EOF
+    go (Error err) = Error err
+    go (Continue k) = Continue $ \strings -> Iteratee $ do
+        let ints = fmap read strings :: Stream Int
+        step <- runIteratee $ k ints
+        return $ go step
+
+

What we've done here is wrap around the original iteratee. As usual, we first need to unwrap the Iteratee constructor and the monad to get at the heart of the Step value. Once we have that innerStep value, we pass it to the go function, which simply transforms that values in the Stream value from Strings to Ints.

+

Even more general

+

Of course, it would be nice if we could apply this transformation to any iteratee. To start with, let's just pass the inner iteratee and the mapping function as parameters.

+
mapIter :: Monad m => (aOut -> aIn) -> Iteratee aIn m b -> Iteratee aOut m b
+mapIter f innerIter = Iteratee $ do
+    innerStep <- runIteratee innerIter
+    return $ go innerStep
+  where
+    go (Yield res _) = Yield res EOF
+    go (Error err) = Error err
+    go (Continue k) = Continue $ \strings -> Iteratee $ do
+        let ints = fmap f strings
+        step <- runIteratee $ k ints
+        return $ go step
+
+

We could call this like:

+
run_ (lineEnum $$ mapIter read sumIter) >>= print
+
+

Nothing much to see here, it's basically identical to the previous version. What's funny is that enumerator comes built in with a map function to do just this, but it has a significantly different type signature:

+
map :: Monad m => (ao -> ai) -> Enumeratee ao ai m b
+
+

since:

+
type Enumeratee aOut aIn m b = Step aIn m b -> Iteratee aOut m (Step aIn m b)
+
+

that's equivalent to:

+
map :: Monad m => (aOut -> aIn) -> Step aIn m b -> Iteratee aOut m (Step aIn m b)
+
+

What's with all this extra complication in type signature? Well, it's not necessary for map itself, but it is necessary for a whole bunch of other similar functions. But let's focus on this map for a second so we don't get lost: the first argument is the same old mapping function we had before. The second argument is a Step value. This isn't really so surprising: in our mapIter, we took an Iteratee with the same parameters, and we internally just unwrapped it to a Step.

+

But what's happening with that return value? Remembering the meanings for all these datatypes, it's an Iteratee which will be fed a stream of aOuts and return a Step (aka, a new iteratee, right?). This kind of makes intuitive sense: we've introduced a middle man which accepts input from one source and transforms a Step to a newer state.

+

But now perhaps the trickiest part of the whole thing: how do we actually use this map function? It turns out that an Enumeratee is close enough in type signature to an Enumerator that we can just do:

+
map read $$ sumIter
+
+

But the type signature on that turns out to be a little bit weird:

+
Iteratee String m (Step Int m Int)
+
+

Remembering that an Iteratee is just a wrapped up Step, what we've got here is an iteratee that takes Strings and returns an Iteratee, which in turn takes Ints and produces an Int. Having this fancy result allows us to do one of our great tricks with iteratees: plug in data from multiple sources. For example, we could plug some Strings into this whole ugly thing, run it, get a new iteratee which takes Ints, feed that some Ints and get an Int result.

+

(If all that went over your head, don't worry. I won't be talking about that kind of stuff any more.)

+

But often times, we don't need all of that power. We just want to stick our enumeratee onto our iteratee and get a new iteratee. In our case, we want to attach our map onto the sumIter to produce a new iteratee that takes Strings and returns Ints. In order to do that, we need a function like this:

+
unnest :: Monad m => Iteratee String m (Step Int m Int) -> Iteratee String m Int
+unnest outer = do -- using the Monad instance of Iteratee
+    inner <- outer -- inner :: Step Int m Int
+    go inner
+  where
+    go (Error e) = throwError e
+    go (Yield x _) = yield x EOF
+    go (Continue k) = k EOF >>== go
+
+

We can then run our unholy mess with:

+
run_ (lineEnum $$ unnest $ map read $$ sumIter) >>= print
+
+

And actually, the unnest function is available in Data.Enumerator, and it's called joinI. So we should really write:

+
run_ (lineEnum $$ joinI $ map read $$ sumIter) >>= print
+
+

Skipping

+

Let's write a slightly more interesting enumeratee: this one skips every other input value.

+
skip :: Monad m => Enumeratee a a m b
+skip (Continue k) = do
+    x <- head
+    _ <- head -- the one we're skipping
+    case x of
+        Nothing -> return $ Continue k
+        Just y -> do
+            newStep <- lift $ runIteratee $ k $ Chunks [y]
+            skip newStep
+skip step = return step
+
+

What's interesting about the approach here is how similar it looks to an Enumerator. We're doing a lot of the same things: checking if the Step value is a Continue; if it's not, then simply return it. Then we capitalize on the Iteratee Monad instance, using the head function to pop two values out of the stream. If there's no more data, we return the original Continue value: just like with an Enumerator, we don't give an EOF so that we can feed more data into the iteratee later. If there is data, we pass it off to the iteratee, get our new step value and then loop.

+

And what's cool about enumeratees is we can chain these all together:

+
run_ (lineEnum $$ joinI $ skip $$ joinI $ map read $$ sumIter) >>= print
+
+

Here, we read lines, skip every other input, convert the Strings to Ints and sum them.

+

Real life examples: http-enumerator package

+

I started working on these tutorials as I was working on the http-enumerator package. I think the usage of enumeratees there is a great explanation of the benefits they can offer in real life. There are three different ways the response body can be broken up:

+
  • Chunked encoding. In this case, the web server gives a hex string specifying the length of the next chunk and then that chunk. At the end, it sends a 0 to indicate the end of that response.

  • +
  • Content length. Here, the web server sends a header before any of the body is sent specifying the total length of the body.

  • +
  • Nothing at all. In this case, the response body lasts until an end-of-file.

  • +
+

In addition, the body may or may not be GZIP compressed. We end up with the following enumeratees, each with type signature Enumeratee ByteString ByteString m b: chunkedEncoding, contentLength and ungzip. We then get to do something akin to:

+
let parseBody x =
+        if ("transfer-encoding", "chunked") `elem` responseHeaders
+            then joinI $ chunkedEncoding $$ x
+            else case mlen of
+                    Just len -> joinI $ contentLength len $$ x
+                    Nothing -> x -- no enumeratee applied at all
+let decompress x =
+        if ("content-encoding", "gzip") `elem` responseHeaders
+            then joinI $ ungzip $$ x
+            else x
+run_ $ socketEnumerator $$ parseBody $ decompress $ bodyIteratee
+
+

We create a chain: the data from the server is fed into the parseBody function. In the case of chunked encoding, the data is processed appropriately and then headers are filtered out. If we are dealing with content length, then only the specified number of bytes are read. And in the case of neither of those, parseBody is a no-op.

+

Whatever the case may be, the raw response body is then fed into decompress. If the body is GZIPed, then ungzip inflates it, otherwise decompress is a no-op. Finally, the parsed and inflated data is fed into the user-supplied bodyIteratee function. The user remains blissfully unaware of any steps the data took to get to him/her.

+

Exercises

+
  • Write an enumeratee which takes hex chars (eg, "DEADBEEF") to Word8s. Its type signature should be Enumeratee Char Word8 m b.

  • +
  • Write the opposite enumeratee, eg Enumeratee Word8 Char m b.

  • +
  • Create a quickcheck property that ensures that these two functions work correctly.

  • +
+

Conclusion

+
  • Enumeratees are the pipes connecting enumerators to iteratees.

  • +
  • The strange type signature of an Enumeratee hides a lot of possible power. Especially notice how similar their type signatures are to Enumerators.

  • +
  • You can merge an Enumeratee into an Iteratee with joinI $ enumeratee $$ iteratee.

  • +
  • Don't forget that you can use the Monad instance of Iteratee when creating your own enumeratees.

  • +
  • You can always compose multiple enumeratees together, such as in http-enumerator.

  • +
+

This concludes the three parts of the tutorial that I'd planned. If people had particular questions or topics they wanted me to cover, just leave a comment or send me an email.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/forms-chapter-written.html b/public/blog/2010/10/forms-chapter-written.html new file mode 100644 index 00000000..21fc5aa2 --- /dev/null +++ b/public/blog/2010/10/forms-chapter-written.html @@ -0,0 +1,58 @@ + Forms Chapter Written +

Forms Chapter Written

October 12, 2010

GravatarBy Michael Snoyman

Special service announcement: please fill out the Haskellers.com survey. That is all ;).

+

I just wanted to give a heads-up that the forms chapter of the Yesod book is up. This is still mostly un-proofread, but contains a good amount of information and should be enough to get you started on forms. I'd appreciate feedback on where it could be further clarified. (By the way, that goes for the other chapters as well.)

+

Schrödinger's applicative cat

+

While going over this chapter with my wife, I was trying to figure out a good analogy for applicatives. I'm not sure if I just explained why monads are like burritos, but it's cute enough to put up anyway.

+

We all know about evil Mr. Schrödinger (is it Dr. Schrödinger?) who locked his cat in a bag with some poison. We're not sure if at any point in time the cat is alive or dead. (Hint: this is like a Maybe.) So in Haskell terms, we might have:

+
data Cat = Cat { pounds :: Int }
+data Bag a = Dead | Alive a
+
+

Now, we're not sadists here (besides locking cats up in their bags with poison), so we decide we would like to feed this perhaps-living cat. So we write up our little cat-feeding function:

+
data Mouse = Mouse
+feedCat :: Mouse -> Cat -> Cat
+feedCat Mouse (Cat oldWeight) = Cat $ oldWeight + 1
+
+

But time comes to feed the cat, and we realize that our cat lives in a bag! In other words, our cat (call him whiskers) is:

+
whiskers :: Bag Cat
+
+

We need some way to send the food into the bag. We'll call that intoBag:

+
intoBag :: (a -> b) -> Bag a -> Bag b
+intoBag f Dead = Dead
+intoBag f (Alive a) = Alive $ f a
+
+

And now we can feed whiskers with:

+
intoBag (feedCat Mouse) whiskers
+
+

All is well and good, until we realize Mr. Schrödinger decided he wanted to put the mouse in a bag as well:

+
squeaker :: Bag Mouse
+
+

We try out intoBag function, and realize the only thing we can do is this:

+
intoBag feedCat squeaker :: Bag (Cat -> Cat)
+
+

We need some way of feeding a mouse that may be alive to a cat that may be alive. Firstly, let's think about the outcome of these things:

+
  • If both the mouse and the cat are alive, then the cat can eat the mouse. The cat will then still be alive. (You can guess what happens to the mouse.)

  • +
  • If the cat is dead, well, the cat's dead.

  • +
  • If the mouse is dead, the cat doesn't have any food, so he dies too.

  • +
+

Fairly gruesome. But we still haven't figured out a way to do this! Turns out we need a combineBags function:

+
combineBags :: Bag (a -> b) -> Bag a -> Bag b
+combineBags (Alive f) (Alive a) = Alive $ f a
+combineBags _ _ = Dead
+
+

And then we can feed whiskers:

+
intoBag feedCat squeaker `combineBags` whiskers
+
+

Assuming, of course, he's still alive.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/haskellers-and-openid.html b/public/blog/2010/10/haskellers-and-openid.html new file mode 100644 index 00000000..5625331a --- /dev/null +++ b/public/blog/2010/10/haskellers-and-openid.html @@ -0,0 +1,48 @@ + Haskellers.com and OpenID +

Haskellers.com and OpenID

October 5, 2010

GravatarBy Michael Snoyman

I mentioned about two weeks ago on the web-devel list that I wanted to have a centralized site for Haskellers to put up professional profiles and for employers to not only find candidates, but also have confidence that Haskell is a language with a strong community of talented people.

+

I asked for feedback, and got a lot of it. I think the thing most demanded by the community was OpenID logins. I'm happy to say that we now have that, and frankly not much else. The reason is that I've spent the past two weeks working towards the goal of OpenID logins; the rest of this post is the explanation of why. Now that I've finally gotten that problem tackled, I can start implementing the cool features we want in Haskellers (you know, like a website).

+

authenticate package, 2 weeks ago

+

You might be surprised that I'm complaining about OpenID support; after all, I wrote the authenticate package, which has OpenID support built in. The problem is that it only supported OpenID version 1, and most of the big OpenID providers (Google, Yahoo!) have moved on to version 2.

+

I've been meaning to add support for OpenID 2 for a while, but I've kind of had an easy out with RPXNow; they are a proprietary site that makes it easy to implement 3rd-party login with a number of different providers. However, I didn't feel like going the proprietary route at all on Haskellers, and decided to bite the bullet and get OpenID 2 support done right.

+

openid package

+

Trevor Elliott put together an openid package which would seem to complement authenticate perfectly: it supports OpenID 2 but not 1. Unfortunately, I was never able to get it to work right out of the box. It seemed to have some issues with HTTPS support; if I'm not mistaken, there was an API change in the HTTP package that caused openid's HTTPS support to fail.

+

This brought up another annoyance I've had: a lack of a good HTTP library in Haskell. So I decided that while I was on this crusade of providing proper OpenID 2 support, I may as well pick up a few quests as well. The result is http-enumerator. I'm quite happy with it; some of its main features are:

+
  • An incredibly straight-forward API.

  • +
  • Built in HTTPS support using either OpenSSL or Vincent Hanquez's tls package.

  • +
  • By building on top of the enumerator package, you can do nice things like constant-memory download to a file. Working on this also inspired me to write two tutorials on the enumerator package (third one is in the works).

  • +
+

I was then able to gut out the HTTP library dependency in the openid package and replace it with http-enumerator. Using this, I could now log in to Yahoo!, but Google support did not work.

+

Associations

+

It turns out the Google issue had to do with associations. For those not familiar: there are two ways you can check if a user really logged in with an OpenID provider: set up an association in advance which uses some cryptographic techniques to allow you to check a signature, or simply ask the provider after the user comes back to your site if the login was real. Associations support ended up causing three problems for me:

+
  • For some reason it wasn't working with Google, as mentioned above.

  • +
  • It introduced a dependency on the OpenSSL library. I'm trying to make Yesod have as few system dependencies as possible.

  • +
  • It depends on the AES package, which has an outstanding issue with 32 bit systems. This is problematic, since many VPS servers install 32 bit OSes to conserve memory.

  • +
+

Frankenstein

+

OpenID 2 is a complicated protocol, and Trevor got all of the complicated stuff (XRDS, for example) handled properly. So instead of reinventing the wheel, I decided to simply pull the necessary code directly into the authenticate package and remove the associations support. At first, this resulted in an extraneous Web.Authenticate.OpenId2 module, which seemed a bit strange.

+

After some more massaging, I was able to combine the OpenID 1 and 2 support into a single interface. This means file are downloaded once and checked for both OpenID 1 and 2 support, and everything happens transparently for the user. I've released this as authenticate 0.6.6, and the cool thing is it has the exact same API as before, so if you've been using this package, you just got OpenID 2 support for free.

+

yesod-auth 0.1.0

+

I also just uploaded a new version of the yesod-auth. For those not aware: the main yesod package, through version 0.5.*, includes a Yesod.Helpers.Auth module. Starting from the next major release of Yesod, this will be removed, in favor of having this functionality in a separate package.

+

This release just offers some minor enhancements to the API, such as replacing defaultRoute with loginRoute and logoutRoute. It's worth checking out this package if you're using Yesod, especially if you need to implement a custom authentication scheme.

+

Back to Haskellers

+

Anyway, that officially clears my immediately pending coding queue, so I can start working properly on Haskellers tomorrow. Right now, it's just a basic site where you log in and enter a few fields of information. I'm planning on adding:

+
  • A skills section users can fill out.

  • +
  • Email address validation.

  • +
  • Recaptcha protection for account creation and viewing user email addresses.

  • +
  • A verified user feature, meaning some human has verified you are who you claim to be.

  • +
  • A full RESTful, JSON-based API for querying data from the site. I'm hoping that we, as a community, can start trying to create more interoperative services like this.

  • +
+

There'll be some more stuff as well, but if you have any thoughts on what you'd like to see in the site, please let me know!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/invertible-monads-exceptions-allocations.html b/public/blog/2010/10/invertible-monads-exceptions-allocations.html new file mode 100644 index 00000000..e265115b --- /dev/null +++ b/public/blog/2010/10/invertible-monads-exceptions-allocations.html @@ -0,0 +1,209 @@ + Invertible monads for exception handling and memory allocations +

Invertible monads for exception handling and memory allocations

October 15, 2010

GravatarBy Michael Snoyman

tldr

+

I've written a MonadInvertIO typeclass, instances for a bunch of the standard monad transformers, and a full test suite. It's currently living in my neither github repo, but after some more testing I'll probably release it as its own package. You can see the code and tests.

+

The rest of this post describes the problems with MonadCatchIO, builds motivation for a new solution through the identity, writer, error and reader monads, and finally presents a new approach. You should feel free to skip around however much you want.

+

The problem

+

I was rather dismayed to see Haskellers spitting out PoolExhaustedExceptions a few days after launch. That exception gets thrown when trying to allocate a database connection from the connection pool. I knew it wasn't simply setting the connection pool size too small: once the errors started, they continued until I did a hard process restart. Rather, connections were leaking.

+

I wrote a bunch of debug code to isolate the line where the connections were being allocated and not returned (the code is still live on Haskellers, just grep the codebase for debugRunDB), and eventually I traced it to a line looking like this:

+
runDB $ do
+    maybeUserName <- isThereAUserName
+    case maybeUserName of
+        Just username -> lift $ redirect RedirectTemporary $ UserR username
+        Nothing -> return ()
+
+

To the uninitiated: run a database action to see if the user has a username, and if so redirect to his/her canonical URL. After a little tracing, I realized the issue was this:

+
  • Yesod uses a specialized Error monad to allow short-circuiting, for example in cases of redirecting.

  • +
  • The Persistent package uses the MonadCatchIO-transformer's finally function to ensure database connections are returned to the pool, even in the presence of exceptions.

  • +
  • The MonadCatchIO family of packages are completely broken :(. For example, finally correctly handles exceptions and normal return, but never calls cleanup code on short-circuiting (ie, calling throwError).

  • +
+

I wrote an email to the cafe explaining the flaws in MonadCatchIO in detail. Essentially, one was to fix this is to create a new typeclass for the bracket function. However, it's always bothered me that we need to write all of these special functions to handle MonadIOs: it seems that it should be possible to simply "invert" the monads. The rest of this post discusses that inversion.

+

Inverting IdentityT

+

Whenever trying to write something generalized for a whole bunch of monads, it's great to start with IdentityT. Let's say we have an action and some cleanup code:

+
action :: IdentityT IO ()
+action = liftIO (putStrLn "action") >> error "some error occured"
+
+cleanup :: IdentityT IO ()
+cleanup = liftIO $ putStrLn "cleanup"
+
+

We want to run action, and then regardless of the presence of exceptions, call cleanup. This is a perfect use case for finally, but in Control.Exception it's defined as:

+
finally :: IO a -> IO b -> IO a
+
+

In order to use this, we need to force IdentityT IO to look like a IO. Well, in this case, it's rather straight-forward.

+
runSafely = IdentityT $
+    runIdentityT action `finally` runIdentityT cleanup
+
+

Fairly simple: unwrap the IdentityT constructor, call finally, and then wrap it up again.

+

What about WriterT?

+

Well, inverting a monad that does nothing is not very impressive. Let's try it out on WriterT:

+
actionW, cleanupW :: WriterT [Int] IO ()
+actionW = liftIO (putStrLn "action") >> tell [1]
+cleanupW = liftIO (putStrLn "cleanup") >> tell [2]
+runSafelyW = WriterT $ runWriterT actionW `finally` runWriterT cleanupW
+
+

That turned out to be incredibly easy. Let's see what happens when we run this:

+
> runWriterT runSafelyW >>= print
+action
+cleanup
+((),[1])
+
+

Wait a second, what about tell [2]? Well, the return value from the cleanup argument to finally gets ignored, and therefore so does anything we tell in there. This becomes more obvious if we expand the calls to tell:

+
actionW' = WriterT (putStrLn "action" >> return ((), [1]))
+cleanupW' = WriterT (putStrLn "cleanup" >> return ((), [2]))
+
+runSafelyW' = WriterT $ runWriterT actionW' `finally` runWriterT cleanupW'
+-- the same as
+runSafelyW'' = WriterT $ finally
+    (runWriterT $ WriterT (putStrLn "action" >> return ((), [1])))
+    (runWriterT $ WriterT (putStrLn "cleanup" >> return ((), [2])))
+-- runWriterT . WriterT == id, so this reduces to
+runSafelyW''' = WriterT $ finally
+    (putStrLn "action" >> return ((), [1]))
+    (putStrLn "cleanup" >> return ((), [2]))
+
+

This may or may not be what you want (an argument could be made either way), but let's see the next example before you decide that this is the wrong approach.

+

ErrorT

+

It turns out the code here is nearly identical again:

+
actionE, cleanupE :: ErrorT String IO ()
+actionE = do
+    liftIO $ putStrLn "action1"
+    throwError "throwError1"
+    liftIO $ putStrLn "action2"
+cleanupE = liftIO (putStrLn "cleanup") >> throwError "throwError2"
+runSafelyE = ErrorT $ runErrorT actionE `finally` runErrorT cleanupE
+
+

As a quick refresher: throwError "short-circuits" the remainder of the computation. For example, "action2" will never be printed. So what's the output of this thing

+
> runErrorT runSafelyE >>= print
+action1
+cleanup
+Left "throwError1"
+
+

The "throwError2" never shows up, but the cleanup does. Just to stress the point, let's modify this ever so slightly and remove the first throwError:

+
actionE2 :: ErrorT String IO ()
+actionE2 = do
+    liftIO $ putStrLn "action1"
+    liftIO $ putStrLn "action2"
+runSafelyE2 = ErrorT $ runErrorT actionE2 `finally` runErrorT cleanupE
+
+

This time, the output is:

+
> runErrorT runSafelyE2 >>= print
+action1
+action2
+cleanup
+Right ()
+
+

Wait a second: why didn't I get a throwError2 this time? Once again, this has to do with ignoring return values from a cleanup function. Let's desugar again and look at what's happening under the surface:

+
runSafelyE' = ErrorT $ finally
+    (do
+        a <- fmap Right $ putStrLn "action1"
+        case a of
+            Left e -> return $ Left e
+            Right a' -> fmap Right $ putStrLn "action2"
+    )
+    (do
+        a <- fmap Right $ putStrLn "cleanup"
+        case a of
+            Left e -> return $ Left e
+            Right a' -> return $ Left "throwError2"
+    )
+
+

This is just a straight mechanical translation of runSafelyE2 using the definition of ErrorT. We can now remove some of the noise, since we know at compile time whether we are returning Right or Left:

+
runSafelyE'' = ErrorT $ finally
+    (do
+        putStrLn "action1"
+        putStrLn "action2"
+        return $ Right ()
+    )
+    (do
+        putStrLn "cleanup"
+        return $ Left "throwError2"
+    )
+
+

We can see that the throwError2 is simply a result of the return value of the cleanup function, which gets ignored by finally. Once again, like in WriterT, it seems that our abstraction is destroying the power of our monads. This is arguably true, but it's also the only correct way to deal with the situation. Let's say that we changed our action function to now be:

+
(do
+    putStrLn "action1"
+    putStrLn "action2"
+    returnsLeftOrRight :: IO (Either String ())
+)
+
+

If returnsLeftOrRight results in Left "throwError3", then we would want our function to print action1, action2, cleanup and result in Left "throwError3". In order to call the cleanup function at all, however, it can't be dependent on the return value of action. This is exactly where MonadCatchIO failed: over there, if the action returns a Left, it short-circuits the cleanup function from running.

+

In theory, you could make the argument that we should follow this logic train:

+
  • If the action returns Left, run through the cleanup code and return the action's return value, ignoring the cleanup return value.

  • +
  • Otherwise, if the cleanup code returns Left, return the cleanup code's Left.

  • +
  • Otherwise, return the action's Right value.

  • +
+

But this is adding a lot of complexity, and it seems to me to work against us. We've seen now with both WriterT and ErrorT that the obvious definitions simply ignore the result of the cleanup function, just like when you use finally in the IO monad. This is the way I've implemented my code, and leads to one important caveat:

+

Excluding IO effects themselves, never rely on monadic side effects from cleanup code.

+

This may seem to make the whole exercise futile, but I think the majority of the time when we would want to use this approach, it is simply to perform cleanup that must occur in the IO monad.

+

ReaderT

+

I've always considered the reader monad to be the simplest of the monads. I find it ironic that reader (and state) introduce a major complication in our approach. We'll define out action and cleanup pretty much as before:

+
actionR, cleanupR :: ReaderT String IO ()
+actionR = ask >>= liftIO . putStrLn
+cleanupR = liftIO $ putStrLn "cleanup"
+
+

Previously, we had runWriterT actionW :: IO (...) and runErrorT actionE :: IO (...). However, reader doesn't work that way; instead: runReaderT actionR :: r -> IO (...). So we have to slightly modify our runSafelyR function to deal with the parameter:

+
runSafelyR = ReaderT $ \r ->
+    runReaderT actionR r
+    `finally`
+    runReaderT cleanupR r
+
+

For our little case here, it's pretty simple to account for this extra parameter. It's a little bit more complicated in the state monad (which I won't cover here, I've bored you enough already), but the point where it really bites is in MonadInvertIO itself.

+

MonadInvertIO +

+ +

Hopefully the above four monad examples showed that it's entirely possible to turn a transformer inside out. If this makes sense for a single layer (a wraps b becomes b around a), it should makes sense for any number of layers (abc becomes cba). This is similar to the concept of the MonadTrans and MonadIO typeclasses: the former allows us to lift one level, while the latter lifts all the way to IO.

+

I originally started with the same premise of having a MonadInvert typeclass to invert one monadic layer and MonadInvertIO to invert to the IO layer, but due to technicalities it would be tedious to work this way. Plus, I'm not sure if there is much use for this functionality outside the realm of MonadInvertIO. In any event, the typeclass is:

+
class Monad m => MonadInvertIO m where
+    data InvertedIO m :: * -> *
+    type InvertedArg m
+    invertIO :: m a -> InvertedArg m -> IO (InvertedIO m a)
+    revertIO :: (InvertedArg m -> IO (InvertedIO m a)) -> m a
+
+

This is using type families. InvertedIO gives the "inverted" representation of our monad, and InvertedArg gives the argument to our function. This argument is the complication I alluded to in the reader section above. For monads like error and writer, InvertedArg is not necessary.

+

invertIO takes our monadic value and returns a function that takes our InvertedArg and returns something in the IO monad. This should look familiar from the reader section above. revertIO simply undoes that action.

+

As an easy example, let's see the IO instance:

+
instance MonadInvertIO IO where
+    newtype InvertedIO IO a = InvIO { runInvIO :: a }
+    type InvertedArg IO = ()
+    invertIO = const . liftM InvIO
+    revertIO = liftM runInvIO . ($ ())
+
+

In this case, InvertedIO doesn't do anything, which isn't really surprising (this instance is, after all, just a wrapper). We don't need any arguments, so InvertedArg is (). invertIO and revertIO are also just dealing with the typing requirements.

+

To get a better idea of how these things work, let's look at IdentityT:

+
instance MonadInvertIO m => MonadInvertIO (IdentityT m) where
+    newtype InvertedIO (IdentityT m) a =
+        InvIdentIO { runInvIdentIO :: InvertedIO m a }
+    type InvertedArg (IdentityT m) = InvertedArg m
+    invertIO = liftM (fmap InvIdentIO) . invertIO . runIdentityT
+    revertIO f = IdentityT $ revertIO $ liftM runInvIdentIO . f
+
+

IdentityT itself does not add anything to the representation of the data, but the monads underneath it might. Therefore, its InvertedIO associated type references the InvertedIO of the underlying monad. We do the same with InvertedArg. invertIO and revertIO simply do some wrapping and unwrapping.

+

Some of the instances can get a bit tricky, but it's all built on these principles. Feel free to explore the code, I don't want anyone reading this post to die of boredom.

+

Using the typeclass

+

The final piece in the puzzle is how to actually use this typeclass in real life. A great, straightforward example is a new definition of finally:

+
import qualified Control.Exception as E
+finally :: MonadInvertIO m => m a -> m b -> m a
+finally action after = revertIO $ \a ->
+    invertIO action a
+    `E.finally`
+    invertIO after a
+
+

This is the general model for all uses of the library. We need to pair up revertIO and invertIO. Like in the reader example, we end up with an argument (called a here), which we need to pass around every time we call invertIO. Once we've done that, we now have two actions living purely in the IO monad, so we can use E.finally on them.

+

In addition to exception handling, we can use this approach for memory allocation:

+
alloca :: (Storable a, MonadInvertIO m) => (Ptr a -> m b) -> m b
+alloca f = revertIO $ \x -> A.alloca $ flip invertIO x . f
+
+

As I said at the beginning, I have a test suite running to ensure the inversion code is working correctly. I'm going to test it out in more complicated situations, but I believe this could be used as a general solution to the recurring problem of functions requiring IO-specific arguments.

+

I'd appreciate feedback on this idea, especially if you find anything which seems to be flawed. I don't want to produce another broken finally function.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/10/yesod-0-6.html b/public/blog/2010/10/yesod-0-6.html new file mode 100644 index 00000000..c2c6d663 --- /dev/null +++ b/public/blog/2010/10/yesod-0-6.html @@ -0,0 +1,30 @@ + Yesod 0.6.0 Released +

Yesod 0.6.0 Released

October 26, 2010

GravatarBy Michael Snoyman

I'm very happy to announce a new release of the Yesod Web Framework. This release is accompanied by version bumps in Persistent and yesod-auth, as well as a massive reorganization of the content in the Yesod book, hopefully making the material easier to follow and more comprehensive. The main goal in this release has been pushing forward the stability of the Yesod API, paving the way for a 1.0 release in the near future. Some notable changes in this release are:

+
  • Changes to the session are visible immediately, instead of needing to wait until the next request. This has been a source of confusion for a few people, and it turns out the new implementation is simpler. I just wrote an email about it.

  • +
  • CSRF protection for forms. A bit more information on that in another email I wrote.

  • +
  • A major renaming of the widget functions for simplification. You can read the email I wrote about it.

  • +
  • Completely replaced MonadCatchIO with MonadInvertIO. The former had a flaw in its implementation of finally which allowed database connections to be leaked. The latter has a full test suite associated with it, and should be safe.

  • +
  • The Auth helper has been moved into the yesod-auth package (version 0.2 just released). This slims down the dependencies in Yesod while allowing us to have more freedom in changing the API. yesod-auth deviates from the previous Auth helper by providing a plugin interface, allowing anyone to add new backends.

  • +
  • Moved the Yesod.Mail module to the new mime-mail package, allowing its use outside of Yesod.

  • +
  • Added support for free-forms. In addition to the formlet-inspired, Applicative form API, we now have a second datatype (GFormMonad) which makes it easier to write custom-styled forms. I wrote a blog post about this already.

  • +
  • The jQuery date picker can now be configured through a Haskell datatype.

  • +
  • The Widget type alias has been removed; now the site template defines this.

  • +
  • mkToForm can be applied to a single datatype instead of all of your entities.

  • +
  • Added fiRequired to FieldInfo, making it easier to style your forms differently for required and optional fields. We also have a fieldsToDivs function for those who hate tables.

  • +
  • By default, sessions are tied to a specific IP address to prevent session hijacking. Under certain circumstances, such as proxied requests, this can cause a problem. There is now a sessionIpAddress method in the Yesod typeclass to disable this.

  • +
+

I'm very encouraged by the fact that most of these changes were incremental enhancements on features already present, and that even those often did not require breaking changes. My goal for the near future is to continue focusing on the Yesod book. I'm hoping to have the majority of the book written by the time we have a 1.0 release.

+

The code should be pretty stable: it's already running haskellers.com, so it's getting a fair amount of stress testing. If you find any bugs, let me know. And if you have any questions, ask away!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/11/atomic-field-modification-persistent.html b/public/blog/2010/11/atomic-field-modification-persistent.html new file mode 100644 index 00000000..961069bc --- /dev/null +++ b/public/blog/2010/11/atomic-field-modification-persistent.html @@ -0,0 +1,42 @@ + Atomic Field Modification in Persistent +

Atomic Field Modification in Persistent

November 16, 2010

GravatarBy Michael Snoyman

I'm happy to announce version 0.3.1 of persistent. Liam O'Connor pointed out to me that not only was there a huge performance gap in the previous method of doing mass modifications to values in persistent, but we were losing all of the wonderful ACIDity offered by our database.

+

The idea is pretty straight-forward: let's say you've got a Person entity:

+
Person
+    name String
+    age Int Add
+
+

That Add means it's now possible to do something like:

+
update personId [PersonAgeAdd 1]
+
+

to make someone 1 year older. Currently, the four verbs I've added are Add, Subtract, Multiply and Divide, though in theory others could be added as well. Let me know if there's demand.

+

Naming cleanup

+

One other thing. Let's review some of the words that we use in persistent:

+
  • Asc
  • +
  • Desc
  • +
  • Eq
  • +
  • Lt
  • +
  • Add
  • +
  • Divide
  • +
  • update
  • +
  • null
  • +
+

Hmm... something doesn't quite fit in here. To make things more consistent, I'm deprecating the last two. In the future, you should use Update instead of update, and Maybe instead of null. Besides the capitalization consistency, semantically null was the wrong word here: I tried very hard to make sure the behavior of a "nullable" field would closely match a Maybe value and not the hodge-podge of NULLity which exists in SQL. Plus, Persistent has nothing to do with SQL (in theory at least), so we shouldn't be using SQL-specific names.

+

For now, this will just print out a message during compile time that the names are deprecated. I'll eventually remove support for them entirely, but I'm not in any rush.

+

Minor Yesod release

+

Greg Weber keeps insisting that I make Yesod better, so I announce version 0.6.2. This is a minor release, only introducing two new functions: runFormTable and runFormDivs. These functions make it just slightly easier to have properly CSRF-protected forms, by building the whole widget up for you. No need to fiddle with %form!method=POST!action=@something@, this does it.

+

On a more general note, there's a problem of increased complexity due to polymorphic forms. In particular, error messages can get out of hand. I'm aware of the issue, and I'm trying to figure out a way to solve it. If anyone has any ideas, let me know. One thought that keeps coming to mind is removing the GHandler that's at the base of the GForm monad stack. My reasoning is that I don't think anyone is actually using the power, and it might (I repeat: might) simplify things.

+

If anyone is actually utilizing this feature as-is, please let me know. Basically, it allows you to perform arbitrary functions- such as database lookup- when constructing forms. This in theory is very cool: you can look up a list of authors to select from when designing a library book entry site, for instance. In practice, it's tedious to write code this way, and it can all be done more easily with a parameter to the formlet function with the list of authors.

+

Anyway, I'm still just at the brainstorming phase of this. Input is greatly appreciated.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/11/javascript-minification.html b/public/blog/2010/11/javascript-minification.html new file mode 100644 index 00000000..ee1e7a28 --- /dev/null +++ b/public/blog/2010/11/javascript-minification.html @@ -0,0 +1,37 @@ + Javascript Minification +

Javascript Minification

November 18, 2010

GravatarBy Michael Snoyman

We all know that serving the fastest web page possible is the "in" thing these days. We await with bated breath every piece of advice from Google, Yahoo! and Facebook. (Whatever you think of the policies of any of these companies, we have to admit they do handle insane volumes of traffic.)

+

One of these techniques is to shrink down our responses as much as possible. Yesod already helps out on this front: Hamlet and Cassius both produce very terse code, and the default scaffolded Yesod site concatenates together your CSS and Javascript code into single files that can be served with caching.

+

But the one piece of the puzzle that has been missing up until now has been minifying our Javascript. I put out a request for someone to address this, and Alan Zimmerman took up the challenge. The result is hjsmin. It is a very well tested piece of software, and seems by all accounts to be perfectly safe to use.

+

I wanted to plug it straight in to Haskellers.com, but soon realized there were some performance issues. Minifying the typical Javascript response of an individual page took about 150ms on my local system. That may not seem like much, but it will effectively kill any performance gains of using minification.

+

With that in mind, Alan and I started working on optimizing hjsmin. I have no doubt that the performance will be seeing some great strides in the future, but I'm impatient: I want to minify now!

+

Caching to the rescue

+

The solution is actually rather simple, and in fact is something I should have done a while ago. Let's review how Haskellers.com serves Javascript (CSS is the same):

+
  1. Run through all the handlers, which produce a bunch of Javascript code.

  2. +
  3. Concatenate all of that code together into a lazy bytestring. (By the way, under the surface we're using blaze-builder for even better performance.)

  4. +
  5. Get an MD5 hash of the content.

  6. +
  7. Store the Javascript in a file with the hashed name.

  8. +
  9. Insert a <script> tag into the resulting HTML page referencing that Javascript.

  10. +
+

Forget about minification for a second. Step four is pretty stupid: if a million people request my homepage, the same Javascript will be generated a million times, and the same file will be written a million times. What was I thinking? Let's replace step four with:

+
  1. Check if a file exists with the hashed named.

  2. +
  3. If not, write the content to the file.

  4. +
+

And suddenly instead of a million file writes, one file write and a million file existence checks (which are much cheaper). Note that this optimization works equally well for CSS. But also notice that writing content now only happens once. I'm going to modify the above process just a little bit more.

+
  1. Check if a file exists with the hashed named.

  2. +
  3. If not, write the minified content to the file.

  4. +
+

And voila! The expensive minification only happens once. The trick to remember is the filename hash is calculated based on the non-minified Javascript. This code is now running live on Haskellers. You can see the relevant code on Github.

+

Please let me know if you see any misbehaving Javascript, but I'm not too worried. Assuming all goes well, expect to see this code as part of the new Yesod site template.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/11/new-book-comments-system.html b/public/blog/2010/11/new-book-comments-system.html new file mode 100644 index 00000000..bfe97272 --- /dev/null +++ b/public/blog/2010/11/new-book-comments-system.html @@ -0,0 +1,23 @@ + New book comments system, and a new site design +

New book comments system, and a new site design

November 26, 2010

GravatarBy Michael Snoyman

One feature that's been requested for the Yesod book, and which I've wanted to add for a while, is paragraph-level comments like Real World Haskell. Now that I've migrated the book to an XML format, this feature was relatively easy to add. Please enjoy, and make good use of it! I'll also be adding in Atom feeds for the comments, and maybe even for book updates, so stay tuned.

+

One little note: the comments only work for the chapters of the book that have been fully translated to XML. Some of the chapters (like persistent) still contain a lot of content in Markdown, so the comments links do not appear on those paragraphs.

+

New Site Design

+

I'd like to make a request from the community now: I think it's time to add some more style to this website. If anyone has some new site design ideas, please send them over. All part of the preparing-for-one-point-oh thing.

+

Oh, and good job everyone on the "Please Break Yesod" campaign, I'm getting bug reports and lots of good API suggestions. Some people are really putting together some creative packages on top of Yesod, and I look forward to sharing them with you as they mature (the packages, not the people).

+

Technical details

+

In case anyone is interested, some minor details on how I implemented the comment system. I decided to just keep all the comments in memory in a TVar, since they are likely going to not take up much memory and be mostly read-only. I defined a simple datatype, and created some cereal instances to encode/decode with bytestrings.

+

Each time a comment is added, I update the TVar with the new comment and write it to file, using cautious-file to make sure I don't accidently the whole comments database. I will probably start doing regular backups of that file as well.

+

As usual, the whole site's code is available on Github, so check it out if you are interested.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/11/please-break-yesod.html b/public/blog/2010/11/please-break-yesod.html new file mode 100644 index 00000000..474ee886 --- /dev/null +++ b/public/blog/2010/11/please-break-yesod.html @@ -0,0 +1,24 @@ + Please Break Yesod +

Please Break Yesod

November 11, 2010

GravatarBy Michael Snoyman

I would like to start a new campaign, trying to break Yesod. I want us to find every bug, every quirk, every bad API choice, every piece of badly performing code. At the end of the day, Yesod should be a bloody mess on the floor. (Either that, or there just aren't any bugs in it at all... yeah, right.) And not just Yesod: I want us to shake down hamlet, persistent, authenticate, clientsession, everything. Let's even see if we can find bugs in underlying libraries, and why the hell not, let's attack GHC as well.

+

I don't plan on adding any new features before a 1.0. That doesn't mean Yesod is feature-complete: I'd like better i18n support, more persistent backends, some more high-level Javascript integration. And I'm sure for every feature I can think of that could be added, the community could come up with 100. But that's not my focus right now. Rigth now, I want to fix things.

+

From my side, the first major step to take is to write a thorough test suite for Yesod. (Thanks for Greg Weber for reminding me that this is sorely lacking.) For example, the 0.6 release had a bug in getMessage. That should have been caught before I even hit git commit. Unfortunately, there don't seem to be many tools to help in this quest.

+

There is now a web development Special Interest Group on Haskellers, and a discussion on testing tools. I strongly encourage everyone interested in this to join the group, follow the news feed and participate in the discussion.

+

Something More Concrete

+

For those of you looking for something concrete to get started on now, there's a code patch I'd like the community's help in reviewing. Matt Brown has made a sorely needed commit which transforms the Route associated data type from a type family to a data family. If you don't know what that means, it basically gets rid of some dreaded "not injective" compiler errors and let's us drop some unneeded parameters in a few places.

+

I'd love to apply this patch immediately, but I have a small problem: I wrote a patch like this before that started causing a bug in GHC to pop up (only GHC 6.12.1 if I remember correctly). I have not been able to reproduce that bug with Matt's code, but if anyone in the community experiences a problem, please let me know sooner rather than later.

+

I'm going to give till about Monday night (UTC+2) to hear back on this. If no one complains, I'll assume the code works perfectly. I'd also appreciate hearing success stories, so that I know I'm not just talking to thin air on this. If it works, this patch will introduce a breaking change, requiring a major version bump. This is a major enough change that I wouldn't feel comfortable including it in the 1.0 release, so we'd likely be looking at a Yesod 0.7. However, the code change is minor enough that almost all 0.6 code will compile again without a hitch.

+

Conclusion

+

I hope everyone will join me in this whack-a-bug frenzy on Yesod. Combined with some major documentation writing (and proofreading), a bit of performance tuning, and people complaining about bad API decisions, I think we can have a Yesod 1.0 release in the next few months that will meet the highest standards.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/11/wishlist.html b/public/blog/2010/11/wishlist.html new file mode 100644 index 00000000..7fabab05 --- /dev/null +++ b/public/blog/2010/11/wishlist.html @@ -0,0 +1,42 @@ + Wishlist +

Wishlist

November 25, 2010

GravatarBy Michael Snoyman

I'm going to describe some projects that are on my wishlist: given an infinite amount of time and an infinite number of snacks, I would write all of these. However, I do live in the real world, so I don't have time to write and maintain everything I'd like. I'm going to post my ideas here to

+
  • not lose them,

  • +
  • try to see which of these the community would be most interested in, and

  • +
  • see if anyone out there is interested in writing one of these him/herself.

  • +
+

If anyone wants to take a crack at one of these, let me know (either comments or email). I'd be more than happy to provide any guidance and support people want. I'll try to rate these projects with a difficulty level: 1 being you don't know monads yet, 10 being your name is Simon.

+

Warning: difficulty levels are made up on the spot and may be completely mistaken. Do not blame me if you stay awake three weeks on end trying to implement what I guessed would be an easy task.

+

wai-vhost

+

I emailed the web-devel list about this the other day. It's a very basic idea: provide a WAI middleware which routes requests to one of many different applications based on the Host request header, just like a webserver does. You can read my original email.

+

Difficulty level: 2.

+

web-extras

+

This could easily be split up into a bunch of different packages. The idea is to provide quality support for services like Gravatar, Disqus, Google Analytics, Recaptcha, and more. None of these are very complicated, it's just finding a balance between nice API and providing flexibility. A lot of this code already exists in the Haskellers website and just needs to be factored out.

+

Difficulty level: 3.

+

yesod-markdown

+

I don't natively provide support for Pandoc in Yesod because 1) it's a large dependency and 2) it's GPLed. However, it's often desireable to use Markdown syntax in a website, and it would be great to have that packaged up somewhere. This package would provide some Markdown datatype, form functions, automatic rendering to HTML, serialization to Persistent, etc.

+

Difficulty level: 3.

+

wai-unit

+

I want to start testing Yesod thoroughly, but I haven't written the test framework yet. This doesn't necessarily need to be a WAI-specific test framework: it would be even more useful if it used http-enumerator and spoke properly over a socket. But starting as WAI-only would probably be easier, and having a WAI-adapter would be incredibly convenient. The trick here is getting a nice, thorough, powerful API.

+

Difficulty level: 5.

+

attoparsec-text

+

I'm relying more and more on the text package, but unfortunately there isn't (to my knowledge) a nice parser combination library for it. It would be wonderful if someone wrote one with a goal towards the same performance characteristics of attoparsec. Almost certainly, a text-based one will be slower, but it would be a nice goal nonetheless.

+

If that gets written, the next thing I would want is an adapter for the enumerator package, but that would likely be fairly easy.

+

Difficulty level: 7.

+

Native XML parser with enumerator interface

+

I'm using libxml-enumerator for the Yesod book these days, and like it a lot. I've put together some basic combinators to ease the job of building up my data value, and will hopefully get those into a package soon. I liked this library so much, I tried to use it for work. One problem: I can't for the life of me get libxml working on Windows properly. (Any advice on getting Windows set up properly to build with pkg-config is welcome.)

+

I've worked around it with expat-enumerator, but it seems that namespace handling in that library is not as powerful. It's not a problem for me at the moment, but could be in the future. A native-Haskell parser (hint: built on attoparsec-text) would be very useful here. Also, it would be amazing to get line-number information in there too.

+

Oh, and I also think that a rendering library would be useful, but that's in the works already.

+

Difficulty level: 6.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/11/yesod-0-6-3.html b/public/blog/2010/11/yesod-0-6-3.html new file mode 100644 index 00000000..b88bbbb1 --- /dev/null +++ b/public/blog/2010/11/yesod-0-6-3.html @@ -0,0 +1,26 @@ + Yesod 0.6.3 Released +

Yesod 0.6.3 Released

November 21, 2010

GravatarBy Michael Snoyman

I don't normally make release announcements for minor release (ie, the third number in the version). But just because 0.6.3 is introducing some cool stuff, I thought I'd mention it here. And while I'm at it, I'll cover all the new features since 0.6.0 was released. In no particular order:

+
  • Matt Brown has added some better subsite support. In particular, the YesodSubRoute typeclass, addSubWidget and toMasterHandlerMaybe. Unfortunately, due to a bug in GHC 6.12, his more impressive patches can't be included yet.

  • +
  • Full GHC 7 support. GHC 7 changes quasi-quotation syntax to no longer use the dollar sign. Using some CPP hackery, Yesod will now compile on both 6.12 and 7. The scaffolded site also adapts appropriately.

  • +
  • The scaffolded site minifies Javascript using Alan Zimmerman's hjsmin. Both CSS and Javascript results are now only written to disk once.

  • +
  • The scaffolded site includes a widgetFile function. If you include $(widgetFile "name"), it will automatically include the Hamlet, Cassius and Julius templates named "name" if available.

  • +
  • runFormTable and runFormDivs provide some nicely packaged support for form submissions. It takes care of all the HTML and CSRF protection automatically.

  • +
  • Fixed some minor bugs in joinPath/splitPath affecting corner cases. Also fixed a cookie expire bug that had cookies expiring at the end of each session. (By the way, that's why Haskellers would always log you out immediately. That's been fixed now.)

  • +
  • fileField, maybeFileField, and atomLink function.

  • +
  • Support for blaze-builder 0.2 and network 2.3.

  • +
+

Please check it out, and let me hear your thoughts. I hope the moves we're making in these releases are helping to solidify the API and fill in the gaps. I'm still up in the air about whether there will be a 0.7 release or if we'll skip straight to 1.0.

+

One last comment: the book has had a massive internal restructuring to convert it to an XML format. I've done this for a number of reasons, but more importantly for you, this means I will more easily be able to generate PDF versions. I'm still in the process of converting some of the old markdown to the newer XML, but hopefully the process won't take too long. If you notice any bugs, please let me know.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/announcing-xml-enumerator.html b/public/blog/2010/12/announcing-xml-enumerator.html new file mode 100644 index 00000000..5ea8e4d2 --- /dev/null +++ b/public/blog/2010/12/announcing-xml-enumerator.html @@ -0,0 +1,68 @@ + Announcing XML Enumerator +

Announcing XML Enumerator

December 15, 2010

GravatarBy Michael Snoyman

I'm very happy to announce the first release of the xml-enumerator package. But first, a word from our sponsors: a large portion of the code in this package was written for work at my day job with Suite Solutions. We are a company that focuses on documentation services, especially using XML in general and DITA in particular. The company has graciously agreed to allow me to release this code as open source.

+

For those who are not yet aware, I'm a very big fan of John Millikan's enumerator package, and so it should come as no surprise that when I needed to pick an XML parsing library I turned to his work. I was surprised to find not one, but two librarys: libxml-enumerator and expat-enumerator, which each wrapped around the respective C libraries. libxml-enumerator is very difficult to get working on Windows systems, and expat-enumerator does not have support for XML namespaces or doctypes. (That may be fixed in the future.)

+

What's nice is that both of these packages rely upon the same datatypes, provided by the xml-types package (also written by John), and therefore can be mostly swapped out transparently. Unfortunately, there was no support for rendering XML using these datatypes, which was something I needed for the tool I was writing. As such, xml-enumerator is composed of three pieces:

+
  • A native XML parser based on attoparsec.
  • +
  • An XML renderer based on blaze-builder.
  • +
  • A set of parser combinators I developed for the Yesod book.
  • +
+

Let me give a brief introduction to each of these:

+

The parser

+

The parseBytes function provides an enumeratee to convert a stream of ByteStrings into a stream of Events. It follows mostly the same type signature as the parseBytesIO functions provided by libxml-enumerator and expat-enumerator, but since it does not call foreign code can live in any monad. There is one limitation: it requires the input to be in UTF8 encoding.

+

To overcome this, the Text.XML.Enumerator.Parse module also provides a detectUtf enumeratee, which will automatically convert UTF-16/32 (big and little endian) into UTF8, leaving UTF8 data unchanged. Therefore, to get a list of Events from a file, you could use:

+
run_ $ enumFile "myfile.xml" $$ joinI $ detectUtf $$ joinI $ parseBytes $$ consume
+
+

Ideally, I would have liked to make this parser work on a stream of Texts instead of ByteStrings, but unfortunately there is not yet an equivalent of attoparsec for Texts.

+

The renderer

+

The renderer is simply two enumeratees: renderBuilder converts a stream of Events into a stream of Builders (from the blaze-builder package). renderBytes uses renderBuilder and the blaze-builder-enumerator package to convert a stream of Events into a stream of optimally-sized ByteStrings, involving minimal buffer copying. In order to re-encode an XML file, for example, we could run:

+
withBinaryFile "output.xml" WriteMode $ \h ->
+    run_ $ enumFile "input.xml" $$ joinI $ detectUtf $$ joinI $ parseBytes
+        $$ joinI $ renderBytes $$ iterHandle h
+
+

Note that you could equally well use libxml-enumerator or expat-enumerator for the parsing, since they are all using the same Event type.

+

Parser combinators

+

The combinator library is meant for parsing simplified XML documents. It uses the simplify enumeratee to convert a stream of Events into SEvents. This essentially throws out all events besides element begin and end events, converts entities into text based on a function you provide it, and concatenates together adjacent contents.

+

Attribute parsing is handled via a AttrParser monad, which lets you deal with both required and optional attributes and whether parsing should fail when unexpected attributes are present. Tag parsing has three functions (tag, tag' and tag''... I'm horrible at naming things) that go from most-general to easiest-to-use, and there are two content parsing functions.

+

There are three combinators: choose selects the first successful parser, many runs a parser until it fails, and force requires a parser to run successfully. And finally, there are two convenience functions, parseFile and parseFile_, that automatically uses detectUtf, parseBytes and simplify for you. The Text.XML.Enumerator.Parse module begins with this sample program:

+
{-# LANGUAGE OverloadedStrings #-}
+ import Text.XML.Enumerator.Parse
+ import Data.Text.Lazy (Text, unpack)
+
+ data Person = Person { age :: Int, name :: Text }
+     deriving Show
+
+ parsePerson = tag' "person" (requireAttr "age") $ \age -> do
+     name <- content'
+     return $ Person (read $ unpack age) name
+
+ parsePeople = tag'' "people" $ many parsePerson
+
+ main = parseFile_ "people.xml" (const Nothing) $ force "people required" parsePeople
+
+

Given a people.xml file containing:

+
<?xml version="1.0" encoding="utf-8"?>
+ <people>
+     <person age="25">Michael</person>
+     <person age="2">Eliezer</person>
+ </people>
+
+

produces:

+
[Person {age = 25, name = "Michael"},Person {age = 2, name = "Eliezer"}]
+
+

Relation to some recent work

+

If you are wondering, the release of this package is very much related to some stuff I mentioned in my last blog post. In particular, I wrote a first version of blaze-builder-enumerator while working on the renderer for this package, and was working on demonstrating this package as a web service when I decided to stitch together WAI and xml-enumerator.

+

Conclusion

+

Since this is an initial release, there are very possibly some bugs in it, so please let me know if you find anything wrong. And I'd just like to give another thank you to my employers for contributing this to the community.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/announcing-yackage.html b/public/blog/2010/12/announcing-yackage.html new file mode 100644 index 00000000..b23eea21 --- /dev/null +++ b/public/blog/2010/12/announcing-yackage.html @@ -0,0 +1,46 @@ + Announcing Yackage: Your personal Hackage server for testing +

Announcing Yackage: Your personal Hackage server for testing

December 27, 2010

GravatarBy Michael Snoyman

I was speaking with my coworker Yitz about a project he's working on. Basically, he's going to end up with about 16 cabal packages that are not going to be deployed to Hackage, and wanted us to set up a Hackage server for our company to deploy these kinds of things. However, getting all the pieces of Hackage aligned properly for such a simple use case seemed a bit overkill.

+

I then realized that I had the exact same problem during Yesod development: before I make a major release, I usually end up with about 10-15 packages that are not yet live on Hackage. It gets to be a real pain when suddenly wai-extra is depending on network 2.3 and authenticate requires network 2.2, and suddenly I need to manually recompile 10 packages.

+

So I decided to write up a simple web service to act as a local Hackage server. It has no security (anyone with access can upload a package), doesn't build haddocks, doesn't show package descriptions, etc. All it does is:

+
  • Show a list of uploaded packages/versions
  • +
  • Links to the tarballs
  • +
  • Allows you to upload new versions, which will automatically overwrite existing packages
  • +
  • Provides the 00-index.tar.gz file needed by cabal-install, as well as the tarballs for all the packages
  • +
+

In order to use this, just do the following:

+
  • cabal install yackage
  • +
  • run "yackage"
  • +
  • Upload your packages
  • +
  • Add remote-repo: yackage:http://localhost:3500/ to your ~/.cabal/config file
  • +
  • cabal update
  • +
  • Install your packages are usual
  • +
+

You'll need to leave yackage running whenever you want to run an update or download new packages. A few other usage notes:

+
  • If you overwrite a package, your cache folder will still have the old version. You might want to just wipe our your cache folder on each usage.
  • +
  • Running cabal update will download the update for both yackage and the main hackage server; the latter can be a long process depending on your internet connection.
  • +
+

Here's a little shell script that will disable the Hackage repo, wipe our the Yackage cache, update and re-enable the Hackage repo:

+
#!/bin/sh
+
+CABAL_DIR=~/.cabal
+
+cp $CABAL_DIR/config $CABAL_DIR/config.sav
+sed 's/^remote-repo: hackage/--remote-repo: hackage/' < $CABAL_DIR/config.sav > $CABAL_DIR/config
+rm -rf $CABAL_DIR/packages/yackage
+cabal update
+cp $CABAL_DIR/config.sav $CABAL_DIR/config
+
+

I hope others find this tool useful.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/early-december-update.html b/public/blog/2010/12/early-december-update.html new file mode 100644 index 00000000..5adfd4f7 --- /dev/null +++ b/public/blog/2010/12/early-december-update.html @@ -0,0 +1,51 @@ + Early December Update +

Early December Update

December 9, 2010

GravatarBy Michael Snoyman

It's been a while since I've given an update on what's going on in the Yesod world. There have been a few minor releases of packages, but most of the work has been in the planning realm. I'm contemplating a number of semi-major changes to infrustructure, at many different levels. I'll put down my thoughts here to hopefully spark some discussion. Don't be surprised to see some of these issues raised on the web-devel list or the web development special interest group.

+

But first, a word from our sponsors: the founders page

+

I'm planning on putting up a page on this site for the "founders" of Yesod: a link and a little bio for each of the contributors to Yesod. If you've written some code that's made it into Yesod, send some info over. This includes code in yesod itself, packages like Hamlet, Persistent or WAI, or packages that were written specifically for use with these. In the email, please make sure to mention what you contributed (I don't want to accidently leave out things). I'm sorry for not tracking everyone down personally, I just haven't had a chance to get around to it yet.

+

And while we're talking about this site, I'm still interested in doing a design refresh. If anyone is interested in contributing some skills for this, please let me know. If someone wants to go all the way and contribute actual patches to the repo, that's great, but I'll gladly accept other forms of input.

+

blaze-builder-enumerator and WAI

+

Note: the next few paragraphs contain some back information you don't need to know to understand the WAI proposal. Feel free to skip. +

+ +

My new job consists of a lot of XML parsing. If anyone remembers, a few weeks ago I rewrote the Yesod book in a customized XML format, and used John Millikan's libxml-enumerator library. The library is great, and I started using it at work as well. However, some problems cropped up:

+
  • It's a major pain getting libxml set up correctly on a Windows system. John has also written expat-enumerator, which is easier to get installed, but it does not support DOCTYPEs, which is something I needed support for. It also doesn't support XML namespaces properly.

  • +
  • One tool I needed to write involved parsing an XML file, modifying some of the tags, and spitting it out again. There was no library for rendering the Event datatype.

  • +
+

I solved the first problem by writing an XML parser based on attoparsec. (In case you're wondering: it assumes UTF-8, but comes with an enumeratee to convert UTF-16 and UTF-32 into UTF-8 automatically. Both BE and LE.) For the latter, I started writing a library, but realized there was no efficient way to produce output from an enumerator. In pure code, I would use blaze-builder, but doing so from an enumerator would involve pulling the entire XML file into memory, which defeats the point of a streaming interface.

+

I started working on a library to bridge these two wonderful packages, and soon found out that there were already two other efforts going on for this. I passed my code off to Simon Meier, and he is taking over maintainership of blaze-builder-enumerator. (Just to close off the story about XML: once Simon releases blaze-builder-enumerator, I will hopefully be releasing my renderer, parser and some parser combinators I developed for the Yesod book as a package called xml-enumerator.

+
+ +

That was a very roundabout way of getting to the point at hand: how should we represent request and response bodies in the WAI? I'm currently using a custom-defined enumerator for the response, and something called a source for the request. Bascially, a source allows the input to be paused and resumed, which an enumerator does not. I've considered in the past moving both of them over to an externally defined enumerator datatype. However, WAI currently has incredibly minimalistic dependencies, and I did not have a compelling reason to make the move.

+

However, blaze-builder-enumerator changes that. It will allow much more efficient implementation of WAI middlewares. For example, the JSONP middleware currently produces some incredibly small bytestrings and sticks them on the beginning and end of the response returned by the application. Ideally, we want to have all our bytestrings come to about 16k, so JSONP is really suboptimal. If instead, our response body became Enumerator Builder IO a, everything would work beautifully again.

+

The case for moving the request body over to enumerator is less powerful: the current approach is strictly more powerful. However, if we move it over as well, we make the interface simpler, and we get to reuse our tools better.

+

A new syntax for Hamlet

+

Greg Weber has suggested a new syntax for Hamlet templates, and I think the idea has a lot of merit. Instead of reposting his ideas here, I will ask everyone to read his proposal. I want to hear everyone's opinion, whether it is yay, nay, or don't care. And I don't care if this turns into a bikeshedding contest: we want to do this right. It will be much more difficult to make a change like this after Yesod 1.0.

+

mime-mail + enumerator, ASCII and Text

+

I recently released mime-mail 0.1 to add support for quoted-printables, encoded headers and headers on individiual parts. I still have a few open API questions that I'd like feedback on:

+
  • The parts of a message are given as lazy bytestrings. This means you have to use lazy IO for attaching files. Another approach would be to use an enumerator here (even using blaze-builder-enumerator again possibly). However, this might add too much complexity to the API. I could also create some kind of data PartBody = PartLBS ByteString | PartEnumerator ... | PartFile FilePath, which might be a good trade-off between power and simplicity.

  • +
  • While header values can be any Unicode value, header names are required by the spec to be ASCII-only. mime-mail currently encodes them as UTF-8, which is definitely the wrong thing to do. I can think of a few possibilities of right things: +
    1. Throw a runtime exception when a non-ASCII value is given. +
    2. Change the datatype to bytestring and let the user worry about it. +
    3. Create an Ascii datatype that only allows valid ASCII characters in. +
    +
  • +
  • I'm still using Strings for the header values. Is it time to move over to Text for this? Internally, they get converted to Text anyway.

  • +
+

Forms

+

Forms in Yesod are becoming a major codebase all on their own. I've been considering moving it out into a separate package to make it easier to make breaking changes to the API. However, I just heard about Jasper's digestive-functors package, which believe it or not is about forms and not laxatives (I kid, I kid). It looks like a better version of formlet, supporting some features such as inline error messages.

+

If this library offers what we need in Yesod, I would like to try to move to it. I believe that whenever possible it's great to use existing tools. I have not yet had a chance to look into this library completely, so I don't know if it has all the features we need. But if I do move Yesod's forms into a separate package, then it could be a user choice which form library to use.

+

Yesod 0.7

+

The magnitude of some of these changes really necessitates a 0.7 release: I don't feel comfortable making such sweeping modifications into the 1.0 release. This will also be a time to update any underlying libraries that have become out-of-date, such as neither. If anyone has any other kinds of thing they would like to see changed at the same time, let me know. I do not have a timetable for 0.7, since it really depends on how much of the above we decide to do.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/magic-of-yesod-part1.html b/public/blog/2010/12/magic-of-yesod-part1.html new file mode 100644 index 00000000..843cdc4a --- /dev/null +++ b/public/blog/2010/12/magic-of-yesod-part1.html @@ -0,0 +1,79 @@ + The Magic of Yesod, part 1 +

The Magic of Yesod, part 1

December 23, 2010

GravatarBy Michael Snoyman

Thanks to everyone who responded to the Yesod user survey. The feedback I have gotten has helped me restructure my Yesod TODO list and given my ideas on how to restructure the docs site. Though I will not be releasing all of the results of the survey (I always ask for participants permission beforehand, especially if I ask for personal details), there are some results from the survey that I will be releasing in the future.

+

For the moment, I will share with everyone that the number one request was improved documentation. (By the way: I agree on this, it just takes a long time to write quality documentation.) Most people seemed to like the book best of all documentation sources. One area in particular that people wanted clarifications about was the "magic" of Yesod. This basically breaks down into the following:

+
  • Hamlet/Cassius/Julius templates
  • +
  • The routing quasi-quoting (parseRoutes)
  • +
  • mkYesod (goes together with parseRoutes)
  • +
  • Persistent entity declarations
  • +
  • Persistent automatic migrations
  • +
+

I think that covers all the major uses of Quasi-Quotation and Template Haskell in Yesod. If I've left any out, please let me know. The point of this post is to explain how this stuff works, and to be the basis upon which to build a chapter devoted to the topic. Even though this goes in the "advanced" category, and I'd rather finish the basics part of the book first, I think this is an important enough topic to warrant going out-of-order.

+

But what is this magic?

+

For the uninitiated, let's first cover what these two extensions are. Template Haskell allows you to write Haskell code using Haskell code. You have datatypes representing expressions, literals, types... everything. You can build these up in the Q monad to create anything you want, including defining new datatypes, new functions, and type instances.

+

Now that Q monad I mentioned has two very important properties for our purposes: you can run IO actions inside of it, and when you wrap a Q value inside a Template Haskell splice, it gets run by the compiler to generate the appropriate values. As a simple example:

+
{-# LANGUAGE TemplateHaskell #-}
+import Language.Haskell.TH
+
+main = putStrLn $(litE $ stringL "Hello World!")
+
+

Wrapping up the Q value inside $( and ) is how you "execute" the TH code. Oh, and since you can do IO actions inside the Q monad, you can have your TH depend on the contents of an external text file. That can be really cool, but one word of warning: GHC will not know to recompile your code if that external file changed.

+

As for quasi-quotation, it uses the same Q monad and Template Haskell datatypes, but allows you to embed some text inside special brackets. This essentially lets you embed an arbitrary syntax inside Haskell code, which can be very cool. A simple example of usage would be:

+
-- Multiline
+module Multiline where
+
+import Language.Haskell.TH
+import Language.Haskell.TH.Quote
+
+multiline = QuasiQuoter
+    { quoteExp = litE . stringL
+    }
+
+-- some other file
+{-# LANGUAGE QuasiQuotes #-}
+import Multiline
+
+main = putStrLn [$multiline|
+This is a multiline
+string.
+|]
+
+

Two important points:

+
  1. You need to define the multiline QuasiQuoter in a separate file due to GHC stage restrictions.

  2. +
  3. In GHC 7, the syntax for quasi-quotation has changed, so our main function would become:

    +

    main = putStrLn [multiline| This is a multiline string. |]

  4. +
+

Hopefully that should be enough of an introduction to TH to understand the rest of the discussion. But as with any kind of metaprogramming, TH can be very complicated to write, and usually even more difficult to read. The Haddocks will be your friend here.

+

Hamlet/Cassius/Julius templates

+

These are the least magical of all. There are three versions of functions for each language: a quasi-quoter, a TH function and a TH debug function. Let's start with the simplest of these: the Julius quasi-quoter. It does the following steps:

+
  1. Given the String input, parse it into raw text, escaped operators (%%, @@ and ^^) and into interpolations (%myVar%, @myUrl@, ^myJulius^).

  2. +
  3. For the interpolations, convert them into references to Haskell values. For example, the interpolation %(foo.bar).baz% would be converted into (foo bar) baz.

  4. +
  5. A Julius value is a function taking a URL rendering function and returning a Javascript value. Javascript is just a newtype wrapper around a blaze-builder Builder value, for efficient building up of ByteStrings. We now have four types of values (raw strings, %variable interplolations%, @url interpolations@ and ^template interpolations^). These are all converted to a Julius, by:

    +
    • Raw strings: UTF8-encoding the data and converting to a Builder. This Builder is wrapped in a Javascript, and const is applied to turn this into a Julius.
    • +
    • Variables: The variable must be an instance of ToJavascript, therefore toJavascript is applied, followed by const.
    • +
    • URLs: The URL rendering function is applied to get a String, followed by conversion to Builder and then Javascript.
    • +
    • Templates: The value must already be a Julius: no action taken.
    • +
  6. +
  7. Since a Julius is an instance of Monoid, we simply mconcat all of these pieces to get our final answer.

  8. +
+

The Template Haskell version does almost the exact same thing, except it reads the contents of the template from a file. Remember, you can run IO actions inside the Q monad. Unfortunately, this means you will need to recompile your code every time you change your Javascript, which can be annoying during development. Therefore, the debug version reloads the contents of template on each call. This involves some fancy footwork to make sure the correct variables are available, but nothing too scary.

+

Cassius is almost exactly the same thing, except:

+
  1. The content is parsed for the whitespace-sensitive layout Cassius uses, and then rendered to proper CSS afterwords.
  2. +
  3. There is no template interpolation in Cassius.
  4. +
+

And Hamlet is just a step above that, introducing things like foralls and conditionals. However, all of this is just following the concept of building up Haskell functions using TH code, and referencing the variables in scope when the template was called.

+

If anyone has specific questions about how all this works, please ask in the comments and I'll try to answer.

+

To be continued

+

Unfortunately, it's late over here, so I need to cut this post short. Stay tuned for later posts as I'll try and explain the rest of the magical Yesod pieces.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/magic-of-yesod-part2.html b/public/blog/2010/12/magic-of-yesod-part2.html new file mode 100644 index 00000000..4e4b63ba --- /dev/null +++ b/public/blog/2010/12/magic-of-yesod-part2.html @@ -0,0 +1,190 @@ + The Magic of Yesod, part 2 +

The Magic of Yesod, part 2

December 25, 2010

GravatarBy Michael Snoyman

For those celebrating one, a happy holiday. To everyone else, a happy day.

+

Before we get started on the second installment in this series, I wanted to give everyone a little update on Yesod 0.7. Felipe Lessa came up with a very good idea: split up Yesod into a yesod-core package which will provide basic functionality and have minimal dependencies, and a yesod package which will tie together a bunch of addon packages. This will help out in a few ways:

+
  • The scaffolded site will no longer have extra dependencies versus the yesod package.

  • +
  • The scaffolded site can be a little smaller, since some of the functionality that was included there can be put in the yesod package.

  • +
  • One of the goals of the 1.0 release is to have a complete test suite. Breaking things up into smaller packages makes it (at least psychologically) easier to test the whole thing.

  • +
  • Make it easier to make breaking changes in addon modules like Sitemap or AtomFeed.

  • +
+

This also means that it should be easier for new contributors to find a package and make contributions, since there will be less far-reaching consequences. So if anyone sees one of these new packages and would like to take a more active role (either contributing, or even taking over maintainership), please let me know.

+

Anyway, on to the topic at hand: the magic.

+

Static subsite

+

There are two pieces of "magic" involved in the static subsite (for serving static files). The first is the very concept of a subsite. Unfortunately, this is a topic that I have not gotten around to documenting properly, and probably will not for a few months. For a one line explanation: it allows you to create reusable components that can be plugged into any Yesod application.

+

But the magic to be addressed now is the staticFiles function. Let's review a few key facts about how the static subsite works:

+
  • There is a StaticRoute datatype which represents each file request. There is a single constructor (StaticRoute) which takes two arguments: a list of path pieces (eg, "/foo/bar.css" becomes ["foo", "bar.css"]) and a list of query string parameters.

  • +
  • The Static datatype contains a field for the lookup function, which takes a FilePath. For this, the path pieces are converted back into a path (eg, ["foo", "bar.css"] -> "/foo/bar.css") and the query string is ignored.

  • +
+

So what's the point of the query string? If you place a hash of the file contents in the query string, then you can set you expires header far in the future and not worry about the client getting stale content. When you change the content, the hash will change, and therefore the URL will change, causing the browser to download a new copy.

+

We now have two annoyances when dealing with the static subsite:

+
  • We need to type in the file paths in our Haskell code without any assurances that the files actually exist. You're one typo away from a broken site.

  • +
  • You'll need to manually calculate the hashes. Besides the extra programming overhead, if you do this at runtime you'll get a major performance overhead as well.

  • +
+

The answer to both of these is the staticFiles TH function. If you give it a filesystem path, it will explore the entire directory structure, find all files, calculate hashes, and create Haskell identifiers for each of them. And it does all of this at compile time, meaning zero runtime performance overhead. So, if you have a "static/images/logo.png" file, and you want to use it, you simply include the line:

+
$(staticFiles "static")
+
+

in your code and you will now have a images_logo_png value in scope with a datatype of StaticRoute. Oh, and I forgot to mention: GHC 6.12 introduced a feature where the TH brackets are not necessary for top-level calls, so you can simply write

+
staticFiles "static"
+
+

There is one downside to this approach that needs to be mentioned: if you change files in your static folder without modifying the module that calls staticFiles, you will still have the old identifiers in your object files. I recommend having a StaticFiles module in each project that just runs the staticFiles function. Whenever you modify your static folder, touch StaticFiles.hs and you should be good to go. For extra safety when compiling your production code, I recommend always starting with a cabal clean.

+

parseRoutes

+

The parseRoutes quasi-quoter is actually even simpler than Julius. However, it goes hand-in-hand with mkYesod, which is significantly more sophisticated than Julius, and therefore this section ended up in this post instead. The quasi-quoter does only two things:

+
  • Converts each non-blank line in its argument into a Resource.

  • +
  • Checks that there are no overlapping paths in the resources provided.

  • +
+

Starting with the second point, an overlapping set of paths could be something like:

+
/foo/#String
+/#String/bar
+
+

since /foo/bar will match both of those. However, there is unfortunately a little bit more to it than that, since even these paths will overlap:

+
/foo/#Int
+/#Int/bar
+
+

This is because the quasi-quoter doesn't know anything about what an Int or String are, it just passes them along. I still think that it is best to avoid such overlapping paths, but if you really want to avoid the overlapping check, you can use parseRoutesNoCheck.

+

Now what about that Resource datatype? It has a single constructor:

+
Resource String [Piece] [String]
+
+

The first String is the name of the resource pattern, and the list of Strings at the end is the extra arguments. For example, in:

+
/foo/bar FooBarR GET POST
+
+

that list of Strings would be

+
["GET", "POST"]
+
+

The quasi-quoter does not apply any meaning to that section; that is handled by mkYesod. As far as the list of Pieces, there are three piece constructors: StaticPiece, SinglePiece and MultiPiece. As a simple example,

+
/foo/#Bar/*Baz
+
+

becomes

+
[StaticPiece "foo", SinglePiece "Bar", MultiPiece "Baz"]
+
+

Of all the magic in Yesod, this is the part that can most easily be replaced with plain Haskell. In fact, this could be a good candidate for an IsString instance. Something to consider...

+

mkYesod

+

I personally think that type safe URLs are the most important part of Yesod. I feel very good saying that, because I'm not even the one who came up with the idea: after release 0.2 of Yesod (I think), both Chris Eidhof and Jeremy Shaw emailed me the idea. It's actually hard for me to imagine where Yesod would have gone had it not been for that recommendation.

+

The good side of type safe URLs is that it makes it all but impossible to generate invalid internal links, it validates incoming input from the requested path, and makes routing very transparent. The bad side is that it requires a lot of boilerplate:

+
  • Define a datatype with a constructor for each resource pattern.
  • +
  • Create a URL rendering function to convert that datatype to a [String]
  • +
  • Write a URL parsing function to convert a [String] to that datatype (well, wrapped in a Maybe)
  • +
  • Write a dispatch function to call appropriate handler functions.
  • +
+

mkYesod is probably the most important single function in all of Yesod. It does all four of these steps automatically for you, based on a list of Resources (which can be created using the parseRoutes quasiquoter described above).

+

As a simple example, let's take a look at what mkYesod does:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies #-}
+import Yesod
+import Yesod.Helpers.Static
+
+mkYesod "MySite" [$parseRoutes|
+/ RootR GET
+/person/#String PersonR GET POST
+/fibs/#Int FibsR GET
+/wiki/*Strings WikiR
+/static StaticR Static getStatic
+|]
+
+

If you run this code with -ddump-splices, you'll see the resulting Haskell code. Here's the cleaned up version:

+
data MySiteRoute
+    = RootR
+    | PersonR String
+    | FibsR Int
+    | WikiR Strings
+    | StaticR Route Static
+    deriving (Show, Read, Eq)
+
+type instance Route MySite = MySiteRoute
+
+dispatch RootR method =
+    case method of
+        "GET" -> Just $ chooseRep <$> getRootR
+        _ -> Nothing
+dispatch (PersonR x) method =
+    case method of
+        "GET" -> Just $ chooseRep <$> getPersonR x
+        "POST" -> Just $ chooseRep <$> postPersonR x
+        _ -> Nothing
+dispatch (FibsR x) method =
+    case method of
+        "GET" -> Just $ chooseRep <$> getFibsR x
+        _ -> Nothing
+dispatch (WikiR x) _ = Just $ chooseRep <$> handleWikiR x
+-- Yes, this next bit is *ugly*...
+dispatch (StaticR x) method =
+    (fmap chooseRep <$>
+        (toMasterHandlerDyn
+            StaticR
+            (\ -> runSubsiteGetter getStatic)
+            x
+            <$>
+            Web.Routes.Site.handleSite
+            (getSubSite :: Web.Routes.Site.Site (Route Static) (String -> Maybe (GHandler Static MySite ChooseRep)))
+            (error "Cannot use subsite render function")
+            x
+            method))
+
+-- produces a pair of path pieces and query string parameters
+render RootR = ([], [])
+render (PersonR x) = (["person", toSinglePiece x], [])
+render (FibsR x) = (["fibs", toSinglePiece x], [])
+render (WikiR x) = ("wiki" : toMultiPiece x, [])
+render (StaticR x) =
+    (\ (b, c) -> (("static" : b), c)) $
+    (Web.Routes.Site.formatPathSegments
+        (getSubSite :: Web.Routes.Site.Site
+            (Route Static)
+            (String -> Maybe (GHandler Static MySite ChooseRep))) x)
+
+parse [] = Right RootR
+parse ["person", s] =
+    case fromSinglePiece s of
+        Left e -> Left e
+        Right x -> PersonR x
+parse ["fibs", s] =
+    case fromSinglePiece s of
+        Left e -> Left e
+        Right x -> FibsR x
+parse ("wiki" : s) =
+    case fromMultiPiece s of
+        Left e -> Left e
+        Right x -> WikiR x
+parse ("static" : s) =
+    case Web.Routes.Site.parsePathSegments
+       $ (getSubSite :: Web.Routes.Site.Site
+            (Route Static)
+            (String -> Maybe (GHandler Static MySite ChooseRep))) of
+        Left e -> Left e
+        Right x -> StaticR x
+parse _ = Left "Invalid URL"
+
+instance YesodSite MySite where
+    getSite = Web.Routes.Site.Site dispatch render parse
+
+

The actual code is a little bit harder to follow, but does the same basic thing. One last thing: in order to make it possible to define your routes in one file and your handlers in a bunch of other files, we need to split up the declaration of the MySiteRoute datatype from the declaration of the dispatch function. That's precisely the purpose of providing both mkYesodData and mkYesodDispatch.

+

Errata for last post

+

One thing I forgot to mention in the last post: Hamlet templates are in fact polymorphic. You can have:

+
[$hamlet|%h1 HELLO WORLD|] :: Html
+[$hamlet|%h1 HELLO WORLD|] :: Hamlet a
+[$hamlet|%h1 HELLO WORLD|] :: GWidget sub master ()
+
+

This is achieved via the HamletValue typeclass. This construct is a little complicated, and probably deserves its own discussion. For now, I will simply say that this typeclass provides htmlToHamletMonad and urlToHamletMonad functions for the hamlet TH code to call, and thus create a polymorphic result.

+

There are two important things to keep in mind:

+
  • You cannot embed a template with one datatype inside a template with a different datatype. For example, the following will not work:

    +

    asHtml :: Html asHtml = [hamlethamlet|asHtml|]

  • +
  • When dealing with the GWidget instance, GHC can get confused. For example:

    +

    -- this works myGoodWidget :: GWidget sub master () myGoodWidget = do setTitle "something" [$hamlet|%h1 Text|]

    +

    -- this doesn't myBadWidget :: GWidget sub master () myBadWidget = do [$hamlet|%h1 Text|] setTitle "something"

  • +
+

Since the datatype for the hamlet quasiquotation in myGoodWidget is required to be GWidget sub master (), everything works out. However, in myBadWidget, the datatype is actually GWidget sub master a, and GHC doesn't know that you want to use the GWidget sub master () instance of HamletValue. The trick to get around this is to use the addWidget function, which is:

+
addWidget :: GWidget sub master () -> GWidget sub master ()
+addWidget = id
+
+

Conclusion

+

I still owe you another post persistent entity declarations and migrations, but that will have to wait for another day. I still have some coding to do tonight!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/serendipity-wai-http-enumerator.html b/public/blog/2010/12/serendipity-wai-http-enumerator.html new file mode 100644 index 00000000..c737d5ea --- /dev/null +++ b/public/blog/2010/12/serendipity-wai-http-enumerator.html @@ -0,0 +1,82 @@ + Serendipity: WAI and http-enumerator +

Serendipity: WAI and http-enumerator

December 14, 2010

GravatarBy Michael Snoyman

It's always an amazing feeling when you realize that two problems you are working on have the same solution. In this case, I was separately trying to figure out a better interface for the http-enumerator library, and migrate WAI over to a combination of blaze-builder and the enumerator package.

+

The problem with http-enumerator is that it's not an enumerator: it would make sense for the http function to in fact create an enumerator that feeds data into a user's iteratee. However, there's a bit of a snag: we have some extra information in the way of the HTTP status code and response headers. So currently, the type signature of the http function is:

+
http :: MonadIO m => (Int -> Headers -> Iteratee ByteString m a) -> Request -> m a
+
+

In other words, you give it a function that produces an Iteratee, and it will run that Iteratee for you. This approach makes it impossible to send more data to the iteratee after the http function is complete, and makes it more difficult to use standard tools like enumeratees. I called a stalemate on this issue about 2 months ago.

+

More recently, I was working on the issue of rendering content within an enumerator. After some emails with Simon Meier, John Millikan and Gregory Collins, I decided to not only use blaze-builder-enumerator for generating XML content (my original goal), but to try it out for the WAI as well. The most obvious approach was to define a response as:

+
data Response = Response Status ResponseHeaders (Enumerator Builder IO ())
+
+

Well, things are actually a little more complicated than this, since we have support for sending files and using lazy bytestrings as well, but that's not the point at hand. This seemed to be a pretty good fit, and I decided that it would be great to demonstrate using the two libraries together with a proxying web server: it will take any request, forward it to a different website, and return the result to you.

+

Unfortunately, we have a big mismatch between the two APIs. There are in fact two major problems:

+
  • http-enumerator does not supply an enumerator at all, which is exactly what the WAI wants.

  • +
  • The WAI expects to know the status code and response headers before the enumerator is run. However, in the case of a proxy, we won't know those until after the enumerator has started.

  • +
+

It turns out that both of these issues have to do with passing extra information in the presence of enumerators. I explored a number of different approaches to get around the problem, but it turns out that my original type signature for the http function was almost right on the money. Let's remind ourselves of what an enumerator is:

+
type Enumerator a m b = Step a m b -> Iteratee a m b
+
+

It's a function that transforms steps into iteratees. But an iteratee is really just a wrapper around a step in a monad. So after a little comprimising with myself, I came to this new type signature:

+
http :: MonadIO m => Request -> (Int -> Headers -> Iteratee ByteString m a) -> Iteratee ByteString m a
+
+

I've put the Request at the beginning, but that's not the important part. This function now returns an iteratee instead of running the computation entirely. This allows it to be resumed later on by another enumerator. This in turn gave me the idea that our response in WAI could be modeled as:

+
(Status -> ResponseHeaders -> Iteratee Builder IO a) -> Iteratee Builder IO a
+
+

Unfortunately, this time around returning the iteratee is not a good idea: it prevents the usage of enumeratees which change the stream type. That will virtually always be the case, since we need to use blaze-builder-enumerator to render the Builders into ByteStrings. So instead, applications need to run the iteratee themselves:

+
(Status -> ResponseHeaders -> Iteratee Builder IO a) -> IO a
+
+

There's something tricky going on here: that type variable "a" makes sure that the application actually runs the iteratee, because otherwise it will have no way of returning a value of type IO a. Well, short of using bottom or exceptions, but that's a different issue.

+

Since the type signatures for these two things were becoming increasingly similar, I decided to bridge the gap a little more. The WAI datatype Status contains both the numeric status code and the textual status message, while the ResponseHeaders datatype uses a case-insensitive bytestring for the keys. http-enumerator now uses both of those, giving us a final type signature of:

+
http :: MonadIO m
+     => Request
+     -> (Status -> ResponseHeaders -> Iteratee S.ByteString m a)
+     -> Iteratee S.ByteString m a
+
+

So the three important differences between these two types is:

+
  1. http uses ByteStrings, since the content is meant to be read. WAI responses use Builders, since the content is meant to be written.

  2. +
  3. http returns an iteratee so the computation can be resumed by another enumerator. WAI responses return a computed value to avoid issues with the stream type.

  4. +
  5. http lives in any arbitrary MonadIO, while WAI responses only live in the IO monad. There's no way for a WAI handler to automatically unwrap arbitrary monads.

  6. +
+

All of these changes are available in respective github repos: http-enumerator, wai and wai-extra (for the SimpleServer). I'm far from sold on these API changes, but I did think the concept interesting enough to warrant discussing them now. I'm very interested in work others have done with enumerators and extra data.

+

Oh, and with these changes, it was a piece of cake to write that proxy application. I used the Yesod Wiki since it uses relative URLs for links, though any site should be fine.

+
{-# LANGUAGE OverloadedStrings #-}
+import qualified Network.HTTP.Enumerator as H
+import qualified Network.Wai as W
+import Network.Wai.Handler.SimpleServer (run)
+import qualified Data.ByteString.Char8 as S8
+import qualified Data.ByteString.Lazy.Char8 ()
+import Data.Enumerator (($$), joinI, run_)
+import Blaze.ByteString.Builder (fromByteString)
+import qualified Data.Enumerator as E
+
+main :: IO ()
+main = run 3000 app
+
+app :: W.Application
+app W.Request { W.pathInfo = path } =
+    case H.parseUrl $ "http://wiki.yesodweb.com" ++ S8.unpack path of
+        Nothing -> return notFound
+        Just hreq -> return $ W.ResponseEnumerator $ run_ . H.http hreq . go
+  where
+    go f s h = joinI $ E.map fromByteString $$ f s $ filter safe h
+    safe (x, _) = not $ x `elem` ["Content-Encoding", "Transfer-Encoding"]
+
+notFound :: W.Response
+notFound = W.ResponseLBS W.status404 [("Content-Type", "text/plain")] "Not found"
+
+

Starting with the less-interesting stuff, main is just a standard call to SimpleServer, notFound is a basic 404 response, safe strips out any response headers that would alter how a client would understand the response delivery itself, and parseUrl simply converts a String into a http-enumerator Request.

+

The interesting stuff is the go function and the line that returns the ResponseEnumerator. The go function converts an iteratee that takes a stream of Builders into an iteratee that takes a stream of ByteStrings. It also needs to address the extra "metadata" of the status and response headers.

+

Once the conversion is done, it's easy to plug that iteratee into the http function to produce a final iteratee, and use the run_ function from enumerator to run the iteratee.

+

By the way, this example was originally going to involve demonstrating XML parsing and rendering as well, but I think I'll save that one for another blog post.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/yesod-user-survey.html b/public/blog/2010/12/yesod-user-survey.html new file mode 100644 index 00000000..791c52c2 --- /dev/null +++ b/public/blog/2010/12/yesod-user-survey.html @@ -0,0 +1,22 @@ + Yesod User Survey +

Yesod User Survey

December 21, 2010

GravatarBy Michael Snoyman

I've been meaning to do this for about a month now, and I finally took the time to write it up: the first Yesod user survey. My goal is to get a good feel of what users like and don't like, and to know where to target efforts going forward. If you are a Yesod user, or even just interested in Yesod, please take the time to fill it out.

+

Also, a little status update on what's going on right now: there are a bunch of changes in the works for the next Yesod 0.7 release. Some things of note:

+
  • A new version of WAI which will be faster and more general, allowing things like creating HTTP proxies.
  • +
  • The new Hamlet syntax proposed by Greg. This will not deprecate the current syntax, though it may break some older Hamlet code if it involved a lot of copy-pasted HTML.
  • +
  • Modularization: we're breaking things out into a lot of different packages.
  • +
+

And it's about this last point that I want to talk to you now. I'm trying to split out packages for two reasons: make core Yesod usage more lightweight, and to get some generic pieces of code available for usage outside of Yesod. As an example of the latter, I'm splitting out cookie parsing and resource pools.

+

I think this is a great opportunity for anyone who's been interested in Yesod to get their hands dirty. A lot of the work here is very minimal: pull out code, document it a bit, add some tests, and deploy to Hackage. If anyone is interested in helping out with this, please send me an email: I'll be more than happy to help out and explain the code, and I think it will make for a more robust community to share the knowledge out more.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2010/12/yesods-own-world.html b/public/blog/2010/12/yesods-own-world.html new file mode 100644 index 00000000..2ff69f61 --- /dev/null +++ b/public/blog/2010/12/yesods-own-world.html @@ -0,0 +1,30 @@ + Library versus Framework: Yesod's Own World +

Library versus Framework: Yesod's Own World

December 10, 2010

GravatarBy Michael Snoyman

I was going to put this at the end of my previous post, but it's really an important enough topic to be put in separately. There was recently a very good discussion on Reddit about different approaches to web development in Haskell, with a large focus on Snap versus Yesod. The top rated comment, from Chris Smith, made this seemingly controversial statement:

+

It's sort of its own country though: if you use Yesod, then there's a Yesod way to do web serving, persistence, templating... the works.

+
+

I think the discussion that followed was very good, and really made clear that there are different ways of having your "own world." However, I want to stick with this statement from Chris. He contrasted this to the other approach to web development in Haskell:

+

The rest of Haskell web applications is more or less similar: you grab a library for request routing, and a library for templating, and a library for persistence... and you just use them.

+
+

I agree with what Chris is saying here: Yesod is a fully baked approach that guides you through all your library choices. To be clear, it doesn't require that you use every one of its libraries. I've written a number of sites that don't store data in Persistent, simply because it wasn't the right match. If someone is absolutely in love with Heist or HSP, I won't be offended if they drop Hamlet. But Yesod is all about making a bunch of different components integrate nicely together.

+

To me, this is the difference between a library and a framework. I have purposely done Yesod development in a modular way: templating was put in Hamlet, storage in Persistent, routing in web-routes-quasi, and so on and so forth. If you like all of these choices, and you want a consistent system that makes it easy to use them together, then Yesod should be a perfect fit. If you want to pick and choose your libraries, you can choose Yesod, but you may be better served not going the framework route.

+

Now, I've said this before and offended some people, but perhaps I can clarify better this time: I think Snap is a library, not a framework. This is purely subjective, but based on the definition above, and on the observations of others, Snap gives you much more freedom about how to approach things. I don't think it's a bad thing to be a library: there are users that want that, and it is often the right approach. But at the current point in time, Snap is not trying to give you an all-in-one solution for every aspect of your web development. Note also that there are plans to add more features to Snap in the future, I am simply talkng about what's available today.

+

I point this out because I think it can help shed light on the difference between Yesod and Snap, and when you should use each. As a practical example: Yesod is very opinionated about how routing should work. It requires your routes to be clearly defined at compile time so it can give you type-safe URLs. On the other hand, Snap has a much more freeform approach to routing. I personally think that the type-safe URL approach is a boon to productivity, much like Haskell's type-safety keeps you from shooting yourself in the foot.

+

On the other hand, there are some applications that may not be able to specify all of their routes at compile time. For those, it would likely feel more natural to use Snap's routing system. It's possible to do this in Yesod: you could use MultiPiece to fake having dynamic routes, set up a subsite, or simply replace Yesod's routing system with something else. But that's not the Yesod way, and you will probably feel a little out of place trying to do it there.

+

And of course there are some people who just don't like type-safe URLs.

+

My point is, these two packages are two very different beasts, and they don't overlap as much as people seem to think they do. Yesod is really just the final glue package for a bunch of individual libraries that can easily be used with Yesod, Snap, Happstack or directly with CGI if you want. But Yesod itself is not meant to be used in other frameworks: it stands on its own, and does things its own way. I am unappologetic about that: my goal here is to create a clean, well designed, unified framework for creating web applications.

+

If you like it, I'm happy if you use it. If you don't, I'd like to hear why so I can improve things, but I won't be modifying things at the expense of our main goal. You can still enjoy a lot of the Yesod ecosystem with some of the packages I've spun off.

+

By the way, there is one thing I wish Happstack and Snap would both do: embrace WAI. If they don't like WAI as-is, then we should fix it. Sorry to pick on them here, but Snap is only now getting multipart form parsing support, while Yesod has had it for a while. If we were all using WAI, the problem could be solved once, as could GZIP compression, JSON-P support, cookie parsing, and a dozen other things. We could all pool our resources and produce high-quality, fast and stable libraries for testing and running in development mode. And we would all be able to deploy our apps to standalone servers, FastCGI, SCGI, CGI, and any other *CGI they make up in the future. In my opinion, by not using a system like that, it is in fact Snap and Happstack that are currently in their own worlds.

+

(By the way, to their credit, Happstack has done some work on an experimental fork that uses WAI, but it is not completely compatible with the main branch, and it has not been officially released.)

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/01/announcing-wai-0-3.html b/public/blog/2011/01/announcing-wai-0-3.html new file mode 100644 index 00000000..e940754f --- /dev/null +++ b/public/blog/2011/01/announcing-wai-0-3.html @@ -0,0 +1,71 @@ + Announcing Web Application Interface 0.3.0 +

Announcing Web Application Interface 0.3.0

January 11, 2011

GravatarBy Michael Snoyman

The Web Application Interface (WAI) is a low-level, highly performant, extrmely generic layer between web applications and web servers. It allows you to write your application once and run it on a development server, production server, FastCGI and even as a desktop application.

+

This new release introduces a large change in philosophy: previous versions only depended on the base and bytestring packages. Now, for both performance and ease-of-use, we have introduced three new dependencies:

+
  • enumerator: Anyone who follows this blog probably knows I'm a fan of John Millikin's enumerator package. Previously, WAI used a built-in Enumerator datatype for the response body, and a custom Source datatype for the request body. Now everything is provided in terms of enumerator's datatypes, making coding against the WAI much easier and providing a wealth of tool support.

  • +
  • blaze-builder: I'm also a fan of Simon Meier and Jasper Van der Jeugt's blaze-builder package. This essentially lets you construct optimally sized ByteStrings with a minimum of buffer copying. Switching to this can provide the WAI with huge performance advantages.

  • +
  • network: You might be surprised that this wasn't a dependency in the first place. It is included now so that remoteHost is represented by a SockAddr instead of a ByteString, which means less DNS lookups in general. Thanks to Aur Saraf for the recommendation.

  • +
+

There is also a large change in how a Response works. In particular, you are now able to generate the status and response headers from within the response enumerator, which allows us to create things such as proxying web sites. A proper example of this is below. If people are interested in more details of the API changes, let me know in the comments and I'll write up a post about it.

+

As usual, a new WAI release comes with new releases of the wai-handler-fastcgi and wai-handler-scgi packages, as well as a new release of wai-extra. There are a few things to note in this release of wai-extra:

+
  • A new autohead middleware, which automatically produces responses to HEAD requests based on your response to a GET request.

  • +
  • A new vhost middleware, which allows you to route requests to different applications based on hostname (or anything else you want for the matter).

  • +
  • SimpleServer is gone.

  • +
+

Welcoming Warp

+

A few people over the past years have contacted me and asked whether SimpleServer could be used in a production environment. One of these people, Matt Brown (one the core Yesod team) actually has been using it in production for a while, and decided to finally push it from a test server to a full-fledged production server.

+

The result of this is Warp. We will likely be releasing the server in the next few days. I won't go into too many details now, but some nice features of it are:

+
  • it's programmed in a fairly high-level manner, relying mostly on enumerator and blaze-builder for the heavy lifting

  • +
  • it's very fast

  • +
+

It turns out servng with Warp is significantly faster than any of the current alternatives, so after Yesod 0.7 is released Warp will become its recommended deployment option.

+

wai-test (and it's about time)

+

Greg Weber (the third Yesod core developer) has been pushing for more testing for a while. The result is wai-test: a very simplistic HTTP unit test framework for WAI applications. It currently does not have very many features, but these will likely be added as we increase our Yesod test suite. wai-extra already uses wai-test for checking all of its middlewares.

+

http-enumerator

+

I wrote a blog post about month ago about the serendipity between wai and http-enumerator. With http-enumerator 0.3, we're now using the same datatypes (Status and CIByteString) as WAI. This makes http-enumerator more correct (response headers should be case insensitive) and more informative (you now have the status message in addition to the status code). Plus it lets you write the fabled web site proxy (uses the not-yet-released Warp):

+
{-# LANGUAGE OverloadedStrings #-}
+import qualified Network.HTTP.Enumerator as H
+import qualified Network.Wai as W
+import Network.Wai.Handler.Warp (run)
+import qualified Data.ByteString.Char8 as S8
+import qualified Data.ByteString.Lazy.Char8 ()
+import Data.Enumerator (($$), joinI, run_)
+import Blaze.ByteString.Builder (fromByteString)
+import qualified Data.Enumerator as E
+
+main :: IO ()
+main = run 3000 app
+
+app :: W.Application
+app W.Request { W.pathInfo = path } =
+    case H.parseUrl $ "http://wiki.yesodweb.com" ++ S8.unpack path of
+        Nothing -> return notFound
+        Just hreq -> return $ W.ResponseEnumerator $ run_ . H.http hreq . go
+  where
+    go f s h = joinI $ E.map fromByteString $$ f s $ filter safe h
+    safe (x, _) = not $ x `elem` ["Content-Encoding", "Transfer-Encoding"]
+
+notFound :: W.Response
+notFound = W.responseLBS W.status404 [("Content-Type", "text/plain")] "Not found"
+
+

Note that you do not need to actually be using WAI to use the new http-enumerator; the dependency is simply there to provide datatypes. And since http-enumerator already depends on all of WAI's dependencies, you only need to install one extra package.

+

There are many more changes for http-enumerator in the pipeline. Gregory Collins pointed out that persistent connections could be very useful, and Iustin Pop has requested support for controlling SSL certificate checks. If anyone would like to volunteer to help implement these features, I'd appreciate it: I'm a bit swamped at the moment.

+

authenticate 0.8

+

This new version of authenticate uses the new http-enumerator, and fixes some API problems in the OpenID code. Jeremy Shaw pointed out that there was no support specifying extra parameters or receiving extra parameters. Additionally, error message handling was a bit weak and some sites require the realm to be set. Thanks to Jeremy, authenticate should now work with even more OpenID providers.

+

Coming up

+

This was the first batch of package releases. I have a number of batches on the way. As I said above, I'll be releasing the Warp web server soon. This will possibly be accompanied by a not-yet-written package for a generic static file server and a warp-based static file server. Along with warp I can release a new version of wai-handler-devel.

+

After that, I want to finally tackle Hamlet 0.7. There have been a lot of proposals put out, and it's time to make some decisions and implement things. Persistent probably won't see any major changes in the 0.4 release besides some dependency bumps and removing some deprecated functions.

+

And then comes the Yesod 0.7 release. As I've mentioned before, this release will see the main package get split up into smaller packages. So far: yesod-core, -form, -json, -newsfeed, -persistent, -sitemap and -static. We'll also have a "yesod" package to provide the scaffolding tool and tie everything together. And when all that's done, I can finally get back to finishing the basics part of the Yesod book.

+

Moral of the story: if you have any ideas you want implemented in any of these packages, let me know soon.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/01/announcing-warp.html b/public/blog/2011/01/announcing-warp.html new file mode 100644 index 00000000..bc9dd532 --- /dev/null +++ b/public/blog/2011/01/announcing-warp.html @@ -0,0 +1,96 @@ + Announcing the Warp web server +

Announcing the Warp web server

January 18, 2011

GravatarBy Michael Snoyman

The Yesod team is very pleased to announce the first release of the Warp web server. Warp is a web server for the Wai Application Interface, meaning any Haskell application written against the WAI can use Warp.

+

Warp was previously part of wai-extra and called SimpleServer. However, after some modifications by Matt Brown it is now ready to be used in a production environment. Since I'm sure everyone wants to see the benchmarks, we'll start with those, and then get to some of the technical details below.

+

+

+

Note: Snap errored out in the above benchmark, and therefore results for it are not available. +

+

+ +

These benchmarks compare the current snap-server 0.3 from Hackage, compiled with the libev backend, against the Warp 0.3 release, as well as a pre-release Yesod 0.7 running on Warp. The difference between Warp and Yesod gives you an idea of the relative cost of Yesod's extra features (such as routing): as you might expect, this difference is relatively large on a small benchmark like PONG, and relatively small on a large benchmark like big-table.

+

All of the code for these benchmarks is available on github. I asked Gregory and Doug from Snap to look over our results: though they did not get as steep a difference as our results show, Warp still came out ahead: 90k vs 30k. It seems the difference is Doug benchmarked on a 8-core system, while Matt ran our benchmarks on a 4-core system. Snap seems to be better at scaling to >4 cores than Warp, though why this difference exists warrants further investigation.

+

What I think is even more impressive than the benchmark numbers is the size of the code itself: all of Warp fits in a single 318 SLOC file. However, it is built on the shoulders of giants:

+
  • The Warp PONG benchmark increased its response rate three-fold when switching from GHC 6.12 to 7. I would attribute a huge amount of the success here to Johan and Bryan's new event manager, as well as GHC's new inliner.

  • +
  • In order to avoid multiple buffer copies while still minimizing system call overhead, we use blaze-builder extensively. Kudos to Simon and Jasper.

  • +
  • And finally, our entire IO is built on top of John's enumerator package. In fact, I attribute the small code base almost entirely to the usage of enumerator here.

  • +
+

I think that the code base is small enough that we can have an adequate walk-through here. This might also serve as an example of using enumerator and blaze-builder together to produce fast code.

+

run and serveConnections

+

The entry point to Warp is the "run" function, which takes a port to listen on and a WAI application to run. A WAI application is just a function that takes a Request and returns a Response. We'll see more details on both of those datatypes later.

+

run opens up a listening socket and brackets to serveConnection. The bracket just makes sure that the socket is closed even in the presence of exceptions.

+

serveConnections is also fairly simple: it accepts a single connection, creates a new thread to handle the connection using serveConnection, and loops. While spawning a single thread per connection may be a bad idea with system threads, GHC's light-weight threads fit this pattern very well.

+

In serveConnection, we see our first bit of mystery. Firstly, for performance reasons, we wrap the whole thing in a catch to ignore any exceptions. We also wrap it inside a finally to ensure that the connection is closed even with exceptions.

+

However, the interesting code is:

+
E.run_ $ enumSocket 4096 conn $$ serveConnection'
+
+

enumSocket converts a socket into an Enumerator, which reads 4096 bytes of data at a time and feeds the data to an Iteratee. Using the $$ operator simply pipes the data into serveConnection', an Iteratee we'll see in a bit.

+

The result of this is a new Iteratee. E.run_ actually executes that Iteratee and gives us back an IO action.

+

serveConnection'

+

Within serveConnection', we are living in our Iteratee. enumerator is taking care of all the piping and buffering of the input stream for us, so we can just focus on parsing the input data. Note that we still haven't done anything about our output stream back to the client.

+

Anyway, serveConnection' is just:

+
serveConnection' = do
+    (enumeratee, env) <- parseRequest port remoteHost'
+    res <- E.joinI $ enumeratee $$ app env
+    keepAlive <- liftIO $ sendResponse env (httpVersion env) conn res
+    if keepAlive then serveConnection' else return ()
+
+

First we parse our request object. This returns us two values: env contains most of the values we'd expect, like path info, query string and remote host. enumeratee is an Enumeratee for piping the request body into the application. Remember that we're living inside an Iteratee monad right now, so there is no way to "extract" the input stream again. Instead, we use this enumeratee to ensure that precisely the right number of bytes are taken from the input stream to fully consume the request body, without going over.

+

The next line is what actually runs our application: app env (by definition in the WAI) returns back an Iteratee ByteString IO Response. We apply enumeratee (with joinI) to get a new Iteratee that cannot overstep its bounds, and then take out our result. sendResponse sends the response on to the client, and lets us know whether or not we should keep this connection alive. If we are in keep-alive, then we loop; otherwise, we leave this function, letting serveConnection close the connection.

+

parseRequest

+

Parsing the request headers involves taking all of the header lines (every line until a blank line) and then parsing those via parseRequest'. takeHeaders (a function I will not explain here) goes ahead and reads in all of the header lines until a blank. A special thanks to Gregory Collins for pointing out a security hole in the initial versions of Warp: takeHeaders now ensures that no header is longer than 1024 bytes, and there are at most 30 headers to avoid a DOS attack.

+

There's nothing special about parseRequest': it just reads the headers to determine request method, path info, query string, HTTP version, etc.

+

headers and Builders

+

A quick detour before we hit sendResponse: the headers function takes an HTTP version, a status, a list of response headers and a bool indicating whether the response is chunked, and returns a Builder.

+

Builder, as you might guess, is a datatype from blaze-builder. Although Simon or Jasper could do a better job describing it than I, a Builder is essentially an instruction for how to fill up a memory buffer with bytes. So this function generates some buffer-filling instructions.

+

blaze-builder and blaze-builder-enumerator provide a few different ways of extracting data from a Builder:

+
  • toLazyByteString will lazily fill up strict ByteStrings. This works well if we are constructing our Builder purely. However, if our Builder is constructed via a series of IO actions, we would need to perform all of the actions, store all source data in memory, and only then get to produce our output.

  • +
  • toByteStringIO is similar to toLazyByteString in that it works badly with non-pure data. Instead of producing a lazy ByteString, it calls an IO function for each strict ByteString produced. It can be more efficient since it gets to share memory buffers.

  • +
  • builderToByteString is perfect for IO-generated data: it is in fact an enumeratee which takes an input stream of Builders and produces an output stream of strict ByteStrings. Each time a buffer is filled up, it sends out a new ByteString.

  • +
+

sendResponse

+

sendResponse is where we get to see blaze-builder and its synergy (buzzword!) with enumerator shine. There are three different constructors for a response:

+

ResponseFile

+

ResponseFile which has a status code, a list of response headers and a filepath. We provide this in WAI so backends like Warp can provide an optimization by calling sendfile.

+

We start off by sending the response headers using toLazyByteString and sendMany. This forces all of the data to be pulled into memory and then uses a vectored IO call. In general, this would be a bad move memory-wise; however, since we are only sending headers, which should be small, it's OK.

+

We next call hasBody. I'll explain its usage here, and then pretend it doesn't exist for the other two constructors. In HTTP, there are two ways (that I'm aware of) for a response to be required to have no body: a response to a HEAD request, and a 204 response. In these cases, we simply send headers and finish.

+

When we do have a response body, we call sendFile, which uses a system call to send the entire contents of the file in one system call. Next we determine keep-alive. Keep-alive allows us to reuse a connection for multiple requests. It depends on how we send the response body. There are four ways a response body can be sent:

+
  • No body at all, as mentioned above. In this case, keep-alive works.

  • +
  • A response body with a content-length. In this case, the client knows precisely how many bytes to read after the response headers before it can expect a new response, so keep-alive works.

  • +
  • A chunked response body. Chunked responses were an added feature of HTTP 1.1. They work well when you don't know the full size of a response before sending it. Instead, you send chunks, each with a header giving its size, followed by a 0-header. This 0-header indicates the end of a response, so the client knows to wait for the next response. Therefore, keep-alive works.

  • +
  • A plain old response body. In this case, the only way for the client to know that it has read the entire response body is for the server to close the connection. In this case, keep-alive does not work.

  • +
+

In the case of file serving, we don't want to use chunking, since we then cannot use the sendfile system call. Therefore, if the application provides a content-length, we can keep-alive; otherwise, we must close the connection.

+

ResponseBuilder

+

A ResponseBuilder has a status code, headers and a single Builder. This constructor is not strictly necessary, as all of its funcitonality can be provided by ResponseEnumerator (described next). However, some of our early benchmarks proved that there are performance advantages to ResponseBuilder, so we kept it. In real life, you are much more likely to use a ResponseBuilder than a ResponseEnum, though the latter provides much more power (eg, interleaved IO).

+

For ResponseBuilder, we use toByteStringIO: it allows us to reuse buffers and not pull the entire reponse into memory. We also use blaze-builders chunking code here when there is no content-length header, assuming the client support HTTP/1.1. Therefore, only HTTP/1.0 clients receiving responses without content-length do not support keep-alive.

+

ResponseEnumerator

+

This is the most complicated and most powerful constructor. If you don't entirely understand it, don't worry: I came up with the idea and implemented it, and I still get a little hazy. We're basically taking enumerators- an already complicated concept- and adding more complication.

+

The motivation goes as such: let's say you want to open a database query and start sending results without caching them in memory. But there's a trick: if there are zero results, you want to return a 404 response. If there is at least one result, you want to send a 200.

+

In the world of enumerators, we can't "take a break" from being inside the enumerator: once the code starts running, we have to stay there. Therefore, we need some way to notify the web server, from within a database action, what the status code will be. Thus was born ResponseEnumerator:

+
type ResponseEnumerator a = (Status -> ResponseHeaders -> Iteratee Builder IO a) -> IO a
+
+

This says "I'm a function which will take a function that will give back an Iteratee". Wait... what?

+

Let's try that again: the web server needs to know the HTTP response code and the response headers before it can start sending the response to the client. Once it has the status and headers, it's ready to start receiving data. Remember, in enumerator-land, an Iteratee is a data consumer. So the web server is essentially a function that takes a status and some headers and returns a data consumer: that data consumer then sends chunks of response data out to the client.

+

So we are handing the web server (as that special function) to the web application and giving it control of how things are run. In our database example, the application code will run its database-query-enumerator and start receiving its stream of results. If it receives zero results, then it can tell the web server that the response is 404; otherwise, it can say 200 and status sending chunks of data, one for each result.

+

I know it's confusing, but this offers a lot of power to web applications while keeping the full safety and guaranteed resource freeing of enumerators. And for the most part, you can either sidestep the complexity by using ResponseBuilder, or use higher level tools that put an easy-to-use API on the whole thing.

+

Anyway, let's look at the Warp code for handling this. We define a "go" helper function which takes a status and a list of headers and returns an iteratee (big surprise), and then hand that go function to the ResponseEnumerator. So far so good? Ok, deeper into the rabbit hole.

+

Within go is what I think is the coolest code in the whole package. We define a chunk' helper function: if the response needs to be chunked (ie, the client is HTTP/1.0 and there is no content-length header), it applies a map over the entire response body to add chunking. If the response is not supposed to be chunked, it makes no changes.

+

Next, we pipe this monstrosity- together with the headers- through builderToByteString, we produces a stream of ByteStrings. We then send off this stream to iterSocket, which sends the ByteStrings to the client. Finally, we return a keep-alive boolean, using the same rules as ResponseEnum.

+

Conclusion

+

Well, that's the whole web server. I think that over all it's still fairly high-level. Some of the lower-level bits (takeHeaders, for instance) can be factored out into some helper libraries so that other packages can also take advantage of these highly optimized iteratees.

+

I've hoped for a long time that as a community we can start to rally around WAI so the community's different web frameworks can work together to create great middleware, servers, and interfaces. I hope that Warp demonstrates that not only is WAI nice theoretically, it also provides an incredibly efficient interface for creating fast web servers, and therefore fast applications.

+

If anyone has questions about the Warp code, or how to use WAI from an application side, let me know. I would love to get more frameworks to be able to take advantage of Warp and all the other tools that go along with WAI.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/01/data-object-yaml.html b/public/blog/2011/01/data-object-yaml.html new file mode 100644 index 00000000..1b03c446 --- /dev/null +++ b/public/blog/2011/01/data-object-yaml.html @@ -0,0 +1,65 @@ + data-object-yaml package +

data-object-yaml package

January 15, 2011

GravatarBy Michael Snoyman

I got an email recently asking for some documentation on the data-object-yaml. It's a fairly straight-forward package, with one or two surprises lurking inside, so I think a blog post ought to do it justice. If anyone has questions, just ask it in a comment and I'll try to address it.

+

As a bit of background, this package is built on a few other packages I wrote. yaml is a low-level wrapper around the C libyaml library, with an enumerator interface. data-object is a package defining a data type:

+
data Object k v = Scalar v
+                | Sequence [Object k v]
+                | Mapping [(k, Object k v)]
+
+

In other words, it can represent JSON data fully, and YAML data almost fully. In particular, it doesn't handle cyclical aliases, which I hope doesn't really occur too much in real life.

+

Another package to deal with is failure: it basically replaces using an Either for error-handling into a typeclass. It has instances for Maybe, IO and lists by default.

+

The last package is convertible-text, which is a fork of John Goerzen's convertible package. The difference is it supports both conversions that are guaranteed to succeed (Int -> String) and ones which may fail (String -> Int), and also supports various textual datatypes (String, lazy/strict ByteString, lazy/string Text).

+

YamlScalar and YamlObject

+

We have a type YamlObject = Object YamlScalar YamlScalar, where a YamlScalar is just a ByteString value with a tag and a style. A "style" is how the data was represented in the underlying YAML file: single quoted, double quoted, etc.

+

Then there is an IsYamlScalar typeclass, which provides fromYamlScalar and toYamlScalar conversion functions. There are instances for all the "text-like" datatypes: String, ByteString and Text. The built-in instances all assume a UTF-8 data encoding. And around this we have toYamlObject and fromYamlObject functions, which do exactly what they sound like.

+

Encoding and decoding

+

There are two encoding files: encode and encodeFile. You can guess the different: the former produces a ByteString (strict) and the latter writes to a file. They both take an Object, whose keys and values must be an instance of IsYamlScalar. So, for example:

+
encodeFile "myfile.yaml" $ Mapping
+    [ ("Michael", Mapping
+        [ ("age", Scalar "26")
+        , ("color", Scalar "blue")
+        ])
+    , ("Eliezer", Mapping
+        [ ("age", Scalar "2")
+        , ("color", Scalar "green")
+        ])
+    ]
+
+

decoding is only slightly more complicated, since the decoding can fail. In particular, the return type is an IO wrapped around a Failure. For example, you could use:

+
maybeObject <- decodeFile "myfile.yaml"
+case maybeObject of
+    Nothing -> putStrLn "Error parsing YAML file."
+    Just object -> putStrLn "Successfully parsed."
+
+

If you just want to throw any parse errors as IO exception, you can use join:

+
import Control.Monad (join)
+object <- join $ decodeFile "myfile.yaml"
+
+

This takes advantage of the IO instance of Failure.

+

Parsing an Object

+

In order to pull the data out of an Object, you can use the helper functions from Data.Object. For example:

+
import Data.Object
+import Data.Object.Yaml
+import Control.Monad
+
+main = do
+    object <- join $ decodeFile "myfile.yaml"
+    people <- fromMapping object
+    michael <- lookupMapping "Michael" people
+    age <- lookupScalar "age" michael
+    putStrLn $ "Michael is " ++ age ++ " years old."
+
+

And that's it

+

There's really not more to know about this library. Enjoy!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/01/hamlet6to7.html b/public/blog/2011/01/hamlet6to7.html new file mode 100644 index 00000000..9a858d59 --- /dev/null +++ b/public/blog/2011/01/hamlet6to7.html @@ -0,0 +1,47 @@ + Hamlet 6 to 7 +

Hamlet 6 to 7

January 28, 2011

GravatarBy Michael Snoyman

I wanted to let everyone know that I've made some decisions about the future of Hamlet: basically, I've decided to bite the bullet and make a bunch of breaking changes at once to make Hamlet, Cassius and Julius more consistent and fit in better with Haskell syntax. I'm still unclear on some minor details, so I will hold off on a post explaining the changes until later.

+

However, I wanted to allay some fears: in case you are terrified at the thought of having to convert all of your templates, worry not! This version of Hamlet will include an executable called hamlet6to7 to automate the conversion. I've already used it on the Yesod codebase, and everything seems to be working out fine. Obviously, be smart when using this tool, and make sure you have your code backed up (ie, already committed) when you run it.

+

The program takes a list of files to modify, and writes out a new file in the same location. It modifies files with the file extensions hamlet, cassius, julius or hs. For the first three, everything is pretty straight forward. For Haskell source files, it makes modifications to quasi-quotation blocks. It looks for a few different things:

+
  • GHC 6.12 quasi-quotation ([$hamlet|...|])
  • +
  • GHC 7 quasi-quotation ([hamlet|...|])
  • +
  • CPP conditional code to select which version to run (#if GHC7...)
  • +
  • CPP definitions to switch between 6.12/7 ([HAMLET|...|])
  • +
+

This should work for most everybody; if you find any bugs, let me know.

+

Sample Hamlet

+

I'm actually quite happy with the new Hamlet syntax: I think it is much more readable and much closer to actual Haskell. Here is some sample code taken straight from Yesod:

+
!!!
+
+<html>
+    <head>
+        <title>#{pageTitle p}
+        ^{pageHead p}
+    <body>
+        $maybe msg <- mmsg
+            <p .message>#{msg}
+        ^{pageBody p}
+
+

Lucius

+

One change I specifically did not make was switching Cassius to following Less syntax. I do think this idea has merit, but instead of making a huge breaking change for Cassius, I've deicded to add a new template language, Lucius (name is up for debate btw), which will follow a different syntax. I can add this in for Hamlet 0.7.1, and if everyone likes it, perhapsCassius will get deprecated.

+

When's Yesod coming out?

+

I was actually hoping to have Yesod 0.7 available this week, but life got a little crazy. Also, very recently, I started working on rewriting some major pieces of Yesod's routing code. Previously, I had a huge mess of TH code, bending over backwards to accomodate the web-routes Site datatype, and a very weird split of code between Yesod and web-routes-quasi. I've made a few decisions:

+
  • web-routes is nice, but it doesn't fit Yesod properly.

  • +
  • Dispatching and parsing need to happen simultaneously. This is to allow general WAI applications to be directly embedded as subsites.

  • +
  • Wherever possible, combine the code for subsites and regular sites. I want to minimize the Template Haskell codebase as much as possible.

  • +
+

The impetus for this change was point number two: I believe we're going to be seeing more WAI-specific applications in the future, and one goal I've always had is great interoperability amonst Yesod web applications (that is the reason for the WAI after all).

+

In any event, this code is now written, and fairly well tested. There's a few datatype issues I need to figure out (does joinPath work on Strings of ByteStrings, for example), and I need to update the scaffolder for recent changes, and then we should be good to go.

+

With the new highly modularized breakdown of Yesod packages, we can easily release an update to smaller components like yesod-form later on. So for the moment, there have been no major improvements there, but they can be included in point releases instead of requiring a brand new major release. An example of such a change is migrating yesod-form to use digestive-functors in the future.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/01/i18n-in-haskell.html b/public/blog/2011/01/i18n-in-haskell.html new file mode 100644 index 00000000..2e7a0547 --- /dev/null +++ b/public/blog/2011/01/i18n-in-haskell.html @@ -0,0 +1,34 @@ + I18N in Haskell +

I18N in Haskell

January 14, 2011

GravatarBy Michael Snoyman

I got a feature request from Антон Чешков to allow the messages in the yesod-auth package to be user-customizable. Adding such a feature is fairly simple: I added a whole bunch of methods to the YesodAuth typeclass, one for each method. While that works, I'm not happy with the result. Besides being an ugly solution, there is a much more fundamental problem: it only support one language.

+

Yesod does in theory have i18n support. There is a function for getting a list of languages that the user accepts, and support for using cookies to override the Accept-Language field. This is all well and good, but it's like saying a car supports flying, all you need is a catapult. (Sorry, I couldn't resist a car analogy.)

+

In theory, I think multi-lingual support needs to be added at the template level, in Yesod's case, Hamlet. However, we first need to figure out the right approach to I18N for Haskell in general. Consider what I'm about to present a straw man: I want to hear why this is such a bad idea.

+

Let's start by making a bold assertion: gettext is a huge, ugly hack. Sure, it works fine for static strings like "Hello World". Of course, there's no compile-time checks to make sure you didn't accidently use "Hello world" (lower-case w) or "Hello World!". But that's not even the big hack: imagine I want to output something as simple a "<count> <object>(s)". gettext has trouble with this because:

+
  • We need to be able to pluralize things.
  • +
  • Believe it or not, not every language puts their numbers before their objects. (In Hebrew, for instance, you can say the equivalent of "house one" or "two houses".)
  • +
+

I'm sure others with more gettext experience than I can explain both how to work around these issues, and how there are even worse hacks involved. Frankly, that's not our issue here. Let's see how we can leverage the strengths of Haskell for this. I present my strawman (explanation below):

+ + +

I start off with the MaybeRead typeclass. It's not really i18n specific, it's just used for parsing the language datatype from strings. The meat of this sample is lines 8-12. We define an I18N typeclass with two associated datatypes: a datatype to represent languages, and an output datatype for displaying the messages.

+

So why bother with the language datatype? After all, there are international language codes that we could rely on. The reason is twofold: we can parse our language code once into a Haskell datatype, helping performance, and more importantly, we can pattern match and the compiler will tell us if we missed a translation. I think the message datatype is more obvious: some people will want String, some Text, and some Html.

+

i18n tries to translate your datatype into a message. It returns its value in a Maybe to let you know that there is no translation for the specified language available. There is also i18nDefault, which gives the default translation for a value.

+

Since we can limit the range of languages using the Language associated type, why not just have i18n return the value outside of Maybe and skip the i18nDefault? A few reasons:

+
  • It's likely that people will want to use more generic Language datatypes, which would include languages that their application does not support.

  • +
  • In the context of web applications, you don't just get a requested language, you get a prioritized list of languages. This allows you to try out different languages until you get a match.

  • +
+

The two example helper functions, i18nLang and i18nLangs, demonstrate two standard use cases for this class: translated to a specific language, and translated to one of many languages. The rest of the gist is a simple example of translated messages for a number guessing game.

+

Oh, and since the code to generate the messages is actual Haskell, you can due any kinds of manipulations you want. You are not constrained to the order in which the arguments are provided, you can use any logic you want to make plural words, you can even do complex date formatting or "fuzzy times" if you want.

+

Anyway, it's almost 1 in the morning here, so perhaps this won't be too coherent, but I hope we can come up with a good general solution for i18n in Haskell. Let me know what you think!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/02/announcing-yesod-0-7.html b/public/blog/2011/02/announcing-yesod-0-7.html new file mode 100644 index 00000000..1e75ffcf --- /dev/null +++ b/public/blog/2011/02/announcing-yesod-0-7.html @@ -0,0 +1,48 @@ + Announcing Yesod 0.7 +

Announcing Yesod 0.7

February 5, 2011

GravatarBy Michael Snoyman

On behalf of the Yesod team, I'm very happy to announce the release of Yesod 0.7. This release incorporates a large number of community recommended changes, and introduces some new developers to the team. For those interested in upgrading, please see the Yesod 0.7 migration guide. Some points of interest:

+
  • Yesod now depends on WAI 0.3, which allows us to use the new Warp web server. WAI 0.3 uses enumerator and blaze-builder, which provides for much more efficient response generation than we had previously.

  • +
  • The yesod package has been split up into a number of sub packages. This makes it easier to try out new ideas without breaking backwards compatibility and more accessible for new developers to help contribute (more on this below). The "yesod" package itself provides a Yesod module that exports the same functionality available previously.

  • +
  • Static file serving is now more intelligent, providing for directory listings and index files (both configurable), as well as proper trailing-slash support based on folder/file checking. It is based on the newly released wai-app-static.

  • +
  • We have new syntaxes for Hamlet, Cassius and Julius. The hamlet6to7 tool is available to help migration.

  • +
  • yesod-json now uses json-types, which has a much more robust set of types. In particular, it allows proper creation of booleans, nulls and numbers. yesod-json also provides a compatibility layer for those using the old API.

  • +
  • The routing system has been rewritten almost entirely. This helps with performance, but there is a much more important consideration here: Code duplication between master and subsites has been removed, making the codebase more streamlined, and we now have the opportunity to make "Yesod middlewares." Expect more on that in the future.

  • +
  • In addition to Atom feeds, the yesod-newsfeed package also supports RSS feeds. Thanks on this to the new co-maintainer, Patrick Brisbin. You can also have a very RESTful approach that serves either Atom or RSS from the same URL depending upon request headers.

  • +
  • In general integration with WAI has been improved greatly. There is a new sendWaiResponse function that lets you bypass a lot of Yesod processing, and it is much easier to embed WAI apps directly as subsites. For an example, see the yesod-static package. There will likely be more improvements on this integration in the future, especially since the WAI userbase is growing.

  • +
  • By default, trailing slashes are not appended to generated URLs.

  • +
  • newIdent has been moved from Widget to Handler. This is the more correct place for it, as previously it was possible to generate identical identifiers on the same page.

  • +
  • You can throw exceptions of type ErrorResponse, such as NotFound or InvalidArgs, and Yesod will show the corresponding error page (404 or 400 in this case).

  • +
  • yesod-auth adds a HashDB backend from Patrick Brisbin.

  • +
  • We've spun off two non-Yesod-specific packages: cookie and pool. Hopefully others will find them useful.

  • +
  • Provide yesodVersion function for those gracious enough to want to put a "Powered by Yesod 0.7.0" line on their sites.

  • +
  • Instead of basicHandler, we now have warp, warpDebug and develServer. warp is good for production environments, warpDebug prints some information on each request, and develServer uses wai-handler-devel.

  • +
  • Installing the yesod package installs all dependencies for scaffolded sites. This was a common point of confusion for new users. The one exception is that neither persistent-sqlite nor persistent-postgresql is installed.

  • +
  • All scaffolded executables are all built threaded. By default, the production executable is now based on Warp, not FastCGI, though you are still free to use FastCGI in production (I still am for the moment, see LambdaEngine below).

  • +
  • The scaffold tool checks that entries are valid. For example, it won't let you created a foundation type name beginning with a lowercase letter.

  • +
+

One word of warning: there are two rough areas still with using GHC7:

+
  • The hint package has not yet been ported to GHC7, meaning wai-handler-devel will not work.

  • +
  • There are still some bugs in the new IO manager which can cause intermittent crashes of applications. I recommend using GHC 6.12 in production until GHC 7.0.2 is released.

  • +
+

Next steps

+

As usual, this release is not the end of the road, but just another milestone. To give an idea of what's planned for the future:

+
  • Continue writing the book. My goal is to have the entire basics section (first 10 chapters or so) finished by the middle of March. I appreciate everyone's comments on the book, please keep them coming!

  • +
  • I consider the "Please Break Yesod" campaign a huge success: we've found a huge number of places where Yesod could be made better for the community. I'm hoping to continue this trend. But assuming that most of the desired changes were addressed in this release, then we can start thinking (again) about a 1.0 release.

  • +
  • In addition to Cassius, I hope to add Lucius to the hamlet package. Lucius will be a new syntax that is similar to Less. In other words, it will be a superset of CSS3. It will use the same underlying types as Cassius, so it should be easy to swap between the two of them, or use both in the same project. Anyone interested in helping on this is welcome to contact me.

  • +
  • There will likely be major changes to yesod-form. In particular, I think that polymorphic forms was a bad idea, and I want to investigate basing things on Jasper's digestive-functors. Because yesod-form is now a separate package, it should be possible to experiment with this new approach without moving to a new version of Yesod. I need to investigate more, but I think it's likely that experimental versions will be released under the package yesod-form-dev.

  • +
  • I'm looking for co-maintainers on packages. The idea is to spread knowledge around (you know, just in case I get hit by a bus), get fresh eyes to look at problems, and free up some of my time. With the new smaller-packages approach, I think this should be much easier to achieve. If anyone sees a package that they would like to help out with, please send me an email.

  • +
  • One new project I'm hoping to embark on soon is LambdaEngine. This will be both a library for supporting hot-loading of WAI applications, and a hosting service for WAI apps. My initial goal is to migrate my current set of applications to a new server using this (you may have noticed that my web pages get downtime occasionally, it's almost always that my node has "had issues"). I'm hoping to make this as easy-to-use as possible, and possibly even provide free hosting for apps, though we'll need to see how that works for me budget-wise. Don't expect any big announcements on this immediately, but if you're interested in the idea let me know.

  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/02/chapters-1-4.html b/public/blog/2011/02/chapters-1-4.html new file mode 100644 index 00000000..6b4fa533 --- /dev/null +++ b/public/blog/2011/02/chapters-1-4.html @@ -0,0 +1,17 @@ + Yesod Book: Chapters 1-4 updated +

Yesod Book: Chapters 1-4 updated

February 11, 2011

GravatarBy Michael Snoyman

Just a quick heads-up for those waiting on it: I have updated chapters 1 to 4 of the book for Yesod 0.7. Well, chapter 1 is just an introduction chapter, so that doesn't count. And chapter 2 is the Haskell overview, which just has a bunch of FIXMEs. So I didn't really do anything there.

+

But chapter 3 and 4 have real code, and they've been updated. In particular, chapter 4 has basically been completely rewritten: it completely covers Hamlet, Cassius and Julius. For those interested in the new syntax introduced in 0.7, please check it out.

+

As usual, comments are much appreciated so I know where to fix things.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/02/reverse-packdeps.html b/public/blog/2011/02/reverse-packdeps.html new file mode 100644 index 00000000..e0a4a6ee --- /dev/null +++ b/public/blog/2011/02/reverse-packdeps.html @@ -0,0 +1,18 @@ + Reverse Packdeps +

Reverse Packdeps

February 8, 2011

GravatarBy Michael Snoyman

A number of months ago, I got a feature request from Ben Boeckel to add reverse dependency listings to packdeps. I will admit that, for some reason, I didn't really understand the request at the time, and really had no idea how I would implement it.

+

Then I got lucky: in the same week, Ben asked me if I had made any progress on this feature, and Simon Hengel announced HackageOneFive. After some follow up with Ben, and some advice from Simon, I was finally able to implement the reverse dependencies packdeps.

+

Just to head off the inevitable question: no, this is not the exact same thing as Hackage 2.0. The main goal of reverse packdeps is to tell you which packages rely on an old version of your package. In Ben's case, this was so that when making packages for Fedora, it would be easy to find out what libraries needed to be updated. Hopefully package maintainers will also find this useful to know if their new versions are catching on.

+

If anyone has ideas for improvement, let me know.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/02/routing-changes-yesod-0-7.html b/public/blog/2011/02/routing-changes-yesod-0-7.html new file mode 100644 index 00000000..0adee94f --- /dev/null +++ b/public/blog/2011/02/routing-changes-yesod-0-7.html @@ -0,0 +1,76 @@ + Routing Changes in Yesod 0.7 +

Routing Changes in Yesod 0.7

February 9, 2011

GravatarBy Michael Snoyman

One of the major changes in Yesod 0.7 is how routing works under the hood. From a user standpoint, not too much has changed, but it's important to understand to some extent how the new system works. This post will explain how Yesod used to work, why it changed, and describe two bugs that came up recently. Hopefully the bugs will give a deeper understanding of what the system is doing.

+

Brief History Lesson

+

Back in the old days of Yesod 0.6 (you know, last week), routing consisted of:

+
  • Convert a path info (eg, /foo/bar/baz/) into a list of strings (eg, ["foo", "bar", "baz", ""], notice the empty string). Thanks to Jeremy Shaw's excellent web-routes package for the utility functions for this.

  • +
  • Apply cleanup rules: in order to ensure canonical URLs, we need to make sure to redirect non-canonical URLs to their canonical form. Though this was a user-controlled setting, the default settings ensured:

    +
    • There are no double slashes.

    • +
    • If the last path segment had a period in it, it must not be followed by a trailing slash. (eg, /file.txt is good, /file.txt/ is bad).

    • +
    • Otherwise, there must be a trailing slash (eg, /folder/ is good, /folder is bad).

    • +
  • +
  • If the URL needed cleanup, send the user a 301. Otherwise, continue.

  • +
  • Convert the path segments into a type-safe URL.

  • +
  • Dispatch based on the type-safe URL value.

  • +
+

This seems to work out well, except for a specific use case: subsites. Point in case, let's say we have the static subsite. In an ideal world, the following should all happen:

+
  1. /static/file.txt serves the file file.txt
  2. +
  3. /static/folder/ shows a directory listing for folder
  4. +
  5. /static/file_no_extension show file_no_extension
  6. +
  7. /static/file_no_extension/ redirects to /static/file_no_extension
  8. +
  9. /static/foo.folder/ shows a directory listing for foo.folder
  10. +
  11. /static/foo.folder redirects to /static/foo.folder/
  12. +
+

However, since the cleanup rules are applied before dispatching to the subsite, we can only apply our default rules, which have no way of knowing that file.txt and file_no_extension should both be missing a trailing slash, while folder and foo.folder both require a trailing slash.

+

The new approach

+

The trick is we need to give subsites complete control over their route handling cleanup. Therefore, the new process goes something like this:

+
  • Convert the path to pieces, like before.

  • +
  • Attempt to dispatch.

  • +
  • If dispatch fails, see if the route can be cleaned up at all. There are three possible results:

    +
    • cleanPath returns a Left value. This means we should redirect the user to the given path.
    • +
    • cleanPath returns a Right value which is different than the current segments. This means that the URL could be cleaned up, but Yesod should not require a redirect, so we try dispatching on this new set of segments.
    • +
    • cleanPath returns a Right value which is the same as the current segments. In this case, dispatch simply won't work, and we should return a 404.
    • +
  • +
  • The new default cleanup rules are much simpler: don't allow any empty path segments. This essentially means no double slashes, and no trailing slashes.

  • +
+

As you can see, this simplifies things greatly. Under this pattern, subsites like static are able to force their own canonical URLs, while the master site can still retain its version of cleanups.

+

Bug 1: /book/

+

The first bug with the new system reared its head on this very website: going to http://docs.yesodweb.com/book/ would previously give the list of chapters in the Yesod book. With the new system, we no longer have the trailing slashes, so a request for /book/ should redirect to /book. Instead, the page was serving 404s.

+

However, a request to /about/ did redirect to /about, so the problem was much more subtle than I'd initially imagined. It turns out that the following was happening:

+
  • /book/ was translated to ["book", ""]
  • +
  • ["book", ""] was compared against all of the routes. It did not match the BookR route, which would expected ["book"]. However, it did match ChapterR, which expects ["book", ]. This is because the SinglePiece instance in web-routes-quasi matched empty strings.
  • +
  • Yesod dispatched to the getChapterR function, giving it a parameter of "". However, there was no chapter named "", so getChapterR returned a 404.
  • +
+

The solution: release a new version of web-routes-quasi that does not match empty strings. Problem solved.

+

Bug 2: can't force a trailing slash

+

I got a question from Dmitry Olshansky about restoring the previous set of URL cleanup rules. The basic approach is simple: write a custom cleanPath function in your Yesod typeclass which forces trailing slashes. For example (ignoring the period-in-last-piece issue):

+
cleanPath _myapp pieces =
+    if pieces == corrected
+        then Right pieces
+        else Left corrected
+  where
+    corrected = (filter (not . null) pieces) ++ [""]
+
+

However, when I tried this, all that happened was that no redirecting occurred: both a request for /foo/ and for /foo would return the same result.

+

The problem now is that dispatch happens before cleaning. /foo turns into ["foo"], which matches a resource, and therefore dispatch succeeds. /foo/ turns into ["foo", ""], cleanPath turns that into Right ["foo"], and the second phase of dispatch succeeds.

+

So we have a predicament: we put dispatch before cleaning to solve the cleanup rules for subsites. But doing so breaks the dispatch rules for non-subsite routes (henceforth called local routes). The solution:

+
  • First, we dispatch to subsites. If a subsite takes the bait, we're in good shape.
  • +
  • If no subsites match, we apply cleanPath. At this point we do any 301 redirects that are necessary. If not, we continue dispatching to the local routes.
  • +
  • If nothing matches, return a 404.
  • +
+

I'm actually quite satisfied with this as the final answer: it does not require any double-dispatch attempts, and it gives subsites full control over what their URLs look like.

+

The test suite has begun

+

Greg Weber has long been pushing me to start a proper test suite for Yesod. With these two bugs, this new test suite has officially begun. As anticipated, the test suite is built on top of wai-test. I'm hoping that both libraries will improve as a result of this push.

+

My goal going forward is to create a test case for each reported issue. I want to start the focus on yesod-core, and then expand into some of the additional libraries, especially yesod-form. So let's try and continue the "Please Break Yesod" campaign.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/02/warp-speed-ahead.html b/public/blog/2011/02/warp-speed-ahead.html new file mode 100644 index 00000000..37dec0e7 --- /dev/null +++ b/public/blog/2011/02/warp-speed-ahead.html @@ -0,0 +1,39 @@ + Warp Speed Ahead +

Warp Speed Ahead

February 15, 2011

GravatarBy Michael Snoyman

By Greg Weber

+

The Yesod team is proud to announce a more stable, performant 0.7.1 release. The new Warp web server was as fast as promised, but suffered from a timeout deadlock bug. Not only has that been squashed, but the fastest Haskell web server just got even faster: Matt Brown's improvements made Warp 20% faster on the pong benchmark!

+

Other minor bug fixes have been committed to Yesod, so we believe the release is very solid now. Please upgrade! A new version of hint for GHC7 has just been released, so go ahead and develop on 7 if you want. We are waiting for the 7.0.2 release (in a few weeks hopefully) before recommending using GHC7 in production.

+

One of the most visible 0.7 changes was with hamlet syntax. We hope that this new syntax will

+
  1. lower the barrier to entry
  2. +
  3. appeal to a wider variety of programmers and
  4. +
  5. appeal to designers.
  6. +
+
<table#an-id.a-class style=display:none;>
+     <thead>
+       <tr>
+         <th>fruit
+         <th>color
+     <tbody>
+       <tr>
+         <td>
+           <a href=@{FruitR fid}>#{fruitName f}
+         <td>#{fruitColor f}
+
+
+

There is no need to learn a radical syntax here. Just by making white space significant and removing closing tags, we eliminate the main source of invalid html. With that, and some id/class shortcuts, and making attribute quoting optional, we have eliminated the main sources of tedium in html.

+

We think Yesod has a lot of great code, and we want to share it with the community. Yesod 0.7 is more modular than ever- it is now broken up into several main packages (yesod-core, yesod-form, yesod-auth, yesod-static, etc), and there are new packages being split out (cookie and pool in this release). We aren't just throwing things over the wall here- some of these we are hoping will become widely used even outside the web development community. json-enumerator is already a fast json generator, and we are planning on making it a complete json implementation.

+

Part of the modularity of Yesod is being built on top of an improved low-level http interface (WAI 0.3). This interface is mostly about sharing code in the web development community, but we also feel it is going to become an option to code apps directly against Wai when you aren't happy with the abstractions Yesod provides or if you are creating a very simple application.

+

Yesod has been a fully-featured, highly productive framework for some time now. But we haven't been content, and we keep striving to make things better. We know that it needs to be easier for new users- and we are now putting a big focus on documentation before making a 1.0 release.

+

The Haskell web development scene in general is starting to get legs. We think 2011 will mark the start of serious web development in the Haskell community. We are excited to be a part of it, and to be able to offer Yesod 0.7.1 to the community today.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/02/yesod-migration-guide.html b/public/blog/2011/02/yesod-migration-guide.html new file mode 100644 index 00000000..0cc88546 --- /dev/null +++ b/public/blog/2011/02/yesod-migration-guide.html @@ -0,0 +1,26 @@ + Yesod 0.7 Migration Guide +

Yesod 0.7 Migration Guide

February 4, 2011

GravatarBy Michael Snoyman

Well, Yesod 0.7 is almost ready for release. Thanks to everyone for some great ideas: I consider the "Please Break Yesod" campaign to be a huge success. There have been a huge number of API tweaks, feature enhancements, and all around goodness.

+

I have just converted the Haskellers codebase to Yesod 0.7, and have taken detailed notes on changes you'll need to make. The process is not that bad really: it took about half an hour for me to convert the code and write up the docs.

+

So that we can get inline commenting, I've put the guide up as a chapter of the Yesod book. I'm planning on making the actual release Saturday night Israel time, assuming I don't get any major bug reports before then.

+

I will hold off on the list of new features for the release announcement. However, I have just released some new packages: Hamlet 0.7, Persistent 0.4 and wai-handler-devel 0.2. Some quick comments:

+
  • Hamlet 0.7 involves a lot of new syntax changes. I appreciate the community's feedback on this: I think we've created a much better product. Check out my previous blog post for migration information, and the Wiki page for some of the surrounding discussion.

  • +
  • Persistent saw no real changes. The main reason for the new major version number is the migration to the monad-peel package. By the way, that package is a work of art, I strongly recommend people start looking into it as a replacement for MonadCatchIO-*. Anders has done an amazing job.

  • +
  • wai-handler-devel has a much more elegant underlying approach (which if I had time I would blog about). It's actually getting close to a proper plugin system, which is something I'm interested for my upcoming LambdaEngine project. </tease>

  • +
+

New site design

+

I've also finally revamped the Yesod site to use the new Haskell color theme that's been going around. I personally like the change, but wouldn't mind any critiques. We also have a new Yesod logo. If it looks a little funny to you, flip it upside down, it should look more familiar that way.

+

Yackage

+

For those of you who can't wait, or who want to help me out with some testing, you can get the most current version of the packages from Yackage. If you find any bugs, please let me know!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/03/hamlet-lucius-widgets.html b/public/blog/2011/03/hamlet-lucius-widgets.html new file mode 100644 index 00000000..34232241 --- /dev/null +++ b/public/blog/2011/03/hamlet-lucius-widgets.html @@ -0,0 +1,55 @@ + Hamlet, Lucius and Widgets, oh my! +

Hamlet, Lucius and Widgets, oh my!

March 23, 2011

GravatarBy Michael Snoyman

I've just released version 0.7.3 of the hamlet package, which introduces two noteworthy changes. The first is improved conditional class support. For those who are not familiar, Hamlet allowed you to put in conditional attributes like so:

+
<option :isSelected:selected=true>My Option
+
+

In this case, if the value of isSelected is True, then the option tag will have a selected attribute with a value of true. option and input[type=checkbox] tags were the original use case for this feature. But another common use is with classes, e.g.:

+
<a href=@SomeRouteR@ :isCurrentRoute SomeRouteR:class=current>
+
+

The problem with this is that it doesn't play nicely with the .class syntax Hamlet provides: using both would mean that you end up with two class attributes, which means one will be ignored by the web browser. The correct approach is to space-separate your classes. Hamlet 0.7.3 makes two changes to help you out here:

+
  • If you have a class=someclass and a .otherclass, Hamlet will automatically combine them into class="someclass otherclass".
  • +
  • You can now put conditionals on .classes, eg: <a href=@SomeRouteR@ :isCurrentRoute SomeRouteR:.current
  • +
+

Thanks to Blake Rain for bringing this up.

+

The other new feature in this release is Lucius: a CSS template language. This is meant as a sister language to Cassius. While Cassius uses whitespace for delineation, Lucius uses braces and semicolons. This means that you can insert CSS verbatim into Lucius and it will work.

+

There are still lots of features to be added: @media and @import don't work yet, for instance. And then there's some extra features we would like to implement from Less, like nested blocks. For the moment, Lucius and Cassius cannot be mixed within the same template, though they use the same datatypes and can therefore be passed to the same functions. Is there desire in the community to allow them to live in the same templates?

+

And finally, we come to widgets. Sven Koschnicke put up a very good example of a horrible performance issue in Yesod. This was a great example of how to properly debug a performance bug. Or rather, it was a reminder to me that I should never assume I know where the bug was coming from. My testing basically came down to:

+
  • Oh, he's doing lots of DB code. That must be the problem. (Do some profiling...) Oh, that's not the problem.
  • +
  • Oh, replacing addWidget with addHtml speeds things up dramatically. Must be a polymorphic Hamlet bug caused by the inliner. (More profiling...) Oh, wrong again.
  • +
  • Huh, I forgot that I programmed the implementation of Widget when I was drunk.
  • +
+

Turns out the real problem was the 8-level monad transformer stack powering widgets. Instead of that, I simply replaced it all with a RWS (read-write-state) transformer with a single datatype containing all the data previously in the stack. As is usually the case, I think I went for the transformer stack approach due to some misguided ideas on performance. As usual, if you want to know about the performance of something, you have to benchmark. So here are the results of the Widget bigtable benchmark:

+ + + + + + + +
Criterion BigTable results, microseconds (lower is better)
Version +Time +
Original code +23,700 +
Lazy RWS, non-strict datatype +141 +
Strict RWS, non-strict datatype +298 +
Lazy RWS, strict datatype +101 +
Strict RWS, strict datatype +102 +
+ +

Yes, those numbers are real. All Yesod users should probably send Sven a beer for helping discover this bug. I've released version 0.7.0.2 of yesod-core to change the definition of Widget appropriately, using the lazy RWS transformer and a strict datatype.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/03/improving-persistent-performance.html b/public/blog/2011/03/improving-persistent-performance.html new file mode 100644 index 00000000..bf25a013 --- /dev/null +++ b/public/blog/2011/03/improving-persistent-performance.html @@ -0,0 +1,25 @@ + Improving Persistent Performance +

Improving Persistent Performance

March 25, 2011

GravatarBy Michael Snoyman

Continuing from our last blog post, Sven's benchmark went from being 14 times slower than Rails to 3 times slower. I originally assumed that the database code was the major bottleneck. It turns out that I was almost right: there was a much bigger performance leak in the Widget code. But the database code was contributing greatly.

+

The first thing I wanted to try out was replacing Persistent with direct HDBC code. Sure enough, runtime went from about 12 seconds (for 25 requests) down to 2.5 seconds. As long as I was at it, I decided to test out another theory I've mentioned some times, and switched to properly prepared queries with libpq. To my surprise (and disappointment), it didn't result in any major performance difference. So much for that.

+

But I was still left with the question: what was slowing down Persistent so much versus raw HDBC? I considered the fact that Persistent was reading every column instead of just the necessary rows. But changing the HDBC code to do the same resulted in almost the same performance. I also tried using quickQuery (and its strict sibling, quickQuery') instead of prepare; also no change.

+

So I wrote a little test script and went to work on Persistent. If I just prepared the statement, and didn't actually read the data, everything worked out fine. I tried fetching all the rows at once, or using HDBC's lazy functions. Still no change.

+

I looked at the profiling results again. It said that the pFromSql function was being called 220,000 times: 1000 rows * 11 columns * 20 runs. pFromSql converts from HDBC's SqlValue datatype to Persistent's PersistValue datatype. (This level of abstraction is one of the reasons I'd like to switch to a lower-level DB library.)

+

What caught my eye was the FIXME line of code. If you look at pFromSql, it does most of its work by pattern matching. For the most part, it's a simple translation: SqlString x becomes PersistString x, all types of integers get mapped to PersistInt64, etc. But the last clause uses HDBC's fromSql function to try and deal with any unhandled constructors.

+

I replaced that line with pFromSql x = error $ show x, ran my test, and got an error message about the SqlLocalTime constructor. I added an extra clause to pFromSql:

+
pFromSql (H.SqlLocalTime d) = PersistUTCTime $ localTimeToUTC utc d
+
+

and ran my test... it finished almost instantly. I went back to Sven's test case and ran it again: runtime was on a par with the pure-HDBC version, just under 3 seconds (down from our original 12). I don't have a Rails setup on my system, so we'll have to wait for someone else to confirm that Yesod is in fact running faster than Rails on this test.

+

persistent-postgresql 0.4.0.1 has been released, and I recommend everyone upgrade. And please keep sending in these benchmarks: it's much easier to make Yesod better when we know where it needs improvement.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/03/preliminary-warp-cross-language-benchmarks.html b/public/blog/2011/03/preliminary-warp-cross-language-benchmarks.html new file mode 100644 index 00000000..bd21facc --- /dev/null +++ b/public/blog/2011/03/preliminary-warp-cross-language-benchmarks.html @@ -0,0 +1,27 @@ + Preliminary Warp Cross-Language Benchmarks +

Preliminary Warp Cross-Language Benchmarks

March 17, 2011

GravatarBy Michael Snoyman

Ever since the first set of Warp benchmarks went live, people have been curious to see how Warp compares against non-Haskell web servers. Instead of simply throwing together some benchmarks on our local systems, Greg Weber, Matt Brown and I (Michael Snoyman) decided to create a fully reproducible set of benchmarks. For those of you dying of curiosity, here are the results, the explanation follows.

+

Pong benchmark, extra large instance, requests/second

+

All of the code that went into this benchmark is available in a Github repo. The file setup.sh is actually a shell script which will install all dependencies for the benchmarks, pull the repo and run the tests. The script uses apt-get, and in theory may work for any Debian derivitive, but has only been tested using the Ubuntu AMI.

+

The test results above were run against an Amazon EC2 extra large instance. I ran the tests on a micro instance as well. However, due to lower RAM, some of the non-Haskell servers fared very poorly, and I decided to only publish the extra large instance results for now. As usual, results will vary based on the setup you run it on, but so far in every setting Warp comes in at least twice as fast as the next contender (excluding Yesod... I'll explain that in a second).

+

Now the caveats:

+
  • I'm not an expert on Ruby, Python, PHP, node.js or Java. (I'm not even sure if I'm an expert in Haskell.) We've all tried our best to set up the most fair comparison, but obviously there might be better ways to tweak things.

  • +
  • I do not believe that Goliath (Ruby), Tornado (Python) or node.js are capable of scaling up to four cores, so to some extent the extra large instance is biased against them. Of course, on the other hand, this is a great demonstration of Haskell's natural ability to scale.

  • +
  • I may not have chosen the best servers to represent each language. In particular, I have heard mixed reviews of Winstone as a representative Java servlet container.

  • +
  • The main goal here is to compare web servers, not web frameworks. However, in the case of Happstack and Snap, the two are tied together very closely. I therefore also included a Yesod test, which tests Yesod + Warp. (You can argue that this isn't a fair comparison for Yesod, since the Snap/Happstack versions to the best of my knowledge are performing no route parsing.)

  • +
  • And in the near future, Happstack will be switching over to WAI/Warp, so the numbers presented here will no longer be relevant.

  • +
+

So for the moment, I am calling these numbers preliminary. I have given clear instructions on how to reproduce the results on your own EC2 instance: just download setup.sh and run it. I encourage everyone who is interested in this to either improve the results of an existing benchmark, or include new benchmarks in the mix (Erlang and C# have both been requested, I simply did not have time to get to them.)

+

Also, I would like to include some bigtable benchmarks in the next run as well, but I do not feel skilled enough to write efficients versions of that benchmark in most languages. If people contribute that code as well we can run that next time.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/03/road-to-yesod-1-0.html b/public/blog/2011/03/road-to-yesod-1-0.html new file mode 100644 index 00000000..1d364aaa --- /dev/null +++ b/public/blog/2011/03/road-to-yesod-1-0.html @@ -0,0 +1,66 @@ + The Road to Yesod 1.0 +

The Road to Yesod 1.0

March 7, 2011

GravatarBy Michael Snoyman

Our personal and professional lives sometimes make demands on us that slow down our dreams. The frantic pace of Yesod development temporarily slowed to mostly just keeping up with patches. But the slowdown made everyone reflect on where we are going to focus our effort now that we pick the pace back up.

+

Seven of Nine

+Seven of Nine

We are very proud of the 0.7 release.

+
  • The new hamlet syntax appears to be a hit. Once you write html this way you won't want to go back to normal templating.

  • +
  • The modularity introduced makes future developments much easier.

  • +
  • Coinciding With the release of the fast Warp web server, deployment got much nicer, and WAI got a lot more attractive. By the way, GHC 7.02 was released with important bug fixes- please deploying Warp with GHC 7.02.

  • +
+

We will continue making 0.7 point releases, and are planning a 0.8 and 0.9 releases before making a 1.0 release.

+

1.0 changes

+

Documentation

+

Perhaps the biggest piece of feedback we have received is a need for better documentation. We have been making steady progress on this, but a 1.0 means a completion of the Yesod book.

+

JSON

+

In our last post we mentioned plans to develop json-enumerator. In the meantime, Bryan O’Sullivan sped up his just released json package called aeson. With Bryan's name behind this fast, feature-rich package, we feel it is going to become the default JSON package. As much as we love enumerators, we feel that integrating with this package is the right move. If the need arises we should be able to place an enumerator interface on it in the future.

+

Testing

+

It is easy to get lulled into a false sense of security by Haskell's types. But the reality is that when you are developing something as complex as a web framework, there are going to be bugs. We know that we can save time by writing tests. They find bugs, and they document code. One of the reasons for a lack of testing is a lack of convenience. Testing code should be as easy as writing it. Now that cabal can build test suites for a package, a big obstacle has been removed. You have to compile from head for this feature, but it should be released soon. Hopefully cabal-dev will support this soon. The recent release of wai-test makes testing WAI application much easier. It is already being put to use and discovering bugs.

+

Testing isn't just for a framework, but also for framework users. We want to provide a testing setup in the scaffolded site, along with testing helpers to lower the barrier to testing for Yesod users.

+

Templating

+

Cassius, meet Lucius

+

Currently we provide a whitespace-sensitive syntax for creating CSS. While this works well for many cases, it has hurt us in two ways:

+
  • You can't simply copy-paste your CSS content into a Cassius file.

  • +
  • It is very tricky to properly support some more advanced CSS-template language features, such as nesting.

  • +
+

Instead of ditching the whitespace syntax of Cassius, we've decided to augment it with a brace-style syntax called Lucius. We'll be looking for community input on which syntax is nicer, but for the moment the plan is to support both.

+

At the same time, we plan to add first-class support for media types, and include more CSS helper functions. We already provide data types for colors and unit measurements, and intend to add more as time goes on. This is also a great place for new contributers to get started.

+

Generalized Julius.

+

Julius (javascript templater) is a pass-through filter- it knows nothing of the semantics of the file, other than that it needs to make substitutions. This can be generalized to template any file. This is similar to how StringTemplate is used now, but you can enjoy type safety and much more convenient interfaces.

+

Forms

+

We are still strongly considering a major overhaul of the forms package. In all likelihood, we'll be migrating it to be based on the digestive-functors package. We are hoping to avoid any major API changes in the process, but hope to gain:

+
  • more human-readable compile-time error messages

  • +
  • a stronger base, with more opportunities to interoperate with other packages.

  • +
+

Faster javascript

+

Loading javascript files from the head tag blocks page loading. Particularly in a high performance framework like Yesod, this can become the bottleneck. We are going to be adding asynchronous javascript loading, like require.js. As with other things Yesod, we plan on supporting any client-side library a programmer wishes to use, but pick the best option available for the recommended approach.

+

Caching support

+

What good is a fast server if files aren't being cached? Instead of needing to setup caching on a proxy server, wai-app-static will add caching headers itself. At a minimum this is useful for creating a more production-like environment when doing local development. But even more significantly, this clears the way for a pure Warp deployment with no proxy server. This lowers the barrier to entry, but may not be for everyone- Apache and Nginx will always offer a lot more features. As always, by being built on WAI, Yesod lets you keep your deployment options open.

+

Persistent

+

The MongoDB backend compiles, but hasn't been given a final push just because our priorities have been elsewhere. It will be ready for the 1.0 release. There is also work going on for a MySQL backend, and discussions of sharding and caching support.

+

By the way, if anyone is interested in helping out, the PostgreSQL backend would like to switch from HDBC to using the C API directly. We think this will offer some major performance advantages. If you'd like to help out, let me know, this is a great way to work on a small corner of Yesod with huge benefits for all its users.

+

Internationalization

+

There have been a few discussions and blog posts regarding i18n support. We have purposely avoided putting in a complete i18n solution so far, since we did not think the design space was properly explored yet. At this point, I think we have enough information to make an informed decision. Type-safe, powerful I18N will be a cornerstone feature of Yesod 1.0.

+

Scaffolding

+

The current scaffolding tool generates a very powerful project with Persistent and authentication support. While this is very nice, it can be overkill for a lot of projects. We're planning on adding an extra scaffolder that will produce a simplified project with less dependencies.

+

Better support for static html

+

We said above that Hamlet is addictive: its light-weight syntax and type-safe URLs are hard to beat. So much so that people want to use Hamlet for creating their static pages as well. We're looking into better support for this.

+

Backend changes

+

What's the right datatype for a URL? There's the URI datatype defined in the network package, but what about when you want to send it over the wire, like in a redirect response? String and Text are not correct, since you can't use arbitrary Unicode charcters. ByteString isn't correct, because it's textual data (and cannot have the eighth bit set).

+

Problems like this have prompted the creation of the ascii package. We're planning on evaluating a number of uses of ByteString and String throughout the Yesod ecosystem, such as yesod-core and mime-mail, and replace them with the Ascii datatype. This is more semantically correct, and will hopefully help avoid bugs.

+

This change will also affect http-enumerator... oh, and while we're on the subject, http-enumerator is getting keep-alive support soon. That's going to make the best Haskell HTTP client solution even better.

+

Implementing The Road-map

+

This list does not exclude anything. Some things are more tentative than others, and it will keep evolving as we listen to feedback. We just want to communicate to Yesod users where we are planning on focusing our efforts and what they can expect in the future.

+

The Yesod community is growing, and the number of casual contributors is growing. If you have a need and are willing to write a patch for it, there is no reason why it can't be in the 1.0 release. We will work to help you understand the code, review your patch, and get it included. Even if you don't have a burning issue, you are welcome to come hack with us on a real-world code base that is large, but still fun Haskell code. It is a great way to learn about haskell or web programming.

+

We are excited to see a 1.0 on the horizon. But 1.0 is just a lonely number taken to the first decimal place- we attach two meanings to 1.0. The first is API stability. The second is a complete framework- you won't come across any major weaknesses. As with any Yesod release, you can can expect a safe, fast, and highly productive framework.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/04/announcing-wai-0-4.html b/public/blog/2011/04/announcing-wai-0-4.html new file mode 100644 index 00000000..7e5c62fa --- /dev/null +++ b/public/blog/2011/04/announcing-wai-0-4.html @@ -0,0 +1,27 @@ + Announcing WAI/Warp 0.4.0 +

Announcing WAI/Warp 0.4.0

April 4, 2011

GravatarBy Michael Snoyman

We're happy to announce the 0.4.0 release of WAI, along with many of its associated packages, including Warp. WAI is the Web Application Interface, providing a uniform layer between applications and backends. WAI is the foundation of Yesod and the future foundation of Happstack, and also provides backends for non-framework web applications. Warp is the premiere WAI backend, providing a very fast standalone server.

+

While the overall interface is almost identical, this new release provides a few nice enhancements:

+
  • Instead of defining our own datatypes, WAI uses the http-types and case-insensitive packages. (Thanks to Aristid and Bas.) In addition to types, http-types provides a number of convenient functions, such as query parsing/rendering. Additionally, many of the special values, like all the status values, are now provided by http-types instead of WAI, making the Network.Wai module much smaller.

  • +
  • The queryString and pathInfo records in Request have been renamed to rawQueryString and rawPathInfo, and parsed versions of these fields are now provided under the previous name. This should allow middlewares to function more efficiently (they won't all need to parse the same query string), and assist with deferred routing.

  • +
  • The errorHandler record was removed. It was poorly named, not implemented well, and never used. Error logging should be handled at the application level instead.

  • +
  • Support for partial file support was added.

  • +
+

For the most part, associated packages have had little to no changes in their external behavior, outside of adapting to the new version of the interface. For Warp, I'd just like to point out two changes:

+
  • There is now a settingsHost setting, which allows you to specify which host to bind to. Previous versions of Warp simply used the listenTo function. The default behavior is to listen on all hosts.

  • +
  • The previous set of run* functions were a bit of a hodge-podge. We now have consistent naming: run, runSettings, and runSettingsSocket.

  • +
+

Yesod

+

For those curious about Yesod: the 0.8 release will support this new version of WAI, and will hopefully be released within the next week and a half. The upcoming release is mostly about smoothing out some type decisions in APIs, and adding some requested features throughout the codebase. The new version should hopefully introduce very few breaking changes.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/04/announcing-yesod-0-8.html b/public/blog/2011/04/announcing-yesod-0-8.html new file mode 100644 index 00000000..a8f7c044 --- /dev/null +++ b/public/blog/2011/04/announcing-yesod-0-8.html @@ -0,0 +1,88 @@ + Announcing Yesod 0.8 +

Announcing Yesod 0.8

April 19, 2011

GravatarBy Michael Snoyman

By Greg Weber

+

Yesod 0.8

+

For a change-log see the beta announcement, although we sneaked in some features since the beta release. The migration guide goes over what you need to change in your application.

+

Originally Yesod 0.8 was mostly about internal consistency- the main goal was to remove the use of String and instead using Text or ByteString everywhere. But some nice features have managed to get into this release.

+

With every release of Yesod, we see more new contributors. Thanks to Chris Casinghino: he caught 3 bugs in the beta release (1 of them being a GHC bug). The other two required breaking API changes to fix. Also a big thanks to Rick Richardson- without his help we wouldn't be able to announce perhaps the biggest feature of this release: preliminary support for MongoDB in Persistent! This was a great example of collaboration in Yesod. There was an original implementation written quite some time ago. Rick finally picked it up and pushed it to completion, consulting with us along the way.

+

Also, thanks to Aur Saraf for the initial SQL join code, and for contributing a lot of good ideas for the final design.

+

Persistent

+

MongoDB support

+

Persistent is a universal, type-safe data store interface for haskell. Persistent was always designed with newer databases (NoSQL) in mind, but only a sqlite and postgresql backend were implemented. The addition of the MongoDB backend forced some under the hood changes, but has validated the extensibility of persistent. And it is a great case of the advantages of Persistent. By default, MongoDB is schema-less, and the haskell driver supports this as it should because some use cases can take advantage of this. However, for most use cases there is a known schema, and you are always a typo away from inserting the wrong key or querying a key that doesn't exist. With Persistent you know at compile time this won't occur. You also get automatic conversion from the driver type to a normal Haskell ADT that you define.

+

This is an alpha release. Some important features of MongoDB do not have full support yet. In particular, while we support embedded lists, maps, and primitives, embedding another ADT is not yet supported.

+

If you haven't tried MongoDB yet, it is perhaps the most feature-rich NoSQL database, and a great general purpose web application database for applications that don't need transactions. The main motivation for its use instead of a traditional SQL database are speed and easier scalability. The 1.8 release of MongoDB adds single-server durability, so it is still a great choice for small-scale web applications.

+

Join modules

+

Persistent is designed for high scalability- we have always thought about application-level joins first. This release adds both application level joins and SQL joins- take your pick and change it up when the need arises.

+

Future work

+

As mentioned above, there is still work to be done to fullly support MongoDB. More types of joins can be added also. Users generally want more database specific features. While we can add these features, it is also important for Persistent to make it easier to drop down and run queries that it cannot generate, but still receive haskell ADTs in response, or to support the addition of raw queries to what it generates. Persistent is a great library for the haskell community, and adding the next data store is always a fun, self-contained project to embark on. One direction we have always been thinking about is having key-value stores support the key-value portion of the Persistent API. I want to stress that while most of Persistent development does occur within the Yesod community, there is nothing specific to Yesod about it. And there is nothing stopping you from using it in a different web framework, or on a project that has nothing to do with web development.

+

Template languages

+

Miscellaneous

+
  • Attributes are now allowed on script and link tags created by widget functions.
  • +
  • addJuliusBody (Javascript inside the body of the page).
  • +
+

Lucius is now a first-class css templating language

+

The Hamlet template family originally include Cassius- a way to tersely describe css using whitespace layout.

+
table tbody
+    width: 100px
+table tr
+    height: 20px
+
+

The advantage of Cassius is a simple syntax that bypasses the need for brackets and semi-colons. We do love cassius for writing new css, but there is a cost to re-formating old css or re-formating cassius to normal css for use elsewhere.

+

Lucius uses familiar hamlet-style variable insertion, but it is a superset of css. The goal is that you can copy and paste any valid css into a lucius template. This release improves css compatibility and gives lucius the benefit of nesting:

+
table {
+  tbody { width: 100px; }
+  tr { height: 20px; }
+}
+
+

Hamlet adds conditional classes

+

Hamlet is really a joy to work with, but we still find things to improve. This release adds conditional classes. The following will conditionally add a class of "current".

+
<a href=@{MyRouteR} :isCurrentRoute MyRouteR:.current>.
+
+

Generalizing the Hamlet family of templates

+

At its core, the Hamlet family of templates are a new (for haskell at least) style of compile time, type-safe, easy to use templates. Hamlet templates automatically capture the variables in their environment so you don't have to waste time re-defining what is inserted. This makes their ease of use on par with dynamic languages, but we can do all the parsing at compile time and still have type-safe insertions.

+

Hamlet (html), cassius, and lucius (css) have specific parsing rules. However, Julius (javascript) has always been a pass-through template: variables are interpolated, but the source text is unmodified. We realized this was a starting point to extend the Hamlet family to other file types, and re-factored Julius. There is now a coffeescript (great language that compiles cleanly to javascript) module, and adding a template over any new file type is a fairly straight-forward task. The variable parsing is configurable. In javascript, it is #{var}, just as in hamlet or lucius, but in coffescript it is %{var} to avoid conflicts with coffescript syntax. We plan on using this to add a simple html template for those who either need to maintain better compatibility with existing html or are not yet enlightened about the joys of using hamlet.

+

Currently the templates are geared towards being used in Yesod- they support type-safe url insertion. When we get around to it we will release a version without url insertion so that users outside of Yesod have no extra overhead. Hamlet-style templates are by far the best haskell has to offer, with one drawback: given that the templates are parsed at compile-time, it isn't always convenient to reload them when developing. In Yesod, we are solving this (and re-loading in general) by creating a smarter development environment.

+

Development environment reloading

+

A new version of wai-handler-devel is underway that will efficiently reload code in development mode. This will finally get us out of the "haskell straight-jacket" and allow an ease of development on par with dynamic languages. Coinciding with this change is a change in the yesod executable.

+

the yesod command

+
  • Running "yesod" by itself gives you a list of commands.
  • +
  • Running "yesod init" gives the behavior previously held by "yesod", i.e. generate a scaffolded site.
  • +
  • Running "yesod build" is almost identical to "cabal build", but with one change: it performs a dependency analysis of external files included by Template Haskell (Hamlet templates, routes, entity definitions) and changes modification times as needed to force cabal to build modules. For example, if "Handler/Root.hs" references "hamlet/root.hamlet", and the latter has a later modification time than the former, the former's modification times will be changed to match that of the latter.
  • +
  • Running "yesod devel" runs the devel server (automatically re-compiles code) which now uses cabal for the compiling (passing in a special "devel" flag). Previously we were using the hint package (uses the ghc api) and re-loading the entire application. The new version uses direct-plugins (uses .hi files) and will load individual files.
  • +
+

If this sounds like overkill to you, keep in mind that best practice in dynamic languages is to have a file watcher that will run the test suite as changes are made (based on user defined rules that need to be adjusted) and that development mode (re-interpreting files when a request comes in) can be noticeably slow. By using (the type-safe language) haskell we have avoided the need to constantly run a test suite. But we need some smarts to achieve fast re-compilation. This project watching tool is another that will hopefully be shared at least among other web frameworks, if not in haskell projects in general.

+

JSON support

+

We are switching our default JSON library to the excellent aeson package. This adds speed, features, and easier compatibility if this package becomes the community's default json package. We are giving up enumerator streaming, but that should be addable to aeson when the need arises.

+

Miscellaneous features

+
  • Partial file sending.
  • +
  • Per-route limits on request body. By default, this is 2 megabytes. See the maximumContentLength method.
  • +
  • The Yesod.Core module now exports everything in its package. This is useful when you want to write an app without the entire Yesod framework.
  • +
  • Logging support built in. This should be extensible enough to support any backend you throw at it (hslogger seems to be popular for this). There is still work for us to do at least to document how to log efficiently in a production envrionment. The key will probably be to log to stdout/stderr and re-use good existing logging tools.
  • +
  • No longer depends on wai-handler-devel. This means that executables are much smaller. (Note: later releases in the 0.7.* already included this.)
  • +
  • New mini scaffolded site, which does not include persistent or authentication code (many fewer dependencies).
  • +
  • Scaffolded sites now keep routes and model definitions in external files. This should allow for add-on tools to automatically generate code for you.
  • +
+

The future: 0.9 - 1.0

+
  • Documentation- we know that this is really the biggest area for improvement in Yesod. By the 0.9 release we will probably make some improvements to the documentation site to make it easier for others to contribute. And we will hold up the 1.0 release until we have documentation that we can be proud of.
  • +
  • static file caching headers support - a rough implementation is already completed. This allows for a pure haskell deployment- Apache or Nginx are optional.
  • +
  • Support for other template types as first class citizens. So you can treat coffeescript as if it were javascript or use a different html template in the same way you would use hamlet.
  • +
  • Easier support for faster, non-blocking Javascript loading via something akin to require.js.
  • +
  • A complete i18n solution.
  • +
  • Reassessing our forms package, probably to use digestive functors. Yesod forms currently use polymorphism- that is a great feature, but the limit of this abstraction, like many other compile-time features in haskell is often the ability to decipher error messages. That is easy enough to deal with when you own the code, but when someone else is interfacing with your libraries, error messages are a part of that interface.
  • +
  • Embedded objects in MongoDB. We are leaving other persistent features off the official roadmap, but expect other improvements.
  • +
  • Performance improvements. We don't have anything specific planned here, but we eagerly tackle any solid reports of slow spots in the framework. We will probably take a closer look at performance aspects post 1.0
  • +
+

0.9 is a chance to finish all the features that we feel are needed for a 1.0 release, and then to have another cycle to reflect and make sure things are done right, well-documented, and that we aren't missing anything that we view as critical for a complete web framework.

+

As always, if you send a patch for or start working on a useful feature, we will make helping you our top priority, and we are always listening to the needs of Yesod users- so there is always a lot of variance in the roadmap.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/04/beta-yesod-0-8.html b/public/blog/2011/04/beta-yesod-0-8.html new file mode 100644 index 00000000..9f9897a4 --- /dev/null +++ b/public/blog/2011/04/beta-yesod-0-8.html @@ -0,0 +1,76 @@ + Beta Release: Yesod 0.8 +

Beta Release: Yesod 0.8

April 8, 2011

GravatarBy Michael Snoyman

For the first time, the Yesod team has decided to make a beta release of Yesod before the official release to Hackage. Between the ease of deployment with Yackage, and the ease of testing with cabal-dev, we feel this will give users a chance to test out the new version without a significant investment of time, and will hopefully lead to some valuable input before the release. It will also give us a chance to focus on updating documentation before the official release.

+

In order to install the beta copy of Yesod 0.8, please add the following line to your ~/.cabal/config file (or wherever it's kept on your system):

+
remote-repo: yesod-yackage:http://yackage.yesodweb.com
+
+

You should then be able to just run "cabal update && cabal install yesod", and get the newest version. Feel free to try it out with cabal-dev as well.

+

I'm announcing a feature freeze as well, excepting two specific issues:

+
  • Changes to Persistent necessary for the MongoDB backend.
  • +
  • Modifications to Hamlet for $let binding. I'm not sure if this will be included yet, and I don't want it to hold up testing. But since it's a non-breaking change, I have no qualms about adding it later.
  • +
+

I'm going to post the changelog for the relevant packages here. When the official release is made, I will hopefully include a migration guide, similar to what we had for the 0.7 release. But hopefully these notes should be enough to get you started.

+

All packages

+
  • The biggest change in this release is a major switch from String to Text. This will probably account for 95% of migration tweaks necessary. We feel that Text is both a better choice performance wise, and a safer fit semantically.
  • +
+

Lucius

+
  • Nesting. For example, foo { bar { baz: bin; } } becomes foo bar { baz: bin; }.
  • +
  • luciusFile and luciusFileDebug added.
  • +
  • @media { ... } support.
  • +
+

Julius

+
  • Significant performance improvement on parsing large documents. This has no effect on runtime performance, since parsing is performed at compile time.
  • +
  • As Greg Weber pointed out, there's nothing Javascript-specific about Julius. We've created a new meta-language (in the Text.Romeo module for now) which allows people to create new template languages using the same interpolations as provided by Julius. Greg has been testing this out for a very interesting project. </suspense>
  • +
+

Hamlet

+
  • Conditional classes, e.g. <a href=@{MyRouteR} :isCurrentRoute MyRouteR:.current>.
  • +
+

Persistent

+
  • Join modules. For now, supports just one-to-many relations, but we will add more in the future as requested. The modules come in two flavors: application-level join and SQL join.
  • +
  • select renamed to selectEnum
  • +
  • Separated out all Template Haskell code into a separate package (persistent-template).
  • +
  • Under-the-hood changes to make way for MongoDB backend.
  • +
  • Switch to monad-control from monad-peel. Bas's performance numbers are just too good to ignore :).
  • +
+

yesod-core

+
  • Upgraded to WAI 0.4
  • +
  • Attributes allowed on script and link tags created by widget functions.
  • +
  • Partial file sending.
  • +
  • addJuliusBody (Javascript inside the body of the page).
  • +
  • Per-route limits on request body. By default, this is 2 megabytes. See the maximumContentLength method.
  • +
  • Logging built in. Should be extensible enough to support any backend you throw at it (hslogger seems to be popular for this).
  • +
  • The Yesod.Core module now exports everything in this package. This is useful when you want to write an app without the entire Yesod framework.
  • +
  • Add Cassius by media type (addCassiusMedia).
  • +
+

yesod-json

+
  • Switch to aeson
  • +
+

yesod

+
  • No longer depends on wai-handler-devel. This means that executables are much smaller. (Note: later releases in the 0.7.* already included this.) You should use the wai-handler-devel executable whenever you want to run in devel mode.
  • +
  • New mini scaffolded site, which does not include persistent or authentication code. Advantage: many fewer dependencies.
  • +
  • Scaffolded sites now keep routes and entity definitions in external files. This should allow for add-on tools to automate some boilerplate (i.e., augment your scaffolding).
  • +
+

For 0.9

+

I'm hoping to make the official release a little over a week from now. If all goes well, perhaps a week from Sunday (the 17th). My main objectives in that period are documentation and bug fixing.

+

Speaking of bug fixing: the Yesod test suite is beginning to take shape nicely. I'm planning on having a test added for each bug reported. Anyone who wants to help with the effort of increasing our test coverage is more than welcome. I think this is a very good place for new contributors to start off.

+

But as usual, the Yesod team likes to keep looking towards the next milestone, in this case, Yesod 0.9. That release will hopefully be our last chance to touch base before the 1.0 release. As such, I'd like to get all of the major changes implemented during this next iteration.

+

Looking over our roadmap, we've actually implemented a fair amount of it already. Combining what's left from that list, plus some new ideas that have come up, I think our goals for 0.9 should be:

+
  • Documentation/testing. These will always be priorities.
  • +
  • New: an overhaul of wai-handler-devel. The hint package is really wonderful, but I don't think it's up for the challenge anymore. I have more to say on the subject, but I'll leave it for an email to the web-devel list instead.
  • +
  • A complete i18n solution at the Hamlet/Widget level, tying in to the existing language infrastructure in Yesod.
  • +
  • Reassessing our forms package. This has been a major TODO list item for a while, and the time has come to finally bite the bullet on it.
  • +
  • Faster Javascript loading, via something akin to require.js.
  • +
  • Better static file caching support. This is actually in the wai-app-static package, and can be implemented without breaking changes. If someone out there wants to lend a hand on this, please let me know, I don't think it should be too daunting a task.
  • +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/04/new-site-design.html b/public/blog/2011/04/new-site-design.html new file mode 100644 index 00000000..85aac285 --- /dev/null +++ b/public/blog/2011/04/new-site-design.html @@ -0,0 +1,17 @@ + New site design, and some book progress +

New site design, and some book progress

April 10, 2011

GravatarBy Michael Snoyman

A huge thanks to Chris Done, who has put together a new design for the Yesod site. I'm sure there's a few kinks to be worked out, plus a possible bug or two since we're now running on the Yesod 0.8 beta release, so please let us know if you notice anything strange.

+

Also, I wanted to let everyone know that I'm really getting to work on the book this week. I've done some cleanup of a few chapters, plus added the beginning of the subsite chapter (I only mention that in particular because there was particular demand for it).

+

I'm hoping that the remainder of this week will stay focused on writing the book, so if there's any section in particular you wish was already written, now's the time to speak up.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/04/rails-can-scale.html b/public/blog/2011/04/rails-can-scale.html new file mode 100644 index 00000000..91c8b4b2 --- /dev/null +++ b/public/blog/2011/04/rails-can-scale.html @@ -0,0 +1,18 @@ + Rails can scale- auto-convert it to the framework formerly known as Yesod! +

Rails can scale- auto-convert it to the framework formerly known as Yesod!

March 32, 2011

GravatarBy Michael Snoyman

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/04/yesod-template-haskell.html b/public/blog/2011/04/yesod-template-haskell.html new file mode 100644 index 00000000..71453653 --- /dev/null +++ b/public/blog/2011/04/yesod-template-haskell.html @@ -0,0 +1,19 @@ + Yesod and Template Haskell +

Yesod and Template Haskell

April 14, 2011

GravatarBy Michael Snoyman

There's a common phrase that I hear with regard to Yesod: it uses too much Template Haskell/Quasi Quoting. I just want to give a quick explanation of why this is the case.

+

I think the number one distinguishing feature of Yesod is type safety. To my knowledge, no other web framework on the planet has the combination of type safe URLs, type-checked HTML/CSS/Javascript and a type-safe persistence layer. However, this requires a lot of work to get right.

+

For instance, take HTML. Another possible approach is something like blaze-html, where you define your entire HTML file as Haskell code. This is a nice approach, and I use it on a few projects. But it's not a template language. In order to get type safety from a template language, you need some form a code generation. So for that, we use Template Haskell.

+

Another is type-safe URLs. If you want this incredibly useful feature, you need to define your route datatype, your render function, and your dispatch function. And you need to make sure that you keep the render/dispatch functions coordinated. That's a tedious, error-prone task. You can do it, but Template Haskell saves you a lot of pain.

+

So it's true that Yesod uses Template Haskell. But every time we do it, we have a very good reason to do so, and it lets the programmer keep type safety without the error prone boiler-plate that generally accompanies it. You could try to bolt these features onto a different core that doesn't support them out of the box, but it will likely be a painful process.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/05/forms-api-decisions.html b/public/blog/2011/05/forms-api-decisions.html new file mode 100644 index 00000000..90b5a7fa --- /dev/null +++ b/public/blog/2011/05/forms-api-decisions.html @@ -0,0 +1,82 @@ + Forms API Decisions (and some status updates) +

Forms API Decisions (and some status updates)

May 19, 2011

GravatarBy Michael Snoyman

It's been a busy week for the Yesod team. The big API changes have been going into the Forms package, which I'll describe shortly. But first, we wanted to let everyone know about some of the other excitement.

+
  • Greg has implemented html5boilerplate integration for the scaffolded site.
  • +
  • Mark Wotton has set us up on his continuous integration server. Greg is continuing the process of switching our test suites over to the new "cabal test" system so they can be run automatically on the CI server.
  • +
  • Erik de Castro Lopo has implemented a slew of enhancements for http-enumerator, focusing on proxy support and HTTP basic authentication. We're still adding a few extra features, so the code is not yet on Hackage.
  • +
  • I've put in a lot of work on a new documentation system to replace the current website. I hope to blog more about this soon, but I'm not sure how much I'm allowed to say at the moment. I will say that one of the major features will be first-class user-contributed documentation, directly from the site.
  • +
  • We've got a new contributors page. We're starting to get the Yesod team a bit more organized and try to make it clear to the otuside world who is working on which pieces of the puzzle; expect such information to appear on that page in the future.
  • +
  • We're starting a testimonials page. There are quite a number of individuals and companies using Yesod in production now. If you're one of them, and you'd like to tell the world about it (and get a bit of free advertising in the process), .
  • +
+

html5boilerplate

+

Perhaps the biggest pain for web developers since the second web browser was created is cross-browser compatibility. The job of a framework is to deal with the common pain points like these. Using browser-independent javascript libraries is a huge win here, and the default Yesod widgets use jQuery and jQuery UI. But there are still html and css issues waiting to trip you up, including when trying to leverage newer html5 features and maintain browser compatibility. Html5boilerplate can help us out here. It is definitely a wide-ranging project- it tries to cover a lot of issues in a backend programming language agnostic way. For many aspects this is not optimal and we are already handling them appropriately within Yesod. But we can leverage their html layout and css file to provide proven techniques for dealing with cross-browser issues.

+

We want to keep things simple and avoid intimidating the first-time user. So the new scaffolding tool silently generates these files:

+
  • hamlet/boilerplate-layout.hamlet
  • +
  • static/css/html5boilerplate.css
  • +
+

The file: hamlet/default-layout.hamlet is still the default. But when you are serious about launching, you will want to switch to use html5boilerplate. Or if you have your own tried and true techniques, just delete these files.

+

Forms

+

Note: Just to make it clear, these "decisions" are still up for discussion. Greg has some ideas on how to clean up the API a bit, and I'd rather delay the release if it means a better final product.

+

I blogged recently about the Yesod forms library, leaving some stuff up in the air. Well, as part of the work on the i18n efforts for Yesod in general, and work on the new documentation site, I've hammered out a number of the details. There are still a few changes to be made, and I haven't decided if it will be released before the 0.9 release. But it's mostly complete. This post will give a high-level overview.

+

Three types of forms

+

There are three types of forms:

+
  • Monad (Form), which does not handle error propogation and view (i.e., the HTML itself). It's very convenient for making custom forms.

  • +
  • Applicative (AForm), which handles error propogation and view automatically, allowing for very concise code. If you need to create a standard form, such as a table layout form, this is what you'd use.

  • +
  • Input (FormInput). This is a special type for simply reading GET/POST parameters without actually constructing any HTML. In the current form package, there is a whole class of stringInput, intInput, etc functions. However, they all lived in the standard form datatype; having a separate type is new.

  • +
+

You can run monadic forms with runFormPost/runFormGet, and input forms with runInputPost/runInputGet. For Applicative forms, you must first convert them to monadic forms, using a render function. The package currently has renderTable and renderDivs.

+

Field

+

An individual field needs three pieces of information: how to parse from textual data, how to render to textual data, and how to create a view (widget). That's where the Field datatype comes into play. It looks like:

+
data Field xml msg a = ...
+
+

The xml type argument specifies the datatype for the view; this will usually be a Widget. And a specifies the datatype the field will return (e.g., intField returns an Int). But the msg is the interesting one: yesod-forms is now fully i18n'ed! Instead of just returning a message like "Invalid integer", intField returns an InvalidInteger value of type FormMessage.

+

yesod-core now includes a typeclass:

+
class RenderMessage master message where
+    renderMessage :: master -> Languages -> message -> Text
+
+

In order to use the forms library, you're going to need to provide an appropriate RenderMessage instance. If you just want to use the default rendering (which always displays English), it would look like:

+
instance RenderMessage MyApp Form where
+    renderMessage _ _ = defaultFormMessage
+
+

So this is a bit of overhead in order to use the library, but the advantages of being able to easily internationalize an application (IMO) are well worth it.

+

Fields to Forms

+

We have six functions for turning a Field into one of the Form types above. We have a required and optional variant, and a different function for each type of form. The naming is very straight-forward: mreq, mopt, areq, aopt, ireq, and iopt. The first four take three arguments: the Field itself, a FieldSettings (we'll get there) and the initial value for the field. The last two only take two arguments: the Field, and the parameter name.

+

So what's this FieldSettings? It's a data type containing four pieces of information: the label of the field, the tooltip, the ID and the name. Only the first is required. Oh, and here's the important bit: the label and tooltip can be any datatype, allowing for internationalized fields again! So for example:

+
data MyMessage = Name | Age
+
+myForm = runFormPost $ renderTable $ (,)
+    <$> areq textField (FieldSettings Name Nothing Nothing Nothing) (Just "Michael")
+    <*> aopt intField (FieldSettings Age Nothing Nothing Nothing) Nothing
+
+

If you're not using i18n, you can go ahead and use plain old Text. And in fact, we even have a convenient IsString instance, so if you turn on OverloadedStrings, you could write the above as:

+
myForm = runFormPost $ renderTable $ (,)
+    <$> areq textField "Name" (Just "Michael")
+    <*> aopt intField "Age" Nothing
+
+

FormResult

+

A form can return three possible results:

+
  1. There was no data present
  2. +
  3. There was invalid data present
  4. +
  5. There was good data present
  6. +
+

We use FormResult for this, completely unchanged from previous versions:

+
data FormResult a = FormMissing | FormFailure [Text] | FormSuccess a
+
+

FieldView

+

You usually won't need to interact directly with a FieldView. This datatype contains information on how a field needs to be rendered, such as the label, HTML for input, and any error messages. renderTables/renderDivs use it internally; if you decide to write a custom rendering function, you'll need it too.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/05/introducing-yesod-wiki.html b/public/blog/2011/05/introducing-yesod-wiki.html new file mode 100644 index 00000000..b9877006 --- /dev/null +++ b/public/blog/2011/05/introducing-yesod-wiki.html @@ -0,0 +1,92 @@ + Introducing the Yesod Wiki +

Introducing the Yesod Wiki

May 30, 2011

GravatarBy Michael Snoyman

Welcome to the new Yesod Site

I work for Suite + Solutions, a company that provides professional services for implementing and maintaining + XML-based content lifecycles. Services include style sheet development for a multitude of output + formats in multiple languages, content conversion, training, consulting, information architecture + guidance, custom integration and support. We are working on ways to bridge the gap between + user-contributed web systems like wikis and the more traditional documentation systems in order + to provide an interactive, structured knowledgebase and allow technical authors to get feedback + directly from their reader community.

+

My company has graciously allowed me to use this system as the basis for a new Yesod + documentation site. I believe this new site will make it much easier for me to add content and + keep it up-to-date, and lower the barrier to entry for other contributors. This post will give an + introduction to the new system.

+

Topic-based editing

At Suite Solutions, our business is documentation. The vast majority of our clients author in a + data format called DITA. There are a number of very cool features about DITA, but for our + purposes today the most important is topic-based authoring.

+

Instead of authoring a book, or a blog post, or a FAQ, you author individual topics. These + topics are intended to be concise and cover a single idea. Then, to produce a larger document, + you organize these topics together into a map.

+

Why we need it

+

I end up writing a large amount of content on Yesod:

+
  • The book
  • +
  • Blog posts
  • +
  • Answers to bug reports
  • +
  • Email responses on question
  • +
+

This results in a lot of duplicated content, which has two problems: wasted effort, and + out-of-date content. For example, I wrote a three part tutorial on enumerators not too long ago. + I then decided that it should be in the Yesod book. I copied the content over, updated it a bit, + and thought I was done. But there were a number of flaws with this:

+
  • Since the blog is written in Markdown, and the book in a special XML format, I had to spend a + significant amount of time converting.
  • +
  • The blog posts were still sitting on the web for some unwitting souls to find and get + confused by the outdated information.
  • +
+

With this new documentation system, we instead author topics and combine them into maps. Then, + these maps get published: as blog posts, in the book, or on their own. If the topics get updated + in one place, the changes propogate everywhere.

+

Community

We've been trying to encourage more community-contributed documentation to Yesod for a while + now. However, there were a few roadblocks to would-be contributors:

+
  • The Wiki is not + tightly integrated with the main site.
  • +
  • To get content into the book, you have to fork the yesoddocs repo on Github and send a pull + request.
  • +
+

By turning the main site into a Wiki itself, it should be much easier to contribute. Individual + topics on the site can be authored in HTML, Markdown, plain text or DITA. Unlike most wikis, a + person's topics are their own. However, a user can mark a topic as "world writeable" to allow others to edit it as well.

+

The last piece in the puzzle is how to find relevant content. The solution we're using is + labels: over time, I'll be creating a hierarchy of different subjects (templates, handlers, REST, + who knows). As you create topics, simply apply the appropriate labels. And when someone wants to + find some content, they can use the browse feature.

+

Bye Old Comments

The new site also brings with it changes to the comment system. You now need to be logged in to + comment. This may sound like a disadvantage, but at least in my experience it's irritating to + have to enter your name every time you make a comment.

+

But the bigger change under the hood is that comments are topics themselves. (Yes, that means + you could in theory comment on a comment.) This fits in very nicely with the philosophy we've + been developing here: you could write a comment on the book, and others will be able to find it + in a normal search. Or relevant comments could be strung together into a map and published as a + blog post.

+

One downside is that I'm not going to be migrating the existing comments from the book. Just to + make it clear, the comments on the book have been possibly the most important feedback in + refining its content, and I am very grateful to everyone who has contributed. Please keep it + up!

+

Missing Features

This is a first release, so you should expect bugs. Also, the UI is very unrefined, especially + on the settings page. But in addition, there are a number of features we have in mind:

+
  • A built-in issue tracker. There are a few advantages to keeping issues on this site instead + of Github:
    • No more confusiong about which project to assign the issue to.
    • +
    • Searching on the site for information will automatically find open issues.
    • +
    • If someone writes a detailed bug report and/or response, it will be easy to incorporate + that content in other sections of the site (e.g., FAQ, the book).
    • +
  • +
  • More social features, such as upvoting topics, forking topics, etc.
  • +
  • Discussion pages. In fact, the comment system is basically a discussion system already, it + just needs a little UI tweaking to make that apparent.
  • +
+

If you notice any issues, please let me know. I'm very excited about this new site. I believe it will + make it much easier to get quality documentation out.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/05/non-poly-hamlet-i18n.html b/public/blog/2011/05/non-poly-hamlet-i18n.html new file mode 100644 index 00000000..601d463a --- /dev/null +++ b/public/blog/2011/05/non-poly-hamlet-i18n.html @@ -0,0 +1,204 @@ + Non-Polymorphic Hamlet and I18N +

Non-Polymorphic Hamlet and I18N

May 5, 2011

GravatarBy Michael Snoyman

I've just released new versions of Hamlet and yesod-core which provide the foundation for a complete i18n solution in Yesod. This post is intended to give an understanding of the details of this implementation. Towards the end, I'll discuss the higher-level goals that are intended for day-to-day usage. On the way, we're going to make a stop off and discuss polymorphism, widgets and type-safe URLs.

+

Side note: I've also just release a new version of Yesod (0.8.1), which in addition to the i18n stuff described below also provides a (hopefully) working "yesod devel". If people are still running into trouble, please let me know.

+

I18N: Datatypes and functions

+

I've spent a lot of time thinking about what the ideal approach to I18N is. Without getting into comparisons with other solutions (I'm sure there will be plenty of time for that later), I think that simple ADTs are in fact a nearly perfect solution. Let's say we've got an app that needs to tell the user hello, the date and how many books the user has just purchased. Let's go through the issues:

+
  • For hello, we simply need a way to provide strings in different languages.
  • +
  • The date display needs to be localized based on language/locale. For en_US, we would likely want month/day/year, whereas for de_DE, we would want day/month/year.
  • +
  • The doozy is the books. We need to address: +
    • Word order. For example, in most languages, the number precedes the noun. In Hebrew, this is true unless you're dealing with the number one, in which case it follows the noun.
    • +
    • Pluralization. Don't even think about saying "you have purchased 3 book(s)." Oh, and by the way, Russian is really tricky.
    • +
    • Gender. English may only have one version of "seven", but many other languages do not.
    • +
  • +
+

I think we can model an incredibly simple solution to this in Haskell. Let's start off with our datatype:

+
data Message = Hello    
+             | TodayIs Day
+             | BooksPurchased Int
+
+

I think we can agree that this datatype will represent all the information necessary for a native speaker of a language to formulate the correct output. In fact, a native speaker could likely write a function to do this automatically. For example:

+
english :: Message -> String
+english Hello = "Hello"
+english (TodayIs day) = "Today is " ++ formatTime defaultTimeLocale "%m/%d/%Y" day
+english (BooksPurchased num) = concat
+    [ "You have purchased "
+    , show num
+    , pluralize num "book" "books"
+    ]
+
+pluralize 1 x _ = x
+pluralize _ _ y = y
+
+

So as to not embarrass myself and my rudimentary high school Spanish, I'll avoid writing similar functions for other languages, but you can see that it's possible to write a similar spanish, french, german and russian function. And you can go as crazy with grammar rules as you want, since you have the full expressivity of Haskell at your disposal.

+

So assuming we have all those functions, let's say we want to write a web app that will display the page in the correct manner based on the user's accept-language header. This header actually gives a prioritized list of languages. So we could write a function like so:

+
type Lang = String
+translate :: [Lang] -> Message -> String
+translate ("en":_) = english
+translate ("es":_) = spanish
+translate ("de":_) = german
+translate (_:rest) = translate rest
+translate []       = english -- The default backup
+
+

YesodMessage

+

And introduced in yesod-core 0.8.1 is the new YesodMessage typeclass. It is built on the ideas above, with a few modifications:

+
  • We want different applications to provide their own translations of messages, so the type class applies to the foundation datatype, not the message itself.
  • +
  • The message is provided via an associated type (i.e., type family).
  • +
  • We use both the subsite and master site foundation datatypes in this typeclass, so that you can have a different translation for a combination. This allows the new yesod-auth, for example, to let app writers provide their own translation strings.
  • +
  • We use Text instead of String for the language.
  • +
  • And we use Html for the output.
  • +
+

The definition of the typeclass is very simple:

+
class YesodMessage sub master where
+    type Message sub master
+    renderMessage :: sub
+                  -> master
+                  -> [Text] -- ^ languages
+                  -> Message sub master
+                  -> Html
+
+

There's also a helper function:

+
getMessageRender :: (Monad mo, YesodMessage s m) => GGHandler s m mo (Message s m -> Html)
+
+

This can be used as follows:

+
getRootR = do
+    render <- getMessageRender
+    defaultLayout $ do
+        setTitle $ render Welcome
+        addWidget [hamlet|
+<p>#{render IntroLine}
+<h6>#{render Copyright}
+|]
+
+

As you can see, this system works very nicely, but can be a bit tedious. We'll get back to the tediousness a little later.

+

Html and Hamlet

+

Let's take a break and talk about datatypes in Hamlet. The first datatype is Html. This is provided by the blaze-html package, and represents content that can be immediately rendered to a bytestring and sent to a client to view. Easy enough.

+

However, we also have the Hamlet datatype. This is defined as:

+
type Render url = url -> [(Text, Text)] -> Text
+type Hamlet url = Render url -> Html
+
+

This type signature is the basis of type-safe URLs. A "Render url" is a function that can convert a type-safe URL and a query string into a textual representation that can actually be sent to the client. For example, with a URL datatype of:

+
data MyRoute = Home | Blog
+
+

we could define a render function (ignoring query strings for now) as:

+
renderRoute Home = "http://www.mysite.com/"
+renderRoute Blog = "http://www.mysite.com/blog"
+
+

So now we can build up a Hamlet value using these datatypes:

+
myHamlet = [hamlet|<a href=@{Blog}>Visit my blog!|]
+
+

and using our render function get a plain old Html value:

+
myHtml = myHamlet renderRoute
+
+

Why bother?

+

But what's the point of this complication? We could just as easily write the above as:

+
myHtml = [hamlet|<a href=#{renderRoute Blog}>Visit my blog!|]
+
+

Well, there's a few advantages of the first approach:

+
  • It avoids the need to explicitly pass the renderRoute function around.
  • +
  • You can easily swap out different rendering functions. One real life example of this would be to generate relative URLs instead of absolute: the render function from /foo/bar will be different than the render function from /foo/bar/baz.
  • +
  • You can't accidently mess up datatypes.
  • +
+

But if you notice, there's a very strong similarity between this second approach and the initial i18n example of Hamlet shown above.

+

New Hamlet interpolation

+

So now I'd like to borrow an idea from type-safe URLs. Just like a URL gets a special interpolation in Hamlet to avoid tediousness and keep type safety, let's do the same thing for i18n. Since the underscore is used so prevalently for this in existing applications, we'll use it as well. So starting with Hamlet 0.8.1, we can write our i18n version of getRootR above like this:

+
getRootR = do
+    render <- getMessageRender
+    defaultLayout $ do
+        setTitle $ render Welcome
+        addWidget [hamlet|
+<p>_{IntroLine}
+<h6>_{Copyright}
+|]
+
+

We still need to get the explicit message renderer for the setTitle call, since it exists outside of Hamlet. But within Hamlet, we get a nice interpolation syntax. Using this, we could write our little bookstore application very easily:

+
postBuyBooksR = do
+    count <- processOrder
+    defaultLayout $ do
+        addHamlet [hamlet|
+<p>_{BookPurchased count}
+|]
+
+

But I've lied a little bit: the code above won't actually work. The reason is that the Hamlet datatype doesn't know anything about messages, it only knows about type-safe URLs. But the approach is basically identical: we need to provide a function that renders some datatype into some value that Hamlet knows how to embed. So Hamlet 0.8.1 introduces a new type synonym for Internationalized Hamlet:

+
type Translate msg = msg -> Html
+type IHamlet msg url = Translate msg -> Render url -> Html
+
+

Hopefully, the pattern is becoming clearer: whenever we have a special datatype, we need to provide a rendering function.

+

Polymorphism

+

Something that has confused a lot of people is polymorphism in Hamlet. It's a very nifty feature which allows a Hamlet template to have a few different types: Html, Hamlet or even a Widget. However, it comes with a lot of baggage:

+
  • Its implementation is relatively slow.
  • +
  • It can produce ridiculously complicated error messages.
  • +
  • Polymorphism only goes so far.
  • +
+

The last point refers to the fact that template interpolation (^{...}) is actually not polymorphic. Let me explain with some code. What is the type of:

+
[hamlet|#{someText}|]
+
+

? Well, whatever you want it to be: Html, Hamlet, or Widget. But what about:

+
[hamlet|^{someOtherTemplate}|]
+
+

? It's constrained by the type of someOtherTemplate. So as convenient as it is to just have one quasi-quoter (hamlet) and one template Haskell function (hamletFile) that works for multiple datatypes, it's just not worth it. Hamlet 0.8.1 includes a new module, Text.Hamlet.NonPoly. In this module, the hamlet quasiquoter will only ever produce a Hamlet value. If you want an Html value instead, use the html quasiquoter. And if you want an IHamlet, you can use the ihamlet quasi-quoter. So for example:

+
getRootR = do
+    mrender <- getMessageRender
+    urender <- getUrlRender
+    defaultLayout $ do
+        setTitle $ mrender Welcome
+        addHtml $ [ihamlet|
+<p>_{IntroLine}
+<h6>_{Copyright}
+|] mrender urender
+
+

But to keep up with the crowd, yesod-core has added its own variant of this family of functions: whamlet. So the code above can be written even better as:

+
getRootR = do
+    render <- getMessageRender
+    defaultLayout $ do
+        setTitle $ render Welcome
+        [whamlet|
+<p>_{IntroLine}
+<h6>_{Copyright}
+|]
+
+

Notice a few things:

+
  • We no longer use addHtml, since the result of whamlet is a Widget.
  • +
  • We don't need to pass the render function to the template; Yesod does that automatically.
  • +
+

To keep backwards compatibility, the polymorphic functions will remain standard until the 0.9 release, at which point they will be removed and replaced with the non-polymoprhic versions. (Most likely, we'll be removing the hamletFileDebug and runtime Hamlet code at the same time, unless I hear a good argument for keeping them.) And in addition to the quasi-quoters, there are external file versions of all these systems (htmlFile, hamletFile, ihamletFile and whamletFile).

+

And as a total aside: this assault on polymorphism is a bit of a theme in the 0.9 release. We're also planning a revamp of the yesod-form package to do away with its polymorphic abilities as well.

+

High level interface

+

I think the interface this provides for the template writer is already very nice: the vast majority of the time, a template author will just use the combination of whamlet(File) and _{Message} and be done with it. The thing I'm concerned about is how we're going to define YesodMessage. Clearly we don't want translators to need to learn Haskell. So we need some new file format that they'll be comfortable with.

+

Here's where I would really appreciate input. I'm currently thinking of having two types of files: one defining the messages, and one giving translations. We could use a similar syntax to Persistent for the former:

+
Hello
+TodayIs
+    day Day
+BooksPurchased
+    books Int
+
+

which would become:

+
data Message = Hello | TodayIs { day :: Day } | BooksPurchased { books :: Int }
+
+

We would then have files for each language that look like:

+
Hello: Hello
+TodayIs day: Today is #{monthYearDay day}
+BooksPurchased books: You have purchased #{show books} #{pluralize "book" "books"}
+
+

We'll stick all of these into a single folder. Then in our Haskell file, we'll write something like:

+
$(messages "folder" settings)
+
+

The settings could contains stuff like language aliases (x_Y is served x is there is no specific x_Y, for instance) and the default language. The system can then make the following guarantees:

+
  • The translator is using the correct variable name. If he/she typed in "BooksPurchased day", the system can give an error- and at compile time no less.
  • +
  • The translator is using the correct number of variables. "BooksPurchased day books" will also break.
  • +
  • The default language has translations available for all strings. That can be optional for other languages.
  • +
+

The other thing we'll need is a library of helper functions. You've seen two already: monthYearDay and pluralize. I won't speculate too much on which other functions such a library would require, I'd rather let it grow naturally.

+

I think the best way to approach this problem is to actually translate a real site. So after getting the initial infrustructure in place, my next goal will be to get it running in the context of Haskellers. If anyone knows some non-English languages and would be interested in helping out with this, please let me know. And in general, I'm interested to hear feedback on the ideas here, especially the critical type.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/05/warp-article-functional-web.html b/public/blog/2011/05/warp-article-functional-web.html new file mode 100644 index 00000000..3936277b --- /dev/null +++ b/public/blog/2011/05/warp-article-functional-web.html @@ -0,0 +1,15 @@ + Warp article in "The Functional Web" +

Warp article in "The Functional Web"

May 2, 2011

GravatarBy Michael Snoyman

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/05/yesod-for-non-haskellers.html b/public/blog/2011/05/yesod-for-non-haskellers.html new file mode 100644 index 00000000..d54ec6d3 --- /dev/null +++ b/public/blog/2011/05/yesod-for-non-haskellers.html @@ -0,0 +1,101 @@ + Yesod for non-Haskellers +

Yesod for non-Haskellers

May 31, 2011

GravatarBy Michael Snoyman

Introduction

My wife (Miriam Snoyman) has been wanting to make a recipe site for a while now. Instead of me + just writing it for her, she decided she would take this chance to learn a bit more about web + programming. She has done some programming in the past, and has knowledge of HTML/CSS (no + Javascript). She's also never done any Haskell.

+

So being the brave sport she is, she agreed to let me blog about this little adventure. I'm + hoping that this helps out some newcomers to Yesod and/or Haskell get their feet wet.

+

Given that our schedule is quite erratic and dependent on both kids agreeing to be asleep, I + don't know how often we will be doing these sessions, or how long they'll last each time.

+

Getting Started

+

The first step is using the yesod executable to generate a scaffolded site. The scaffolded site + is a "template" site and sets up the file/folder structure in the preferred way. A few other + optimizations like static file serving. It gives you a site that supports databases and logins. + Start with yesod init.

+

Some of these questions (like name) are for the cabal file. Other questions (database) actually + change the scaffolded site.

+

The "Foundation" is the central datatype of a Yesod application. It's used for a few different + things:

+
  • It can contain information that needs to be loaded at the start of your application and used + throughout. For example, database connection.
  • +
  • A central concept in Yesod is a URL datatype. This datatype is related to your foundation via + a type family, aka associated types.
  • +
+ +

The foundation is often times the same as the project name, starting with a capital letter.

+

Once you have your folder, you want to build it. Run cabal configure and + yesod build. We use "yesod build" instead of "cabal build" because it does + dependency checking for us on template files. You should now have an executable named + "dist/build/<your app>/<your app>". Go ahead and run it.

+

If you used PostgreSQL, you'll get an error about not having an account. We need to create it. + To see how it's trying to log in, look in config/Settings.hs. A few notes:

  • Top of file defines language extensions.
  • +
  • Haskell project is broken up into modules. The "module Settings (...) where" line defines + the "export list", what the module provides to the outside world.
  • +
  • Then the import statements pull code in to be used here. "qualified" means don't import all + the content of the module into the current namespace. "as H" gives a shortcut as you can type + "Text.Hamlet.renderHtml" as "H.renderHtml".
  • +
  • Note that there's a difference between things starting with uppercase and lowercase. The + former is data types and constructors. The latter is functions and variables.
  • +
  • #ifdef PRODUCTION allows us to use the same codebase for testing and production and just + change a few settings.
  • +

+

connStr contains our database information. We need to create a user and database in + PostgreSQL.

+
sudo -u postgres psql
+> CREATE USER recipes password 'recipes';
+> CREATE DATABASE recipes_debug OWNER recipes;
+> \q
+

Now try running your app again. It should automatically create the necessary tables. Open up + the app in your browser: http://localhost:3000/. You should see a "Hello" and "Added from + JavaScript".

+

Entities/Models

+

We store everything in the database via models. There is one datatype per table. There is one + block in the models file per entity. The models definition file is "config/models". By default, + we have "User" and "Email", which are used for authentication. This file is whitespace sensitive. + A new block is not indented at all.

+

An entity is a datatype, so it needs a capital letter. It makes more sense to use singular + nouns. Fields are all lowercase. First give a field name, and then a datatype. Some basic + datatypes are "Text" and "Int". If you want something to be optional, you put a "Maybe" after the + datatype.

+

We work off of SQL's relational approach to datatypes. Instead of having a "recipe" with a list + of "ingredients", we will have a recipe, and each ingredient will know which recipe it belongs + to. (The reason for this is outside our scope right now.) This kind of a relationship is called a + one-to-many: each recipe can have many ingredients, and each ingredient belongs to a single + recipe. In code:

+
Recipe
+    title Text
+    desc Text Maybe
+Ingredient
+    recipe RecipeId
+    name Text
+    quantity Double
+    unit Text
+

You can think of RecipeId as a pointer to an actual recipe, instead of actually holding the + recipe itself.

+

We can always come back and make modifications later. Persistent migrations will automatically + update the table definitions when possible. If it's not possible to automatically update, it will + give you an explanation why.

+ +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/05/yesod-form-revamp.html b/public/blog/2011/05/yesod-form-revamp.html new file mode 100644 index 00000000..7aa26ca8 --- /dev/null +++ b/public/blog/2011/05/yesod-form-revamp.html @@ -0,0 +1,98 @@ + Yesod Form Revamp +

Yesod Form Revamp

May 8, 2011

GravatarBy Michael Snoyman

One of the major goals for the Yesod 0.9 release is a revamp of the yesod-form package. The main change I want to push through is getting rid of the polymorphism introduced by the IsForm typeclass. This typeclass allows functions to work for both monadic and applicative forms (to be covered in a second). However, this introduces a lot of complexity and creates type errors which are impossible to discern.

+

Instead of spending time comparing and contrasting the current approach to what we want, I'm going to jump straight into a discussion of the goals of the package and what we can do to achieve them. I'm looking for feedback with this post, especially feedback on names: I think improving the naming scheme will go a long way towards making yesod-form more approachable.

+

Abstract away the view

+

Let's say I want to ask a user to enter his/her age. This field will be made up of a few components:

+
  • The label
  • +
  • The input element
  • +
  • Any error messages from failed validation
  • +
+

How should this be displayed? There are a few choices that immediately pop into mind: a row in a table, some divs, or perhaps some completely customized form like:

+
<p>Hey y'all, my name is <input name="name"> and I am <input name="age"> years old.</p>
+
+

But we don't want to have a separate intField function for every possible layout. So instead, we need some intermediate datatype that contains this information (along with some other stuff like field ID, if it's required, and a tooltip). Then every field function can create a view based around this data type, and we can provide a few helper functions to map this datatype to some of the standard displays (like a table row).

+

In the current library, this is called FieldInfo, which I think is a horrible name. Ideas?

+

Basic fields, required and optional

+

A great number of fields can be summed up by three pieces of information:

+
  • How to render our datatype to a Text.
  • +
  • How to parse a Text to our datatype (returning validation errors as necessary).
  • +
  • How to create an HTML input form.
  • +
+

In fact, this is such a common pattern that we have a data type for it (currently FieldProfile, open to renaming again). And to generate actual Form functions, we have two helpers: one for required fields, one for optional fields.

+

These helpers need a little bit more information though: the label, any forced ID or name attributes, and the tooltip. So we have another data type, FormFieldSettings. (At this point, I'm not going to mention any more that I'd like to rename things, just take it for granted.)

+

One little annoyance in the current design is that we need three functions for each datatype: the FieldProfile version, required form and optional form. We'll come back to this later.

+

Monadic or applicative?

+

Here's the real meat of the discussion. yesod-form originally followed the design principles of formlets. formlets heavily use Applicative. This allows very simple construction of forms from smaller pieces, which automatically track error responses and concatenate the HTML for you. In such a system, type signatures will look something like this:

+
intField :: Form xml Int
+
+

This works out very nicely. Let's say we have a data type that has two Ints (e.g., data MyType = MyType Int Int). Then we can immediately build up a new form:

+
myTypeField :: Form xml MyType
+myTypeField = MyType <$> intField <*> intField
+
+

And this seems like a great system. But eventually, I started getting feature requests for custom forms (you know, the kind of thing that does fit nicely in a table). The Applicative approach presents a bit of a problem, since it doesn't allow direct access to the view code. No problem, we'll just rewrite it as:

+
intField :: Form (Int, xml)
+
+

Except now we've got another problem: the error handling is still embedded in the Form type. So let's say that the user provides an invalid Int; now this function will not return. This gives us two problems:

+
  • The xml will not be generated for this field at all.
  • +
  • All subsequent form fields will not be generated.
  • +
+

So we really need something that looks more like:

+
intField :: Form (FormResult Int, xml)
+
+

Using this system, our code above becomes much more verbose:

+
myTypeField = do
+    (i1, x1) <- intField
+    (i2, x2) <- intField
+    return (MyType <$> i1 <*> i2, x1 `mappend` x2)
+
+

The originally IsForm-polymorphic approach of yesod-form tried to get around this verbosity by allowing the same intField to simultaneously work as an Applicative and Monadic form. But as I said, we're getting rid of that.

+

Possible solution

+

So here's the system I've been playing around with: we need to except the fact that our code will end up with a few extra keystrokes. We'll have a single function for each datatype, which will give the field profile itself. We'll then have four helper functions:

+
    +
  • areq (applicative required)
  • +
  • aopt (applicative optional)
  • +
  • mreq (monadic required)
  • +
  • mopt (monadic optional)
  • +
+

Some theoretical type signatures would be:

+
type AForm xml a -- applicative
+type Form a -- monadic
+
+areq :: FieldProfile xml a -> AForm [xml] a -- we'll explain the list below
+aopt :: FieldProfile xml a -> AForm [xml] (Maybe a)
+mreq :: FieldProfile xml a -> Form (FormResult a, xml)
+mopt :: FieldProfile xml a -> Form (FormResult (Maybe a), xml)
+
+

Using the current nomenclature, our built-in functions will all use FieldInfo as their XML type. The Applicative versions want to be able to automatically append XML values together, so we wrap up the FieldInfo in a list.

+

Finally, we'll want to be able to display our Applicative forms, so we'll have some built-in functions that will convert a list of [FieldInfo] into a Widget. This will simultaneously convert back to a monadic Form data type so that we only need one set of run functions. So we end up with something like:

+
formTable, formDivs :: AForm [FieldInfo] a -> Form (FormResult a, Widget)
+runFormGet, runFormPost :: Form x -> Handler (x, Enctype)
+
+

Obviously we're glossing over some details like inner monads, site arguments and FormFieldSettings, but I think the overall design should be clear. So now we get pretty close to our original, concise code:

+
myTypeForm = runFormPost $ formTable $ MyType
+    <$> areq intField
+    <*> areq intField
+
+

CSRF protection and missing input

+

One nifty feature in yesod-form is automatic Cross Site Request Forgery protection. This is a system where every user session has a nonce value associated with it, and no POST form submissions are allowed without that nonce. This prevents someone from creating a nefarious POST form on their site that points to your site.

+

Totally different issue: some fields like checkboxes need to know whether or not a form was submitted at all. If the form was submitted, and no value is present for this field, it means false. If the form wasn't submitted at all, it means "use the default value." This is a case not handled well by yesod-form right now, and we intend to add support for it. This is easy enough for POST forms: just check the request method. But for GET forms, it's more complicated. The solution: every GET form includes an extra hidden field that indicates that it has been submitted.

+

If you look closely, both of these use cases involve inserting a little bit of extra HTML into each form. So we can make some minor modifications to the type signatures above to accomodate this:

+
formTable, formDivs :: AForm [FieldInfo] a -> Html -> Form (FormResult a, Widget)
+runFormGet, runFormPost :: (Html -> Form x) -> Handler (x, Enctype)
+
+

Devil's in the details

+

As usual, there are probably a whole bunch of corner cases that have not been addressed here. But I'm hoping this post can spark some design discussions and we can get a strong, stable form package out in the next few weeks.

+

As far as planning: I'm planning on releasing the new yesod-form to work with Yesod 0.8 so it can be more easily tested. But it will only become the preferred version with the Yesod 0.9 release.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/06/building-better-chm.html b/public/blog/2011/06/building-better-chm.html new file mode 100644 index 00000000..3c67ed6e --- /dev/null +++ b/public/blog/2011/06/building-better-chm.html @@ -0,0 +1,125 @@ + Building a Better CHM +

Building a Better CHM

June 10, 2011

GravatarBy Michael Snoyman

The Problem

+

CHMs are a commonly used help format in the Windows world. They are based on HTML, meaning the + core content is portable and easy to write. But they've got a number of issues:

+
  • They're Windows-only. Some of us prefer Linux and Mac.
  • +
  • You can't easily take your CHM content and deploy it on the server.
  • +
  • There are a number of issues with CHMs, particularly with regards to i18n.
  • +
+

Nonetheless, CHMs provide some features we don't really have in any other system: single-file + deployment with search and an index (aka, tri-pane). We have other systems like Eclipse Help + which try to do the same thing, but they have requirements on the client side. Another + possibility is simply providing the raw HTML to users, but this would require sending multiple + files, and precludes the usage of Ajax and other features.

+

The Solution

+

The solution we came up with is providing a single executable that embeds a server (Warp) and + the static files, and launches a browser to view them. This is a natural extension of Yesod in + general, where Hamlet and family are compiled directly into our executables. This approach works + fairly well, but required some extra work.

+

wai-handler-launch

+

My original idea was to embed Webkit directly in the executable as well. This is really a very + cool solution: you never have to worry about Internet Explorer again. However, distributing + Webkit (or QtWebkit) was simply too heavy a dependency, and made compiling a nightmare. Paulo + Tanimoto and I also took a crack at using MSHTML on Windows, but our win32-fu wasn't up to the + task.

+

So instead, we have wai-handler-launch. This package lets you run any WAI + application using Warp, and will automatically launch the default web browser. On Windows, it + uses the ShellExecute API call, on Mac it calls out to the open + program and on Linux it uses xdg-open. (Note that this may cause trouble for + some desktop environments.)

+

The other interesting thing in this package is automatic shutdown. wai-handler-launch + automatically inserts a piece of Javascript into every HTML page that pings the server every + minute. If it doesn't get pinged for two minutes, it shuts down.

+

Template Haskell

+

The next trick is embedding static files into an executable. For this, we can use Template + Haskell (or more specifically, the file-embed package). A Template Haskell + splice always lives in the Q monad, which can embed arbitrary IO actions via qRunIO. And using + the StringL constructor, we can embed arbitrary content. Therefore, with just a little work, we + can embed a whole file at compile time:

+
embeddedFile :: String
+embeddedFile = $(fmap (LitE . StringL) $ readFile "myfile.txt")
+

One downside of this approach (well, there's a few, but the one I want to mention now) is that + it isn't very good for binary data. Sure, we can use Data.ByteString.Char8.pack, but we'll see + soon why that's not exactly what we want. Also, GHC has trouble with dealing with string literals + that get too big. So instead of StringL, we'll use StringPrimL, which instead of returning a + String provides an Addr#. Combined with Data.ByteString.Unsafe.unsafePackAddressLen, we're in + good shape.

+ +

Well, good shape except for the fact that it doesn't work for non-ASCII data. It turns out that + GHC encodes the contents of both StringL and StringPrimL, and automatically decodes the results + of StringL. We would need to manually decode this data ourselves. You can see this with a little + sample program:

+
{-# LANGUAGE TemplateHaskell #-}
+import Data.ByteString.Unsafe (unsafePackAddressLen)
+import Language.Haskell.TH.Syntax
+import qualified Data.ByteString as S
+
+main = do
+    fromAddr <- unsafePackAddressLen 7 $(return $ LitE $ StringPrimL "123\0\&456")
+    print fromAddr
+    let fromStr = S.pack $ map (toEnum . fromEnum) $(return $ LitE $ StringL "123\0\&456")
+    print fromStr
+

But considering the next stunt we're about to pull, that's not really an issue.

+

Modifying an Executable

+

Here's the last curveball in this project. We're going to need to generate these all-in-one + executables on a number of different systems, many of which won't have Haskell compilers set up. + Also, it would be very convenient to be able to produce executables for Windows, Linux and Mac on + a single system, instead of needing a compiler farm for each new web help we want to deploy.

+

Yitz Gale (my coworker at Suite Solutions) came up with an idea: let's embed some dummy content + with a recognizable pattern in the executable. Then, we'll have another program that comes along + and swaps out the dummy data with the real stuff. Downside: we need to pre-define the maximum + size of the webhelp. But we can mitigate this disadvantage by generating executables of various + sizes, and then using the smallest executable that will hold our data.

+

And this is why we need to go with StringPrimL. Besides the much quicker compile time, if we + used StringL then GHC would attempt to decode the data for us. Now, we can embed whatever + arbitrary binary content we want in our executable. GHC will give us an Addr# to the raw bytes, + and we can access it directly. Apply some binary pickling scheme, such as the + cereal package, and we're in business.

+

The final result, using some not-yet-released versions of the packages described in this post, + are two files: the template code and the injector:

+
-- Template
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Data.FileEmbed
+import Data.Serialize (decode)
+import Network.Wai.Handler.Launch (run)
+import Network.Wai.Application.Static
+
+main = do
+    let bs = $(dummySpace 1000000)
+    let files = either error id $ decode bs
+    run $ staticApp (defaultStaticSettings NoCache)
+        { ssFolder = embeddedLookup $ toEmbedded files
+        , ssDirListing = StaticDirListing (Just defaultListing) ["index.html", "index.htm"]
+        }
+
-- Injector
+import Data.FileEmbed
+import Data.Serialize
+import qualified Data.ByteString.Char8 as S
+import qualified Data.ByteString.Lazy as L
+import System.Environment (getArgs)
+import Codec.Compression.Zlib (compress)
+
+main = do
+    args <- getArgs
+    (webhelpTemp, srcDir, dstExe) <-
+        case args of
+            [a, b, c] -> return (a, b, c)
+            _ -> error "Usage: webhelp-inject <webhelp-template.exe> <source dir> <output exe>"
+    folder <- getDir srcDir
+    injectFile (encode folder) webhelpTemp dstExe
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/06/cookbook-part-2.html b/public/blog/2011/06/cookbook-part-2.html new file mode 100644 index 00000000..f5cea8c3 --- /dev/null +++ b/public/blog/2011/06/cookbook-part-2.html @@ -0,0 +1,215 @@ + Cookbook, part 2 +

Cookbook, part 2

June 14, 2011

GravatarBy Michael Snoyman

JSON Web Service

+

Let's create a very simple web service: it takes a JSON request and returns a JSON response. + We're going to write the server in WAI/Warp, and the client in http-enumerator. We'll be using + aeson for JSON parsing and rendering.

+

Server

+

WAI uses the enumerator package to handle streaming request bodies, and + efficiently generates responses using blaze-builder. aeson + uses attoparsec for parsing; by using attoparsec-enumerator + we get easy interoperability with WAI. And aeson can encode JSON directly into a Builder. This + plays out as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai (Response (ResponseBuilder), Application)
+import Network.HTTP.Types (status200, status400)
+import Network.Wai.Handler.Warp (run)
+import Data.Aeson.Parser (json)
+import Data.Attoparsec.Enumerator (iterParser)
+import Control.Monad.IO.Class (liftIO)
+import Data.Aeson (Value (Object, String))
+import Data.Aeson.Encode (fromValue)
+import Data.Enumerator (catchError, Iteratee)
+import Control.Exception (SomeException)
+import Data.ByteString (ByteString)
+import qualified Data.Map as Map
+import Data.Text (pack)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _ = flip catchError invalidJson $ do
+    value <- iterParser json
+    newValue <- liftIO $ modValue value
+    return $ ResponseBuilder
+        status200
+        [("Content-Type", "application/json")]
+        $ fromValue newValue
+
+invalidJson :: SomeException -> Iteratee ByteString IO Response
+invalidJson ex = return $ ResponseBuilder
+    status400
+    [("Content-Type", "application/json")]
+    $ fromValue $ Object $ Map.fromList
+        [ ("message", String $ pack $ show ex)
+        ]
+
+-- Application-specific logic would go here.
+modValue :: Value -> IO Value
+modValue = return
+
+

Client

+

http-enumerator was written as a comapnion to WAI. It too uses enumerator + and blaze-builder pervasively, meaning we once again get easy interop with aeson. A few extra + comments for those not familiar with http-enumerator:

+
  • A Manager is present to keep track of open connections, so that multiple + requests to the same server use the same connection. You usually want to use the + withManager function to create and clean up this Manager, since it is + exception safe.
  • +
  • We need to know the size of our request body, which can't be determined directly from a + Builder. Instead, we convert the Builder into a lazy ByteString and take the size from + there.
  • +
  • There are a number of different functions for initiating a request. We use http, which allows + us to directly access the data stream. There are other higher level functions (such as httpLbs) + that let you ignore the issues of enumerators and get the entire body directly.
  • +
+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Enumerator
+    ( http, parseUrl, withManager, RequestBody (RequestBodyLBS)
+    , requestBody
+    )
+import Data.Aeson (Value (Object, String))
+import qualified Data.Map as Map
+import Data.Aeson.Parser (json)
+import Data.Attoparsec.Enumerator (iterParser)
+import Control.Monad.IO.Class (liftIO)
+import Data.Enumerator (run_)
+import Data.Aeson.Encode (fromValue)
+import Blaze.ByteString.Builder (toLazyByteString)
+
+main :: IO ()
+main = withManager $ \manager -> do
+    value <- makeValue
+    -- We need to know the size of the request body, so we convert to a
+    -- ByteString
+    let valueBS = toLazyByteString $ fromValue value
+    req' <- parseUrl "http://localhost:3000/"
+    let req = req' { requestBody = RequestBodyLBS valueBS }
+    run_ $ flip (http req) manager $ \status headers -> do
+        -- Might want to ensure we have a 200 status code and Content-Type is
+        -- application/json. We skip that here.
+        resValue <- iterParser json
+        liftIO $ handleResponse resValue
+
+-- Application-specific function to make the request value
+makeValue :: IO Value
+makeValue = return $ Object $ Map.fromList
+    [ ("foo", String "bar")
+    ]
+
+-- Application-specific function to handle the response from the server
+handleResponse :: Value -> IO ()
+handleResponse = print
+
+

Persistent: Raw SQL

The Persistent package provides a type safe interface to data stores. It tries to be + backend-agnostic, such as not relying on relational features of SQL. My experience has + been you can easily perform 95% of what you need to do with the high-level interface. + (In fact, most of my web apps use the high level interface exclusively.)

+

But occasionally you'll want to use a feature that's specific to a backend. One feature I've + used in the past is full text search. In this case, we'll use the SQL "LIKE" operator, which is + not modeled in Persistent. We'll get all people with the last name "Snoyman" and print the + records out.

+ +
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE GADTs #-}
+import Database.Persist.Sqlite (withSqliteConn)
+import Database.Persist.TH (mkPersist, persist, share, mkMigrate, sqlSettings)
+import Database.Persist.GenericSql (runSqlConn, runMigration, SqlPersist)
+import Database.Persist.GenericSql.Raw (withStmt)
+import Database.Persist.GenericSql.Internal (RowPopper)
+import Data.Text (Text)
+import Database.Persist
+import Control.Monad.IO.Class (liftIO)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name Text
+|]
+
+main :: IO ()
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+    insert $ Person "Michael Snoyman"
+    insert $ Person "Miriam Snoyman"
+    insert $ Person "Eliezer Snoyman"
+    insert $ Person "Gavriella Snoyman"
+    insert $ Person "Greg Weber"
+    insert $ Person "Rick Richardson"
+
+    -- 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'"
+    withStmt sql [] withPopper
+
+-- A popper returns one row at a time. We loop over it until it returns Nothing.
+withPopper :: RowPopper (SqlPersist IO) -> SqlPersist IO ()
+withPopper popper =
+    loop
+  where
+    loop = do
+        mrow <- popper
+        case mrow of
+            Nothing -> return ()
+            Just row -> liftIO (print row) >> loop
+
+

Internationalized Julius

Hamlet has built-in support for i18n via the underscope interpolation syntax:

+
<h1>_{MsgHelloWorld}
+

There was a concious decision not to include this syntax for Cassius, Lucius and Julius, since + it is relatively uncommon to need this interpolation, and the added complexity of using the + library didn't seem to warrant it. However, there are times when you do want to add an + internationalized message to your Javascript.

+

The trick is fairly simple: getMessageRender returns a function that will + convert a type-safe message into an actual string. We can directly use those strings with normal + variable interpolation. getMessageRender handles all the work of determining the user's language + preference list.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Yesod
+
+data JI = JI
+type Handler = GHandler JI JI
+mkYesod "JI" [parseRoutes|
+/ RootR GET
+|]
+
+instance Yesod JI where
+    approot _ = ""
+
+data JIMsg = MsgHello
+
+instance RenderMessage JI JIMsg where
+    renderMessage a [] x = renderMessage a ["en"] x
+    renderMessage _ ("en":_) MsgHello = "Hello"
+    renderMessage _ ("es":_) MsgHello = "Hola"
+    renderMessage a (_:ls) x = renderMessage a ls x
+
+getRootR :: Handler RepHtml
+getRootR = do
+    render <- getMessageRender
+    defaultLayout $ addJulius [julius|alert("#{render MsgHello}")|]
+
+main :: IO ()
+main = warpDebug 3000 JI
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/06/cookbook.html b/public/blog/2011/06/cookbook.html new file mode 100644 index 00000000..c23814aa --- /dev/null +++ b/public/blog/2011/06/cookbook.html @@ -0,0 +1,221 @@ + Cookbook +

Cookbook

June 7, 2011

GravatarBy Michael Snoyman

Introduction

This is hopefully the first blog post in many with "cookbook" style answers. If you have either questions or "recipes" to submit, please . Eventually, this content will make its way into the Yesod book.

Adding a local Javascript file

There are two main functions used to include a reference to an external Javascript file: + addScript and addScriptRemote. The latter is used when you + want to provide the actual URL to the script, and can be especially useful for referencing + CDN-hosted libraries, such as Google-hosted jQuery.

+

The former function is used when you want to refer to a type-safe URL. The most common case of + this is with the yesod-static package; if you want to use that, the best place + to see an example usage is in the scaffolded site. Here is a minimal example using the + lower-level sendFile.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TemplateHaskell #-}
+import Yesod
+
+data Local = Local
+type Handler = GHandler Local Local
+
+mkYesod "Local" [parseRoutes|
+/ RootR GET
+/myfile.js MyFileJsR GET
+|]
+
+instance Yesod Local where
+    approot _ = ""
+
+getRootR = defaultLayout $ do
+    addScript MyFileJsR
+    addHamlet [hamlet|
+<h1>HELLO WORLD!
+|]
+
+ -- type sig necessary, since sendFile is polymorphic
+getMyFileJsR :: Handler ()
+-- Serves "myfile.js" with text/javascript mime-type.
+-- Served from /myfile.js as defined above, but your code needn't know that.
+getMyFileJsR = sendFile "text/javascript" "myfile.js"
+
+main = warpDebug 3000 Local

Virtual Hosts

Often times, we'll want to run multiple sites from a single machine, using virtual + hosts. The recommended approach in general is to use a web server like Nginx to be the + frontend server, and to reverse HTTP proxy to your individual sites. However, it's entirely + possible to do this in 100% pure Haskell, with just a single instance running for all your + sites.

+

To achieve this, we'll use the vhost middleware. We'll start off with some boilerplate + definitions of three Yesod sites:

+
{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+
+import Yesod
+import Network.Wai.Middleware.Vhost
+import Network.Wai.Handler.Warp
+import Network.Wai
+
+data Site1 = Site1
+data Site2 = Site2
+data DefaultSite = DefaultSite
+
+mkYesod "Site1" [parseRoutes|/ Root1 GET|]
+getRoot1 = return $ RepPlain "Root1"
+
+mkYesod "Site2" [parseRoutes|/ Root2 GET|]
+getRoot2 = return $ RepPlain "Root2"
+
+mkYesod "DefaultSite" [parseRoutes|/ RootDef GET|]
+getRootDef = return $ RepPlain "RootDef"
+
+instance Yesod Site1 where approot _ = ""
+instance Yesod Site2 where approot _ = ""
+instance Yesod DefaultSite where approot _ = ""
+

Now comes the actual virtual hosting. We want to serve Site1 from "host1", Site2 from "host2", + and otherwise serve DefaultSite. In order to determine which site to use, we'll compare against + the serverName record of the WAI request value. The code is very straight-forward:

+
main = do
+   app1 <- toWaiApp Site1
+   app2 <- toWaiApp Site2
+   appDef <- toWaiApp DefaultSite
+   run 3000 $ vhost
+       [ ((==) "host1" . serverName, app1)
+       , ((==) "host2" . serverName, app2)
+       ] appDef
+

The vhost function takes two arguments: a list of pairs of predicates and applications, and a + final default application. This approach works very well for just a few sites, but if you have + dozens of virtual hosts, it would be more efficient to use a Map. Add the following to your + import list:

+
import qualified Data.Map as Map
+

And then replace your main with:

+
main = do
+   app1 <- toWaiApp Site1
+   app2 <- toWaiApp Site2
+   appDef <- toWaiApp DefaultSite
+   let sites = Map.fromList
+                   [ ("host1", app1)
+                   , ("host2", app2)
+                   ]
+   run 3000 $ \req ->
+       case Map.lookup (serverName req) sites of
+           Nothing -> appDef req
+           Just app -> app req

Yesod Proxy Server

Yesod is built on top of WAI, which in turn is built on enumerators. One of the advantages of + enumerators is being able to work with streams of data, in constant space. By building on a + central enumerator system, multiple libraries are able to interoperate very easily. In our case, + both WAI and http-enumerator share an enumerator approach. Combined with + Aristid Breitkreuz's http-types, it is much easier to get these packages to + interoperate.

+

Unfortunately, there are still a few gotchas. If you're not familiar with enumerators, it can + be difficult to get the types to work out. Yesod purposely doesn't advertise the low-level + functions necessary to get things to work, since they are confusing to new users. And there's a + bit of a catch with Builders versus ByteStrings. We'll step through a very simple Yesod app that + proxies the Yesod homepage. We'll start with our standard language extensions:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+

Next we have our import statements. Try to follow which modules/libraries and providing + different functionalities.

+
import Yesod hiding (Request)
+import Network.HTTP.Enumerator (parseUrl, withManager, http, Request)
+import Network.HTTP.Types (Status, ResponseHeaders)
+import Network.Wai (Response (ResponseEnumerator))
+import Data.ByteString (ByteString)
+import Blaze.ByteString.Builder (Builder, fromByteString)
+import Data.Enumerator (Iteratee, run_, ($$), joinI)
+import qualified Data.Enumerator.List as EL
+

And now some simple code to create a new Yesod application with a single route and is served by + Warp:

+
data Proxy = Proxy
+type Handler = GHandler Proxy Proxy
+
+mkYesod "Proxy" [parseRoutes|
+/ RootR GET
+|]
+
+instance Yesod Proxy where
+   approot _ = ""
+
+main :: IO ()
+main = warpDebug 3000 Proxy
+

All that's left is to define our handler function. If we look at the + http-enumerator package, we'll see that there are a number of functions + available for running an HTTP request. However, all of them (except simpleHttp) take a Request. + So we'll start by using parseUrl to generate such a value. We'll take advantage of the IO + instance of Failure by using liftIO:

+
getRootR :: Handler ()
+getRootR = do
+   req <- liftIO $ parseUrl "http://www.yesodweb.com/"
+

For this exercise, we're going to use the http function, which has the + signature:

+
http :: Request IO 
+     -> (Status -> ResponseHeaders -> Iteratee ByteString IO a)
+     -> Manager
+     -> Iteratee ByteString IO a
+ +

Alright, the Manager is simply a collection of open connections. We can generate one using + withManager. And we already have our request. Now we need to do something about that second + argument. But it looks strangely familiar... let's look at the definition of ResponseEnumerator + from WAI:

+
type ResponseEnumerator a = (Status -> ResponseHeaders -> Iteratee Builder IO a) -> IO a
+

Interesting... it's almost identical, except for the ByteString versus Builder issue. Let's + take a quick break from the definition of getRootR and define a new function, blaze, to help us + out here:

+
blaze :: (Status -> ResponseHeaders -> Iteratee Builder IO a)
+      -> Status -> ResponseHeaders -> Iteratee ByteString IO a
+blaze f status headers =
+

First, a little side note: http-enumerator returns all of the content headers from the server, + including content-encoding. This can cause a bit of a problem for clients (you know, they don't + like being told their data is encoded when it isn't), so we need to strip that header out:

+
let notEncoding ("Content-Encoding", _) = False
+        notEncoding _ = True
+        headers' = filter notEncoding headers
+

Next, we want to get apply the status and headers to f to get our Builder Iteratee:

+
--builderIter :: Iteratee Builder IO a
+        builderIter = f status headers'
+

Finally, we need to turn our Builder Iteratee into a ByteString Iteratee. Let's remember that + an Iteratee is a data consumer, being fed a stream of data. In order to convert an Iteratee to + receive a new stream type, we need to stick an adapter at the beginning of it, called an + Enumeratee. In our case, we need to convert a stream of ByteStrings to a stream of Builders. For + this, we can use Data.Enumerator.List.map and fromByteString, together with joinI and $$ for some + glue:

+
in joinI $ EL.map fromByteString $$ builderIter
+

Well, now that our helper function is done, we can return to http. We need to use + sendWaiResponse, which will let us send a raw WAI response from our Yesod app. And we'll want to + use the ResponseEnumerator constructor for Response. So our getRootR ends up looking like:

+
getRootR :: Handler ()
+getRootR = do
+   req <- liftIO $ parseUrl "http://www.yesodweb.com/"
+   sendWaiResponse $ ResponseEnumerator $ inside req
+

And we need inside to have a type signature of:

+
inside :: Request IO
+       -> (Status -> ResponseHeaders -> Iteratee Builder IO a)
+       -> IO a
+

As we mentioned before, we'll use withManager to get the manager, so our function will start + off as:

+
inside req f = withManager $ \manager ->
+

And the rest is just playing with the types. We know we want to call http here, and the first + argument is going to be req. f is almost the right type for the second argument to http, but it + needs a little help from the blaze function. And the final argument will be manager. This gives + us:

+
http req (blaze f) manager :: Iteratee Bytestring IO a
+

But we need our function to return IO a. So the last step is to actually run_ the Iteratee to + produce a value. Our complete definition of inside is:

+
inside req f = withManager $ \manager ->
+   run_ (http req (blaze f) manager)
+

Or in point-free style, for those inclined:

+
inside req f = withManager $ run_ . http req (blaze f)

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/06/yesod-docs-haskellwiki.html b/public/blog/2011/06/yesod-docs-haskellwiki.html new file mode 100644 index 00000000..b416164a --- /dev/null +++ b/public/blog/2011/06/yesod-docs-haskellwiki.html @@ -0,0 +1,25 @@ + Yesod and Haskell Docs +

Yesod and Haskell Docs

June 23, 2011

GravatarBy Greg Weber

Yesod Documentation Efforts

+

We love the new Yesod documentation system! We can use Markdown or a WYSIWYG editor, depending on whatever is convenient. We can easily take content from a blog post or a mail list response and put it in the book or the wiki. Speaking of which, the wiki doesn't suck anymore! I have been using it and working out some kinks with Michael, and it is ready for contributors now. And we still support comments- thanks to those that told us about problems with the Yesod examples in the book, which are now fixed.

+

Our biggest documentation hassle now is that our haddocks frequently don't build (due to dependency haddocks not building). We hope the haskell community can fix this on the current hackage server. Otherwise we are forced into the hassle and user confusion of hosting our own haddocks.

+

Yesod Wiki is open sourced!

+

Michael's employer, Suite Solutions, not only sponsored the initial work on what we are using for the Yesod Wiki, but has now decided to open source it!

+

Lets keep talking about wikis - haskellwiki.org

+

Haskellwiki is slow! I found it a pain to work with because edits would frequently take long periods or timeout. Simple page requests often take a long time. My guess as to the root of the problem is that either the database is overwhelmed, or that PHP's blocking IO means that requests are being delayed. If the latter, this is a problem that Haskell has solved in the runtime! In most languages, like PHP, unless one uses a specific asynchronous driver, an IO request (to the database) will block the current process. The only thing that can be done is to spin up another process, which ends up bloating memory. In haskell, the current thread will block until the database call returns, but another thread can immediately take on the next request. And we don't even need any special libraries in Haskell! Not only that, but we can deploy a single binary that can spread across multi-core, rather than managing separate processes.

+

Of course the easiest solution to haskellwiki probably lies in fixing the php deployment- maybe we can get someone with MediaWiki experience to speed the wiki. But shouldn't the wiki for Haskell be written in Haskell? Starting a new haskellwiki written in Haskell wouldn't be a problem. The big pain is converting all the existing MediaWiki content. Pandoc already has a MediaWiki reader, but it is probably incomplete and there are still going to be files to transfer. And maybe user accounts also. It would be great to see someone step up and take on this task. And John MacFarlane would love to see a new maintainer of gitit.

+

Keep up the Docs

+

Lets not let our documentation infrastructure decay and make documentation more of a hassle. The problem with maintaining community infrastructure is probably that it is a thankless job. Can we have a donation drive that encourages every Haskell user to donate $20 to improve haskell infrastructure? That way we could at least thank some of the maintainers with money.

+

Thanks to everyone who documents their libraries, writes tutorials, and answers questions on mail lists- your contributions make the Haskell community work!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/07/browserid-support.html b/public/blog/2011/07/browserid-support.html new file mode 100644 index 00000000..2ff867f9 --- /dev/null +++ b/public/blog/2011/07/browserid-support.html @@ -0,0 +1,25 @@ + BrowserID support +

BrowserID support

July 17, 2011

GravatarBy Michael Snoyman

I just implemented BrowserID support for Haskellers, and wanted to share some quick thoughts. Firstly, the process is a piece of cake: it amounted to about one line of HTML, 4 lines of Javascript, and 10 lines of Haskell. (The commit to the repo is significantly larger, since there is some logic for backwards-compatibility with verified emails, but that's a separate issue.)

+

I think this system is much more user friendly than OpenID. You get an identifier that you can actually remember (as opposed to Google's OpenID implementation). There's less of a question of which identifier you used. With OpenID, there's always the question of whether you logged in with Google, Wordpress, or Yahoo. Now the only question is which email address you used, which I would imagine is a simpler one to track. Also, BrowserID's handling of multiple email addresses is very nice for people who share computers with spouses/roommates.

+

User testing

+

I happened to have two willing test subjects (my sisters) visiting me, and I asked them each to log in with BrowserID. Here are the issues they ran up against:

+
  • They were very concerned giving their email address to a site they didn't recognize.
  • +
  • The initial login window was unclear, asking for email and password. This implied that she was supposed to provide the password for her email account, which made her even more concerned.
  • +
  • Since my sister uses a webmail based account, she verified by going back to the original browser window and opening a new tab to her email account. Once she verified, she went back to the Haskellers tab. However, since the BrowserID window was already open, the "Login" button was no longer responsive.
  • +
+

It seems like these issues could be overcome with integration within the web browser and user education.

+

Yesod integration

+

So far, all of the BrowserID code is in Haskellers itself. After some testing, I'm planning on adding support to both the authenticate and yesod-auth packages. After a little more testing, I will likely include BrowserID in the default scaffolded site. If things go as well as they seem to be, I'd likely use BrowserID as the primary (or perhaps only) login method for future sites.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/07/haskell-on-heroku.html b/public/blog/2011/07/haskell-on-heroku.html new file mode 100644 index 00000000..d5b8d625 --- /dev/null +++ b/public/blog/2011/07/haskell-on-heroku.html @@ -0,0 +1,37 @@ + Haskell on Heroku +

Haskell on Heroku

July 20, 2011

GravatarBy Greg Weber

Deploying Yesod keeps getting better. I just deployed an application to Heroku. Heroku isn't right for every usage case, but it is an easy and free (for one small app) deployment option that we should take advantage of.

+

Heroku recently created a new Cedar application stack that is very flexible- it was just a matter of time before Haskell was deployed there. Big thanks goes to Mark Wotton and everyone else at the recent Australian Haskell hackathon for demonstrating you can deploy Haskell to Heroku- they got an app running, although they didn't have a real database working.

+

I deployed a simple landing page Yesod application to Heroku. It saved e-mails to a database- demonstrating a web-app using the Heroku shared Postgresql database. And I know it works because I did a heroku db:pull to copy the database to my local machine. This is why people use Heroku- they provide a lot of nice built in features and take the hassle out of deploying. The other nice thing they do is aggregate logs. heroku logs will show your recent log output, and log aggregation and rotation is already setup for you.

+

As part of this effort I started a heroku package containing code for deploying to Heroku. All the package does now is parse the DATABASE_URL environment variable to help with connecting to the database.

+

Configuring Yesod

+

I also worked on Yesod to make it much more configurable. Previously we just had a production build flag, and all application settings were determined at compile time. Static guarantees are nice, but this was a very inflexible setup that isn't usable for Heroku. You now have the following tools at your disposal for configuring your application:

+
  • YAML configuration files
  • +
  • Command line arguments
  • +
  • Check what environment the application is in at runtime
  • +
  • Continue to check what environment the application is at build time with CPP flags.
  • +
+

These aren't things we are comfortable direcly coupling to Yesod, so this is all generated in the new scaffolding that will be in the 0.9 release. Or you can grab the latest Yesod from github. The downside is that the upgrade path is unclear. You might have different ideas as to what you want- so for now you have to generate a new scaffold site and look at the config/Settings.hs and the main file and decide what you want. We plan on provide a migration guide for the latest version of Yesod that will provide some more specific instructions.

+

In the new scaffold, command line arguments override existing settings. There is still a config/Settings.hs but also a settings.yaml file with a few site settings. Database connectiion parameters are in YAML files named after their database type (config/postgres.yml).

+

The scaffolder also generates a Heroku Procfile, which documents what you need to do to deploy in its comments. It actually only contains one line of Heroku configuration:

+
web: ./dist/build/~project~/~project~ -p $PORT
+
+

Yesod is now setup by default to accept the port command line argument. There are a few other simple steps to go through for your application. There is one big issue that might prevent you from deploying: you must compile your app on 64bit and statically link it. I did this on my Ubuntu computer without issue for my application by just adding the -static ghc option. However, I am told that static linking is not always this simple and fool proof.

+

What we have now is a pure Warp deployment. This means no Apache/Nginx for static file serving. But this doesn't concern us- Warp can serve static assets quickly, and in Yesod 0.9 static files will automatically be given proper caching headers.

+

If you already work on 64 bit Linux and are using postgresql, Heroku is likely the easiest way to deploy your next application.

+

The next Yesod release (0.9) will be the easiest web application to deploy available anywhere- a single binary will correclty serve your dynamic and static requests, use every core, and handle IO asynchronously. We just need a few recipes for deploying to different environments. Let us know what your deployment techniques are.

+

Update: Heroku is not just for Ruby

+

Someone on Reddit asked for more particulars. Heroku's new cedar stack is designed to be flexible enough to run Ruby, Node.js, and Clojure. This new flexibility means more supported languages. However, it also means you can run any executable on Heroku. The issue is that you can't install to Heroku. In interpreted languages you don't have to do anything special to install packages- you just need the interpreter to be installed. In a compiled language you must staticly link your binary.

+

Perhaps the biggest downside of static linking is the long time it takes to transfer a new binary. Anyone have solutions for quicker binary updates?

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/07/syntax-highlighting.html b/public/blog/2011/07/syntax-highlighting.html new file mode 100644 index 00000000..4c8ea55a --- /dev/null +++ b/public/blog/2011/07/syntax-highlighting.html @@ -0,0 +1,15 @@ + Line numbering/syntax highlighting added +

Line numbering/syntax highlighting added

July 8, 2011

GravatarBy Michael Snoyman

For everyone who's been asking for it, I've just adding syntax highlighting and line numbering to most of the Haskell code snippets. I'm using John McFarlane's very nice illuminate package for this. If you notice any problems, please let me know.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/07/yesod-community.html b/public/blog/2011/07/yesod-community.html new file mode 100644 index 00000000..95a2b316 --- /dev/null +++ b/public/blog/2011/07/yesod-community.html @@ -0,0 +1,61 @@ + Yesod Community +

Yesod Community

July 16, 2011

GravatarBy Greg Weber

The principle sources of information for Yesod have been the book, the source code, and the web-devel mailing list. The community of Yesod users is growing, and we are constantly trying to adapt different approaches. Mail lists keep knowledge concentrated in one area but many users don't feel comfortable using them to ask questions of give answers. We want a system where users are comfortable helping each other out, which is a great way to learn. So recently we have been encouraging the use of:

+
  • Stack Overflow for basic help questions
  • +
  • haskell web-devel mail list for discussions about Yesod or help questions that could likely turn into discussions. Please use subject tags in your posts indicating the topic.
  • +
  • IRC (Freenode#yesod) for synchronous communication. Don't expect that you can always get a questioned properly answered here, but it is a great place for discussions brainstorming, and hanging out.
  • +
+

Let us know how you like the system, and how we can improve the flow of information in the community.

+

One thing that I think would be nice would be to aggregate Yesod blog posts somehow. Blogging is great, but the downside is it can store information off in different corners of the internet that you have to connect with Google searches.

+

Wiki- now with a tutorial

+

davidbe has started a Yesod tutorial on the wiki. The goal is to create a beginner level tutorial with a different style than the book- taking a step-by-step approach.

+

The wiki is a great place to document your knowledge that is not in the Yesod book.

+

Contributors

+

Contributions are what make open source work. There are many different ways to contribute, the most obvious of which is to commit code. Thanks to everyone who has committed to Yesod!

+
  • Michael Snoyman
  • +
  • Greg Weber
  • +
  • Matt Brown
  • +
  • Kazu Yamamoto
  • +
  • patrick brisbin
  • +
  • Alexey Khudyakov
  • +
  • Hiromi Ishii
  • +
  • Erik de Castro Lopo
  • +
  • Mark Bradley
  • +
  • Vincent Hanquez
  • +
  • Stefan Kersten
  • +
  • Paulo Tanimoto
  • +
  • Michael Steele
  • +
  • Stefan Kersten
  • +
  • Simon Hengel
  • +
  • Ian Duncan
  • +
  • DavidM
  • +
  • Patrick Palka
  • +
  • Bas van Dijk
  • +
  • Steven Robertson
  • +
  • Rick Richardson
  • +
  • Rehno Lindeque
  • +
  • Aristid Breitkreuz
  • +
  • alios
  • +
  • Aaron Culich
  • +
  • Yair Chuchem
  • +
  • Paolo Losi
  • +
  • Michael Xavier
  • +
  • Leon P Smith
  • +
  • dbpatterson
  • +
  • Bryan O'Sullivan
  • +
  • Björn Buckwalter
  • +
  • Leif Warner
  • +
  • Hamish Mackenzie
  • +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/07/yesod-reorg.html b/public/blog/2011/07/yesod-reorg.html new file mode 100644 index 00000000..8d8ae280 --- /dev/null +++ b/public/blog/2011/07/yesod-reorg.html @@ -0,0 +1,78 @@ + The Great Yesod Reorganization of 2011 +

The Great Yesod Reorganization of 2011

July 24, 2011

GravatarBy Michael Snoyman

tl;dr

Check out the new yesodweb organization on Github and the yesodweb mailing + list (via Google Groups).

+

History

+

In the beginning, Yesod was a single repository written by a single developer (me). It included + all the bells and whistles internally, from authentication to forms and static file serving. And + all was well and simple.

+

Except this didn't scale very well. Making breaking changes in a single component meant bumping + version numbers everywhere. Someone wanting to implement a small site with few features still + needed to pull in on the dependencies of the greater Yesod. And as features continued to be + added, this overhead continued to grow.

+

The solution was to split Yesod up into many smaller packages: yesod-core, yesod-auth, + yesod-static, etc. And this was a very good solution (sans some Cabal annoyances).

+

Problems

+

But of course, no good solution is without its issues. In this case, the sheer number of + repositories involved made it prohibitively difficult for people to test out beta code. Branching + for a beta version also meant branching across 8 different repositories. Often, this meant that + some packages weren't branched, and this made it even more confusing to build straight from the + repos.

+

It also became very difficult to follow Yesod development. People tracking the yesod repo + itself suddenly saw a significant drop in activity, since most development now occurs on + yesod-core. And combined with my non-Yesod packages (yes, I do write code besides Yesod), my + personal Github account was at about 80 repositories, making it very difficult to find Yesod + code.

+

Solution

+

The solution is actually very simple. There is now a yesodweb organization on Github. Instead + of an individual repository for each package, it has "megarepositories" grouping together common + packages. The wai repo contains WAI, Warp, wai-handler-fastcgi, wai-test and others. Persistent + contains the core persistent code, as well as all the backends and TH wrappers. (Actually, this + was already the case, a fluke of our development history.) And yesod contains a bunch of yesod-* + packages, plus the wrapping yesod package.

+

With the new setup, it should be significantly simpler for users to try out beta code: clone a + few repos and run the convenient "install-all.sh" script. This script also leverages the new + "cabal test" framework to automatically run unit tests. The goal is that by having more eyes on + the code, we can do a better job. And by having more people testing beta release before they + arrive an Hackage, we can catch bugs before they go into the wild, and particularly eliminate bad + API decisions.

+

Why now?

+

Well, why not? Actually, it's a very good question. We are quickly approaching the 0.9 release + of Yesod. We're waiting on the release of a few libraries that we're very excited to be using + (I'll leave that as a surprise for later), and hopefully within the next week or two we'll be + ready to go live. 0.9 is a very important release: it's our last milestone before the 1.0 + release.

+

Over this last development iteration, you'll be interested to know that there were no + breaking changes in yesod-core. I consider this a huge milestone: the base Yesod API has + stabilized to such an extent that we didn't feel the need to break people's code this time. There + were still changes in the underlying libraries: removal of polymoprhic Hamlet, a greatly improved + forms API, drastically cleaned up Persistent architecture, etc. But the core of our framework is + stabilizing.

+

Now is when we're getting very serious about taking Yesod to the next level. We have all the + raw features we want. Most of them have been there for over a year. We've had a number of + iterations to clean things up. We've found high quality libraries we can rely on: text, + blaze-builder, enumerator.

+

Our focus at this point is to refine Yesod. We're focusing on things like more helpful logging, + better deployment strategies, increased performance. In the next two weeks, we'll be tying off a + very high-quality release, and beginning on the last touch-ups before our 1.0 release.

+

How you can help

+

If you've made it this far, odds are you care about Yesod. So if you'd like to help, here are + some ideas:

  • We love the positive feedback, but the constructive criticism is what helps us grow. Keep it + coming!
  • +
  • Participate in the discussion on our new mailing list.
  • +
  • Download the code now and test it with your current codebase.
  • +
  • Keep up the comments on the book.
  • +

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/0.9-rc3-and-scaffold-upgrade.html b/public/blog/2011/08/0.9-rc3-and-scaffold-upgrade.html new file mode 100644 index 00000000..602de84f --- /dev/null +++ b/public/blog/2011/08/0.9-rc3-and-scaffold-upgrade.html @@ -0,0 +1,86 @@ + Yesod 0.9 Release Candidate 3 +

Yesod 0.9 Release Candidate 3

August 26, 2011

GravatarBy Greg Weber

Yesod Release Candidate 3 is out!

+

Thanks to those who spotted issues with the scaffolding in the previous release candidates. So far no issues have been reported in Yesod proper, so if we keep the streak going through the weekend we will put out the official release.

+

Also, we are going to version bump from 0.9.0 to 0.9.1 after the release so that you don't have to blow away your .ghc folder when the release comes out. So now you have no excuse to avoid upgrading right now.

+

To get the latest RC, add this to your ~/.cabal/config:

+
remote-repo: yesod-yackage:http://yackage.yesodweb.com
+
+

And run:

+
cabal update && cabal install 'yesod >= 0.9.0'
+
+

More Information on upgrading your site

+

First, see the previous upgrading post, which tackles most of the upgrade issues. I will first mention a couple more changes I had to do, that will be obvious when you compile:

+
  • import Text.Blaze.Renderer.String (renderHtml) -> import Text.Blaze.Renderer.Text (renderHtml)
  • +
  • tabs are not allowed in hamlet
  • +
+

Extra Hamlet Upgrade Info

+

A big change not mentioned in specific detail is changing the use of [hamlet||]. You should probably just skip this paragraph and read the new documentation, which covers how to use Yesod 0.9. When you upgrade your site, you may have to use the terms ihamlet, whamlet, or even shamlet. Which one of these funny names should you use? It is pretty simple, the first letter stands for something. ihamlet isn't an Apple device that makes pork, it just allows internationalization interpolation in hamlet.

+ + + + + + + + + + + + + + + + + + + + + + +
QuasiQuoterPrefix letterInterpolation
shamlets = simple#{}
hamletnone#{} @{}
ihamleti = internationalized#{} @{} ^{} _{}
whamletw = widgets#{} @{} ^{} _{}
+

And you need to prefix using these templates with toWidget (or if you were using addHtml or something similar, remove that and use toWidget). This all seems like extra work, but we are hoping it will pay off with understandable error messages.

+

Scaffolding Changes

+

One of the reasons the scaffolding had a couple issues in the RC is because of all the great stuff that has been added to it. We are going to work on taking some of the code out of the scaffold and into the library, but much of this does belong in the scaffold to be configured by the user. These changes should effect just a few key common files instead of your entire site, but they are also a more error-prone and manual process. You don't have to do all of this conversion now if you don't need the features. But I highly recommend getting the new logger working properly, even if you don't need all the new configurable settings.

+

I recommend generating a new scaffold with the same foundation type, and copying over the new functionality. Luckily we know the compiler will guide you with your changes.

+

Settings.hs, and cabal file

+
  • copy over config/settings.yml, and change the settings appropriately (to match what is in your Settings.hs file)
  • +
  • copy over Settings.hs, but you have to manually inspect against your config/Settings.hs and make sure you don't lose any needed customizations.
  • +
  • copy over main.hs (previously it was named after your project and located in the config dir), and change your cabal file so the executable points to main.hs. Likely you don't have anything you care about that gets overwritten (but double check).
  • +
  • while in your cabal file, add dependencies for cmd-args, data-object, data-object-yaml, yesod-core, shakespeare-text, and unix (if you are on a unix based system).
  • +
  • move config/StaticFiles.hs to Settings/StaticFile.hs (create the Settings directory first) and then you can remove config from the hs-source-dirs section of your cabal file. This can make life easier for some IDEs.
  • +
+

Controller.hs -> Application.hs

+
  • rename Controller.hs to Application.hs
  • +
+

The best approach may be to copy over the Application.hs file from a new scaffolded site. Here are instructions for making indifidual changes:

+
  • approot = Settings.appRoot . settings
  • +
  • add to Application.hs:
  • +
+
import Yesod.Logger (makeLogger, flushLogger, Logger, logLazyText, logString)
import Network.Wai.Middleware.Debug (debugHandle)
-- only if on unix
import qualified System.Posix.Signals as Signal
import Control.Concurrent (forkIO, killThread)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
+
  • copy the new withFOUNDATION handlers from the new scaffolding site you generated.
  • +
+

foundation file -> Foundation.hs

+
  • rename your foundation type file (the one with instance Yesod to Foundation.hs. This isn't necessary, but will allow you to communicate better with others about your site.

  • +
  • add these to your foundation type

  • +
+
settings :: Settings.AppConfig
getLogger :: Logger
+
  • add to your imports
  • +
+
import Yesod.Logger (Logger, logLazyText)
+
  • add to your Yesod instance
  • +
+
messageLogger y loc level msg =
formatLogMessage loc level msg >>= logLazyText (getLogger y)
+

When this is all working, you should see all requests logged, and logging messages should be thread-safe, and you should be able to configure your application as you see fit. We will go over some more aspects of these features in the release announcement.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/announcing-yesod-0.9.html b/public/blog/2011/08/announcing-yesod-0.9.html new file mode 100644 index 00000000..629f2ed8 --- /dev/null +++ b/public/blog/2011/08/announcing-yesod-0.9.html @@ -0,0 +1,96 @@ + Yesod 0.9 Release Anouncement +

Yesod 0.9 Release Anouncement

August 29, 2011

GravatarBy Greg Weber

Yesod 0.9 Release

Yesod 0.9 is on hackage! This release represents a massive amount of improvements, in some cases resulting in significant API changes. We believe this release is good enough to be a 1.0 (a release to be proud of, with a stable API). But first we need you to use it for a while and give us all of your great feedback.

+

We had a changelog page going for a while now to keep track of changes. But here we will explain all of the changes in their full glory.

+

Versioning

+

Yesod is split into many separate packages, all eventually wrapped up by the "yesod" package itself. When we say version 0.9, we are referring to the version of the "yesod" package.

+

This release releases version 0.9.1 (not 0.9.0) to hackage so that release candidate users will have an easy time upgrading.

+

Most of the changes for this release are not part of the "yesod" package, but rather underlying packages like hamlet or yesod-forms. As such, many of these changes have been available on Hackage since before the actual 0.9 release. However, a standard usage of the "yesod" package would not notice them. For more information, please see Using non-standard package versions.

+

Shakespearean templates (Hamlet)

+

Yesod started with hamlet, which gives one the ability to just insert variables into templates as they might be used to with a dynamic language. This was so powerful that Michael made hamlet-like template languages for css and javascript. "hamlet" is the name of the html template. We are now officially adopting the term "Shakespeare" for this style of templates.

+

To facilitate this, the hamlet package underwent a re-organization and was broken up into separate packages: hamlet (just hamlet), shakespeare-js (julius), shakespeare-css (lucius, cassius), and xml-hamlet. And there is a shakespeare package that contains the base code for making shakespeare-style templating languages. This makes it really easy to create a new shakespeare style template for any of your templating needs.

+

The major addition to this release is the shakespeare-text package. shakespeare-text is a good example of how easy it is to create a new pass-through Shakespeare template language. All it does is allow you to insert variable into normal Haskell text. This is great for creating e-mail templates, but we are also exposing 2 helpers you can choose to use in your normal haskell code. st for strict text, and lt for lazy text.

+
{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
import Text.Shakespeare.Text (st)
import Data.Text.IO (putStrLn)
import Prelude hiding (putStrLn)

main = do
let version = "0.9"
putStrLn [st|Yesod release #{version}|]
+

As with all Shakespeare templates, there is no dependency on haskell-src-extras. If you aren't using Yesod and don't mind that dependency, there are some similar packages to shakespeare-text: Interpolation, interpolatedstring-perl6, interpolatedstring-qq.

+

The major difference users will notice in this release the removal of directly polymorphic Hamlet. You now choose the simplest hamlet version that suits your needs so you don't have to pay in efficiency for features you aren't using. However, the real motivation was to produce better error messages. We have upgrade notes in the previous post and the book documentation has been updated.

+

Forms

+

Just like templating, forms underwent a major cleanup. Polymorphic forms were removed, which should get rid of the most annoying error message Yesod users encountered. Some functions were also renamed- see the initial release candidate anouncement for upgrade instructions.

+

Thanks to Luite Stegman's help, we also added a way to easily add custom verification to your forms with the following new functions: check, checkM, and checkBool. You can see the original anouncement or the updated book documentation.

+

WAI

+

wai-app-static works together with yesod-static to now automatically set proper caching headers. This will have a huge impact, because our recommended deployment approach is now to just run your binary. Apache/Nginx are no longer required for static file serving. wai-app-static also saw some other improvements to make desktop usage better- static files can be embedded directly in the executable rather than being on the filesystem.

+

The debug middleware now uses the new logger to print out a thread-safe log which now includes all POST parameters.

+

Yesod

+

There is built-in support for BrowserID, a next generation, finally user-friendly authentication system. Yesod has added a logging interface, allowing you to switch to any logging system you want. But we included a very simple thread-safe logger, which we plan on switching out for a faster one in the near future. The logging interface give you the option to log the file and line number using template haskell. This same technique is available for use in my recently release file-location debugging utility package.

+

There have been a lot of other improvements. One example is more secure client side cookies, using CBC mode instead of ECB. (Thanks to Loïc Maury.)

+

Scaffolding

+

Scaffolding is what is generated by yesod init. This is where the most noticeable Yesod improvements show up.

+
  • The new logger setup to use the new WAI debug middleware.
  • +
  • in development mode emails are logged instead of sent. This is actually more convenient and gets rid of the sendmail dependency for development.
  • +
  • Signal handling on unix added to scaffolder (should probably be moved out of the handler).
  • +
  • Documentation and code for deploying to Heroku (deploy/Procfile)
  • +
  • upgrade to html5boilerplate version 2, which just means use normalize.css (instead of a css reset)
  • +
  • file name changes - see the previous upgrade post
  • +
  • configurable settings - you can place settings into yaml configuration files. By default, this is done for the host, port, connection pool size, and other database settings. And you can override settings with command line switches. The default allows you to override port argument. This added flexibility now makes deploying to Heroku much more straightforward.
  • +

Persistent 0.6 Changes

Feature-wise, there were only a few additions to Persistent 0.6 The most noticeable addition is support for "OR" queries. However, the implementation has undergone a complete re-write, making Persistent more powerful and flexible, and allowing us to switch from constructors to operators.

+

Persistent 0.6, a re-write and a better API

+

Old:

+
selectList [PersonNameEq "bob"] [] 0 0
+

New:

+
selectList [PersonName ==. "bob"] []
+

We are using 2 extra characters to make the same selection. However, the separation between field and operator makes for more readable code, and we have removed one use of Template Haskell to create the constructor with an Eq postfix. Our policy on Template Haskell has always been to use it only when the same cannot be achieved without it. When Michael originally wrote Persistent, it wasn't clear to him how to implement the operator approach rather than the constructor approach. The release of the Groundhog library showed how this can be done. We had a discussion with Boris Lykah, the Groundhog author, and figured out what we could learn from Groundhog. Persistent is now using phantom types, similar to Boris's approach.

+

We also discussed possible APIs at length. For Persistent we settled on something that is composable in a way similar to Groundhog, but slightly more optimized for the common use case. The main difference people will notice is no more hanging zeroes! That can be seen in the example above. The zeroes represent the limit and offset. Lets see how they are handled now when you actually need them:

+

Old:

+
selectList [PersonName ==. "bob"] [] 5 5
+

New:

+
selectList [PersonName ==. "bob"] [OffsetBy 5, LimitTo 5]
+

More verbose when you actually need it- but still better in that case in my mind because it is now clear which is the limit and which is the offest- the reason why I used 5 for both is because I never remembered the argument order of the older style.

+

The old style of sorting required constructors created by Template Haskell. Here again, we used the Groundhog approach of re-using Desc and Asc constructors with the field types.

+

Old:

+
selectList [] [PersonNameAsc, PersonLastNameAsc] 0 0
+

New:

+
selectList [] [Asc PersonName, Asc PersonLastName]
+

This shows why we are using lists- it makes it easy to combine pieces of a query in Haskell syntax. The most common query combination is to AND multiple conditions:

+
selectList [PersonName ==. "bob", PersonLastName ==. "jones"] []
+

Lets show the OR implementation:

+
selectList ([PersonName ==. "bob"] ||. [PersonLastName ==. "jones"]) []
+

This is not a great DSL syntax- it does require parentheses. However, OR is actually a very rare use case- so we just want something that works well and is intuitive rather than the most compact notation.

+

Less Template Haskell is good

+

Persistent still requires the same basic amount of Quasi-Quoting from the user to declare datatypes.

+
mkPersist sqlSettings [persist|
+  Person
+    name String
+    lastName String
+|]
+
+

However, operator annotations (Update, LT, GT, etc) are no longer required, reducing what is required from users in their quasi-quoted models for the normal use case. And the Template Haskell generated is now substantially less. This should speed up compilation time and make Persistent more extensible. The Quasi-Quoting of datatypes is still important to allow for a friendly interface with easy customizations. However, it would no longer be difficult to provide a Template Haskell interface to Persistent (and we would welcome such a patch). In fact, the Template Haskell portion of persistent is an entirely separate cabal project, and one can manually declare types in the persistent style now. There are some iphone users doing this now because Template Haskell did not work on the iphone.

+

Limitations

+

MongoDB is waiting on some upstream improvements we are helping to create in the MongoDB driver. This release has been great for Mongo though- we are really excited about the ability to add custom operators to better take advantage of MongoDB.

+

The biggest limitation on Persistent that makes using it worse is Haskell's lack of record name-spacing.

0.9 goal accomplishments

We laid out a list of 1.0 goals after the 0.8 release. Lets go through each of them and see how things are coming along:

+
  • Documentation- We are really proud in the increase of breadth and quality of the documentation. We are now at the point where the book, in combination with supplemental documentation on the wiki and in blog posts is giving people the high-level documentation that they need. The most common complaint is now about the lack of detail in the haddocks. So we are going to make a push to improve the haddocks, and also link between them and the book.
  • +
  • static file caching headers support - Done (mentioned in WAI section).
  • +
  • Support for other template types as first class citizens- the templating system has undergone a re-organization that helps make the whole situation clearer, and adding new template systems should be more straightforward.
  • +
  • Easier support for faster, non-blocking Javascript loading via something akin to require.js- not yet started.
  • +
  • A complete i18n solution- shipped!
  • +
  • Reassessing our forms package- yesod-forms were re-written to produce clearer error messages and additional features were added.
  • +
  • Embedded objects in MongoDB- just started
  • +
  • Performance improvements- Kazu has been our main source of performance improvements, submitting a patch for a faster SendFile, and writing a blazing-fast logger just before this release that we will soon incorporate. As always, we eagerly tackle any reports of slow spots in the framework. Our only other planned speedup is to use a faster system for route matching.
  • +
+

There are some additional 1.0 goals we are considering, including:

+
  • easy validation of your persistent models
  • +
  • better interactions between the models and forms
  • +
  • improving the development mode experience
  • +
+

Thanks you, Yesoders

+

I am really proud that we have accomplished our most important 1.0 goals already while cramming in so many other great changes. Thanks to everyone in the community that used the bleeding edge code, contributed bug reports, asked and answered questions, submitted pull requests, weighed in on design decisions, added things to the wiki, or suggested documentation improvements- you helped make this the by far the best Yesod release ever.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/monad-control.html b/public/blog/2011/08/monad-control.html new file mode 100644 index 00000000..5b4d82b2 --- /dev/null +++ b/public/blog/2011/08/monad-control.html @@ -0,0 +1,340 @@ + monad-control +

monad-control

August 25, 2011

GravatarBy Michael Snoyman

Overview

+

One of the powerful, and sometimes confusing, features in Haskell is monad transformers. They + allow you to take different pieces of functionality- such as mutable state, error handling, or + logging- and compose them together easily. Though I swore I'd never write a monad tutorial, I'm + going to employ a painful analogy here: monads are like onions. (Monads are not like cakes.) By + that, I mean layers.

+

We have the core monad- also known as the innermost or bottom monad. I'll likely use all forms + of terminology here. On top of this core, we add layers, each adding a new feature and spreading + outward/upward. As a motivating example, let's consider an Error monad stacked on top of the IO + monad:

newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
+type MyStack = ErrorT MyError IO
+
Now + pay close attention here: ErrorT is just a simple newtype around an Either wrapped in a monad. + Getting rid of the newtype, we + have:
type ErrorTUnwrapped e m a = m (Either e a)
+

+

At some point, we'll need to actually perform some IO inside our MyStack. If we went with the + unwrapped approach, it would be trivial, since there would be no ErrorT constructor in the way. + However, we need that newtype wrapper for a whole bunch of type reasons I won't go into here + (this isn't a monad transformer tutorial after all). So the solution is the MonadTrans + typeclass:

+
class MonadTrans t where
+    lift :: Monad m => m a -> t m a
+
+

I'll admit, the first time I saw that type signature, my response was stunned confusion, and + incredulity that it actually meant anything. But looking at an instance helps a + bit:

instance (Error e) => MonadTrans (ErrorT e) where
+    lift m = ErrorT $ do
+        a <- m
+        return (Right a)
+
All + we're doing is wrapping the inside of the IO with a Right value, and then applying our newtype + wrapper. This allows us to take an action that lives in IO, and "lift" it to the outer/upper + monad.

+

But now to the point at hand. This works very well for simple functions. For example:

+
sayHi :: IO ()
+sayHi = putStrLn "Hello"
+
+sayHiError :: ErrorT MyError IO ()
+sayHiError = lift $ putStrLn "Hello"
+
+

But let's take something slightly more complicated, like a callback:

+
withMyFile :: (Handle -> IO a) -> IO a
+withMyFile = withFile "test.txt" WriteMode
+
+sayHi :: Handle -> IO ()
+sayHi handle = hPutStrLn handle "Hi there"
+
+useMyFile :: IO ()
+useMyFile = withMyFile sayHi
+
+

So far so good, right? Now let's say that we need a version of sayHi that has access to the + Error monad:

+
sayHiError :: Handle -> ErrorT MyError IO ()
+sayHiError handle = do
+    lift $ hPutStrLn handle "Hi there, error!"
+    throwError MyError
+
+

We would like to write a function that combines withmyFile and sayHiError. Unfortunately, GHC + doesn't like this very + much:

useMyFileErrorBad :: ErrorT MyError IO ()
+useMyFileErrorBad = withMyFile sayHiError
+
+    Couldn't match expected type `ErrorT MyError IO ()'
+                with actual type `IO ()'
+
Why + does this happen, and how can we work around it?

+

Intuition

+

Let's try and develop an external intuition of what's happening here. The ErrorT monad + transformer adds extra functionality to the IO monad. We've defined a way to "tack on" that extra + functionality to normal IO actions: we add that Right constructor and wrap it all in ErrorT. + Wrapping in Right is our way of saying "it went OK," there wasn't anything wrong with this + action.

+

Now this intuitively makes sense: since the IO monad doesn't have the concept of returning a + MyError when something goes wrong, it will always succeed in the lifting phase. (Note: This has + nothing to do with runtime exceptions, don't even think about them.) What we have is a + guaranteed one-directional translation up the monad stack.

+

Let's take another example: the Reader monad. A Reader has access to some extra piece of data + floating around. Whatever is running in the inner monad doesn't know about that extra piece of + information. So how would you do a lift? You just ignore that extra information. The Writer + monad? Don't write anything. State? Don't change anything. I'm seeing a pattern here.

+

But now let's try and go in the opposite direction: I have something in a Reader, and I'd like + to run it in the base monad (e.g., IO). Well... that's not going to work, is it? I need that + extra piece of information, I'm relying on it, and it's not there. There's simply no way to go in + the opposite direction without providing that extra value.

+

Or is there? If you remember, we'd pointed out earlier that ErrorT is just a simple wrapper + around the inner monad. In other words, if I have errorValue :: ErrorT MyError IO + MyValue, I can apply runErrorT and get a value of type IO + (Either MyError MyValue). The looks quite a bit like bi-directional translation, + doesn't it?

+

Well, not quite. We originally had an ErrorT MyError IO monad, with a value of + type MyValue. Now we have a monad of type IO with a value of + type Either MyError MyValue. So this process has in fact changed the value, + while the lifting process leaves it the same.

+

But still, with a little fancy footwork we can unwrap the ErrorT, do some processing, and then + wrap it back up again.

+
useMyFileError1 :: ErrorT MyError IO ()
+useMyFileError1 =
+    let unwrapped :: Handle -> IO (Either MyError ())
+        unwrapped handle = runErrorT $ sayHiError handle
+        applied :: IO (Either MyError ())
+        applied = withMyFile unwrapped
+        rewrapped :: ErrorT MyError IO ()
+        rewrapped = ErrorT applied
+     in rewrapped
+
+

This is the crucial point of this whole article, so look closely. We first unwrap our monad. + This means that, to the outside world, it's now just a plain old IO value. Internally, we've + stored all the information from our ErrorT transformer. Now that we have a plain old IO, we can + easily pass it off to withMyFile. withMyFile takes in the internal state and passes it back out + unchanged. Finally, we wrap everything back up into our original ErrorT.

+

This is the entire pattern of monad-control: we embed the extra features of our monad + transformer inside the value. Once in the value, the type system ignores it and focuses on the + new outermost monad. When we're done playing around with that inner monad, we can pull our state + back out and reconstruct our original monad stack.

+

Types

+

I purposely started with the ErrorT transformer, as it is one of the simplest for this + inversion mechanism. Unfortunately, others are a bit more complicated. Take for instance ReaderT. + It is defined as newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }. If + we apply runReaderT to it, we get a function that returns a monadic value. So + we're going to need some extra machinery to deal with all that stuff. And this is when we leave + Kansas behind.

+

There are a few approaches to solving these problems. In the past, I implemented a solution + using type families in the neither package. Anders Kaseorg implemented a much + more straight-forward solution in monad-peel. And for efficiency, in + monad-control, Bas van Dijk uses CPS (continuation passing style) and + existential types.

+

The first type we're going to look at + is:

type Run t = forall n o b. (Monad n, Monad o, Monad (t o)) => t n b -> n (t o b)
+
That's + incredibly dense, let's talk it out. The only "input" datatype to this thing is t, a monad + transformer. A Run is a function that will then work with any combination of types n, o + and b (that's what the forall means). n and o are both monads, while b is a simple value + contained by them.

+

The left hand side of the Run function, t n b, is our monad transformer + wrapped around the n monad and holding a b value. So for example, that could be a MyTrans + FirstMonad MyValue. It then returns a value with the transformer "popped" inside, with + a brand new monad at its core. In other words, FirstMonad (MyTrans NewMonad + MyValue).

+

That might sound pretty scary at first, but it actually isn't as foreign as you'd think: this + is essentially what we did with ErrorT. We started with ErrorT on the outside, wrapping around + IO, and ended up with an IO by itself containing an Either. Well guess what: another way to + represent an Either is ErrorT MyError Identity. So essentially, we pulled the IO + to the outside and plunked an Identity in its place. We're doing the same thing in a Run: pulling + the FirstMonad outside and replacing it with a NewMonad.

+

Alright, now we're getting somewhere. If we had access to one of those Run functions, we could + use it to peel off the ErrorT on our sayHiError function and pass it to withMyFile. With the + magic of undefined, we can play such a game:

+
errorRun :: Run (ErrorT MyError)
+errorRun = undefined
+
+useMyFileError2 :: IO (ErrorT MyError Identity ())
+useMyFileError2 =
+    let afterRun :: Handle -> IO (ErrorT MyError Identity ())
+        afterRun handle = errorRun $ sayHiError handle
+        applied :: IO (ErrorT MyError Identity ())
+        applied = withMyFile afterRun
+     in applied
+
+

This looks eerily similar to our previous example. In fact, errorRun is acting almost + identically to runErrorT. However, we're still left with two problems: we don't know where to get + that errorRun value from, and we still need to restructure the original ErrorT after we're + done.

+

MonadTransControl

+

Obviously in the specific case we have before us, we could use our knowledge of the ErrorT + transformer to beat the types into submission and create our Run function manually. But what we + really want is a general solution for many transformers. At this point, you know we need + a typeclass.

+

So let's review what we need: access to a Run function, and some way to restructure our + original transformer after the fact. And thus was born MonadTransControl, with its single method + liftControl:

+
class MonadTrans t => MonadTransControl t where
+    liftControl :: Monad m => (Run t -> m a) -> t m a
+
+

Let's look at this closely. liftControl takes a function (the one we'll be writing). That + function is provided with a Run function, and must return a value in some monad (m). liftControl + will then take the result of that function and reinstate the original transformer on top of + everything.

+
useMyFileError3 :: Monad m => ErrorT MyError IO (ErrorT MyError m ())
+useMyFileError3 =
+    liftControl inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+
+

Close, but not exactly what I had in mind. What's up with the double monads? Well, let's start + at the end: sayHiError handle returns a value of type ErrorT MyError IO (). This + we knew already, no surprises. What might be a little surprising (it got me, at least) is the + next two steps.

+

First we apply run to that value. Like we'd discussed before, the result is that the IO inner + monad is popped to the outside, to be replaced by some arbitrary monad (represented by m here). + So we end up with an IO (ErrorT MyError m ()). Ok... We then get the same result after applying + withMyFile. Not surprising.

+

The last step took me a long time to understand correctly. Remember how we said that we + reconstruct the original transformer? Well, so we do: by plopping it right on top of everything + else we have. So our end result is the previous type- IO (ErrorT MyError m ())- + with a ErrorT MyError stuck on the front.

+

Well, that seems just about utterly worthless, right? Well, almost. But don't forget, that "m" + can be any monad, including IO. If we treat it that way, we get ErrorT MyError IO (ErrorT + MyError IO ()). That looks a lot like m (m a), and we want just plain + old m a. Fortunately, now we're in + luck:

useMyFileError4 :: ErrorT MyError IO ()
+useMyFileError4 = join useMyFileError3
+
And + it turns out that this usage is so common, that Bas had mercy on us and defined a helper + function:
control :: (Monad m, Monad (t m), MonadTransControl t)
+        => (Run t -> m (t m a)) -> t m a
+control = join . liftControl
+
So + all we need to write + is:
useMyFileError5 :: ErrorT MyError IO ()
+useMyFileError5 =
+    control inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

+

And just to make it a little + shorter:

useMyFileError6 :: ErrorT MyError IO ()
+useMyFileError6 = control $ \run -> withMyFile $ run . sayHiError
+

+

MonadControlIO

+

The MonadTrans class provides the lift method, which allows you to lift an action one level in + the stack. There is also the MonadIO class that provides liftIO, which lifts an IO action as far + in the stack as desired. We have the same breakdown in monad-control. But first, we need a + corrolary to + Run:

type RunInBase m base = forall b. m b -> base (m b)
+
Instead + of dealing with a transformer, we're dealing with two monads. base is the underlying monad, and m + is a stack built on top of it. RunInBase is a function that takes a value of the entire stack, + pops out that base, and puts in on the outside. Unlike in the Run type, we don't replace it with + an arbitrary monad, but with the original one. To use some more concrete types:

+
RunInBase (ErrorT MyError IO) IO = forall b. ErrorT MyError IO b -> IO (ErrorT MyError IO b)
+
+

This should look fairly similar to what we've been looking at so far, the only difference is + that we want to deal with a specific inner monad. Our MonadControlIO class is really just an + extension of MonadControlTrans using this RunInBase.

+
class MonadIO m => MonadControlIO m where
+    liftControlIO :: (RunInBase m IO -> IO a) -> m a
+
+

Simply put, liftControlIO takes a function which receives a RunInBase. That RunInBase can be + used to strip down our monad to just an IO, and then liftControlIO builds everything back up + again. And like MonadControlTrans, it comes with a helper function

+
controlIO :: MonadControlIO m => (RunInBase m IO -> IO (m a)) -> m a
+controlIO = join . liftControlIO
+
+

We can easily rewrite our previous example with + it:

useMyFileError7 :: ErrorT MyError IO ()
+useMyFileError7 = controlIO $ \run -> withMyFile $ run . sayHiError
+
And + as an advantage, it easily scales to multiple + transformers:
sayHiCrazy :: Handle -> ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+sayHiCrazy handle = liftIO $ hPutStrLn handle "Madness!"
+
+useMyFileCrazy :: ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+useMyFileCrazy = controlIO $ \run -> withMyFile $ run . sayHiCrazy
+

+

Real Life Examples

+

Let's solve some real-life problems with this code. Probably the biggest motivating use case is + exception handling in a transformer stack. For example, let's say that we want to automatically + run some cleanup code when an exception is thrown. If this were normal IO code, we'd + use:

onException :: IO a -> IO b -> IO a
+
But if we're + in the ErrorT monad, we can't pass in either the action or the cleanup. In comes controlIO to the + rescue:
onExceptionError :: ErrorT MyError IO a
+                 -> ErrorT MyError IO b
+                 -> ErrorT MyError IO a
+onExceptionError action after = controlIO $ \run ->
+    run action `onException` run after
+

+

Let's say we need to allocate some memory to store a Double in. In the IO monad, we could just + use the alloca function. Once again, our solution is + simple:

allocaError :: (Ptr Double -> ErrorT MyError IO b)
+            -> ErrorT MyError IO b
+allocaError f = controlIO $ \run -> alloca $ run . f
+

+

Lost State

+

Let's rewind a bit to our onExceptionError. It uses onException under the surface, which has a + type signature: IO a -> IO b -> IO a. Let me ask you something: what happened to + the b in the output? Well, it was thoroughly ignored. But that seems to cause us a bit of a + problem. After all, we store on transformer state information in the value of the inner monad. If + we ignore it, we're essentially ignoring the monadic side effects as well!

+

And the answer is that, yes, this does happen with monad-control. Certain functions will drop + some of the monadic side effects. This is put best by Bas, in the comments on the relevant + functions:

Note, any monadic side effects in m of the "release" computation will be + discarded; it is run only for its side effects in IO.
In practice, monad-control will + usually be doing the right thing for you, but you need to be aware that some side effects may be + disappearing.

+

More Complicated Cases

+

In order to make our tricks work so far, we've needed to have functions that give us full + access to play around with their values. Sometimes, this isn't the case. Take, for instance:

+
addMVarFinalizer :: MVar a -> IO () -> IO ()
+
+

In this case, we are required to have no value inside our finalizer function. Intuitively, the + first thing we should notice is that there will be no way to capture our monadic side effects. So + how do we get something like this to compile? Well, we need to explicitly tell it to drop all of + its state-holding + information:

addMVarFinalizerError :: MVar a -> ErrorT MyError IO () -> ErrorT MyError IO ()
+addMVarFinalizerError mvar f = controlIO $ \run ->
+    return $ liftIO $ addMVarFinalizer mvar (run f >> return ())
+

+

Another case from the same module + is:

modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b
+
Here, + we have a restriction on the return type in the second argument: it must be a tuple of the value + passed to that function and the final return value. Unfortunately, I can't see a way of writing a + little wrapper around modifyMVar to make it work for ErrorT. Instead, in this case, I copied the + definition of modifyMVar and modified it:

+
modifyMVar :: MVar a
+           -> (a -> ErrorT MyError IO (a, b))
+           -> ErrorT MyError IO b
+modifyMVar m io =
+  Control.Exception.Control.mask $ \restore -> do
+    a      <- liftIO $ takeMVar m
+    (a',b) <- restore (io a) `onExceptionError` liftIO (putMVar m a)
+    liftIO $ putMVar m a'
+    return b
+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/new-forms-chapter.html b/public/blog/2011/08/new-forms-chapter.html new file mode 100644 index 00000000..28547091 --- /dev/null +++ b/public/blog/2011/08/new-forms-chapter.html @@ -0,0 +1,504 @@ + Forms 0.9 +

Forms 0.9

August 31, 2011

GravatarBy Michael Snoyman

Forms

+

I've mentioned the boundary issue already: whenever data enters or leaves an application, we + need to validate our data. Probably the most difficult place this occurs is forms. Coding forms + is complex; in an ideal world, we'd like a solution that addresses the following problems:

+
  • Ensure data is valid.
  • +
  • Marshal string data in the form submission to Haskell datatypes.
  • +
  • Generate HTML code for displaying the form.
  • +
  • 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.
  • +
  • Automatically assign names to our fields that are guaranteed to be unique.
  • +
+

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 Haskell's type system to make sure everything + is working correctly.

+

Synopsis

{-# LANGUAGE QuasiQuotes, TemplateHaskell, MultiParamTypeClasses,
+OverloadedStrings, TypeFamilies #-}
+import Yesod
+import Yesod.Form.Jquery
+import Data.Time (Day)
+import Data.Text (Text)
+import Control.Applicative ((<$>), (<*>))
+
+data Synopsis = Synopsis
+
+mkYesod "Synopsis" [parseRoutes|
+/ RootR GET
+/person PersonR POST
+|]
+
+instance Yesod Synopsis where
+    approot _ = ""
+
+-- Tells our application to use the standard English messages.
+-- If you want i18n, then you can supply a translating function instead.
+instance RenderMessage Synopsis FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- And tell us where to find the jQuery libraries. We'll just use the defaults,
+-- which point to the Google CDN.
+instance YesodJquery Synopsis
+
+-- The datatype we wish to receive from the form
+data Person = Person
+    { personName :: Text
+    , personBirthday :: Day
+    , personFavoriteColor :: Maybe Text
+    , personEmail :: Text
+    , personWebsite :: Maybe Text
+    }
+  deriving Show
+
+-- Declare the form. The type signature is a bit intimidating, but here's the
+-- overview:
+--
+-- * The Html parameter is used for encoding some extra information, such as a
+-- nonce for avoiding CSRF attacks
+--
+-- * We have the sub and master site types, as usual.
+--
+-- * FormResult can be in three states: FormMissing (not data available),
+-- FormFailure (invalid data) and FormSuccess
+--
+-- * The Widget is the viewable form to place into the web page.
+personForm :: Html -> Form Synopsis Synopsis (FormResult Person, Widget)
+personForm = renderDivs $ Person
+    <$> areq textField "Name" Nothing
+    <*> areq (jqueryDayField def
+        { jdsChangeYear = True -- give a year dropdown
+        , jdsYearRange = "1900:-5" -- 1900 till five years ago
+        }) "Birthday" Nothing
+    <*> aopt textField "Favorite color" Nothing
+    <*> areq emailField "Email address" Nothing
+    <*> aopt urlField "Website" Nothing
+
+-- The GET handler displays the form
+getRootR :: Handler RepHtml
+getRootR = do
+    -- Generate the form to be displayed
+    ((_, widget), enctype) <- generateFormPost personForm
+    defaultLayout [whamlet|
+<p>The widget generated contains only the contents of the form, not the form tag itself. So...
+<form method=post action=@{PersonR} enctype=#{enctype}>
+    ^{widget}
+    <p>It also doesn't include the submit button.
+    <input type=submit>
+|]
+
+-- The POST handler processes the form. If it is successful, it displays the
+-- parsed person. Otherwise, it displays the form again with error messages.
+postPersonR :: Handler RepHtml
+postPersonR = do
+    ((result, widget), enctype) <- runFormPost personForm
+    case result of
+        FormSuccess person -> defaultLayout [whamlet|<p>#{show person}|]
+        _ -> defaultLayout [whamlet|
+<p>Invalid input, let's try again.
+<form method=post action=@{PersonR} enctype=#{enctype}>
+    ^{widget}
+    <input type=submit>
+|]
+
+main :: IO ()
+main = warpDebug 3000 Synopsis
+
+

Kinds of Forms

+

Before jumping into the types themselves, we should begin with an overview of the different + kinds of forms. There are three categories:

+
Applicative
+
These are the most commonly used (it's what appeared in the synopsis). Applicative gives us + some nice properties of letting error messages coallesce together and keep a very high-level, + declarative approach.
+ + +
Monadic
+
A more powerful alternative to applicative. While this allows you more flexibility, at the + cost of being more verbose. Useful if you want to create forms that don't fit into the standard + two-column look.
+ + +
Input
+
Used only for receiving input. Does not generate any HTML for receiving the user input. + Useful for interacting with existing forms.
+ +
+

In addition, there are a number of different variables that come into play for each form and + field you will want to set up:

+
  • Is the field required or optional?
  • +
  • Should it be submitted with GET or POST?
  • +
  • Does it have a default value, or not?
  • +
+

An overriding goal is to minimize the number of field definitions and let them work in as many + contexts as possible. One result of this is that we end up with a few extra words for each field. + In the synopsis, you may have noticed things like areq and that extra Nothing parameter. We'll + cover why all of those exist in the course of this chapter, but for now realize that by making + these parameters explicit, we are able to reuse the individuals fields (like + intField) in many different ways.

+

Types

+

The Yesod.Form.Types module declares a few types. Let's start off + with some simple helpers:

+
Enctype
+
The encoding type, either UrlEncoded or Multipart. This datatype declares an instance of + ToHtml, so you can use the enctype directly in Hamlet.
+ + +
Env
+
Maps a parameter name to a list of values.
+ + +
FileEnv
+
Maps a parameter name to the associated uploaded file.
+ + +
Ints
+
As mentioned in the introduction, yesod-form automatically assigns a unique name to each + field. Ints is used to keep track of the next number to assign.
+ + +
FormResult
+
Has one of three possible states: FormMissing if no data was submitted, FormFailure if there + was an error parsing the form (e.g., missing a required field, invalid content), or FormSuccess + if everything went smoothly.
+ +
+

Next we have three datatypes used for defining individual fields.

+ +
Field
+
Defines two pieces of functionality: how to parse the text input from a user into a Haskell + value, and how to create the widget to be displayed to the user. yesod-form defines a number of + individual Fields in Yesod.Form.Fields.
+ + +
FieldSettings
+
Basic information on how a field should be displayed, such as the display name, an optional + tooltip, and possibly hardcoded id and name attributes. (If none are provided, they are + automatically generated.)
+ + +
FieldView
+
An intermediate format containing a bunch of view information on a field. This is hardly + ever used directly by the user, we'll see more details later.
+ +
+

And finally, we get to the important stuff: the forms themselves. There are three types for + this: Form is for monadic forms, AForm for Applicative and IForm (declared in + IForm) for input. Form is actually just a simple + type synonym for a monad stack that provides the following features:

+
  • A Reader monad giving us the parameters (Env and FileEnv), the master site argument and the + list of languages the user supports. The last two are used for i18n (more on this later).
  • +
  • A Writer monad keeping track of the Enctype. A form will always be UrlEncoded, unless there + is a file input field, which will force us to use multipart instead.
  • +
  • A State monad holding an Ints to keep track of the next unique name to produce.
  • +
+

An AForm is pretty similar. However, there are a few major differences:

+
  • It produces a list of FieldViews. This allows us to keep an abstract idea of the form + display, and then at the end of the day choose an appropriate function for laying it out on the + page. In the synopsis, we used renderDivs, which creates a bunch of div tags. Another options + would be renderTable.
  • +
  • It does not provide a Monad instance. The goal of Applicative is to allow the entire form to + run, grab as much information on each field as possible, and then create the final result. This + cannot work in the context of Monad.
  • +
+

An IForm is even simpler: it returns either a list of error messages or a result.

+

Converting

+

"But wait a minute," you say. "You said the synopsis uses applicative forms, but I'm sure the + type signature said Form. Shouldn't it be Monadic?" That's true, the final form we produced was + monadic. But what really happened is that we converted an applicative form to a monadic one.

+

Again, our goal is to reuse code as much as possible, and minimize the number of functions in + the API. And Monadic forms are more powerful than Applicative, if more clumsy, so anything that + can be expressed in an Applicative form could also be expressed in a Monadic form. There are two + core functions that help out with this: aformToForm converts any applicative form to a monadic + one, and formToAForm converts certain kinds of monadic forms to applicative forms.

+

"But wait another minute," you insist. "I didn't see any aformToForm!" Also true. The + renderDivs function takes care of that for us.

+

Create AForms

+

Now that I've (hopefully) convinced you that in our synopsis we were really dealing with + applicative forms, let's have a look and try to understand how these things get created. Let's + take a simple example:

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    }
+  deriving Show
+
+carAForm :: AForm Synopsis Synopsis Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+
+carForm :: Html -> Form Synopsis Synopsis (FormResult Car, Widget)
+carForm = renderTable carAForm
+
+

Here, we've explicitly split up applicative and monadic forms. In carAForm, we use the <$> + and <*> operators. This should be surprising; these are almost always used in + applicative-style code. (For more information on applicative code, see the + Haskell wiki.) And we have one line for each record in our Car datatype. Perhaps + unsurprisingly, we have a textField for the Text record, and an intField for the Int record.

+

Let's look a bit more closely at the areq function. Its (simplified) type signature is + Field a -> FieldSettings -> Maybe a -> AForm a. So that first argument is going + to determine the datatype of this field, how to parse it, and how to render it. The next + argument, FieldSettings, tells us the label, tooltip, name and ID of the field. In this case, + we're using the previously-mentioned IsString instance of FieldSettings.

+

And what's up with that Maybe a? It provides the optional default value. So let's say we want + our form to fill in "2007" as the default car year, we would use areq intField "Year" + (Just 2007). We can even take this to the next level, and have a form that takes an + optional parameter giving the default values.

+
Form with default values
+ +
carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField "Year" (carYear <$> mcar)
+
+
+

Optional fields

Now let's say that we wanted to have an optional field (like the car color). All we do instead + is use the aopt function.

+
Optional fields
+ +
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Text
+    }
+  deriving Show
+
+carAForm :: AForm Synopsis Synopsis Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+    <*> aopt textField "Color" Nothing
+
+
+

And like required fields, the last argument is the optional default value. However, this has + two layers of Maybe wrapping. This may seem redundant (and it is), but it makes it much easier to + write code that takes an optional default form parameter.

+
Default optional fields
+ +
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Text
+    }
+  deriving Show
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField "Year" (carYear <$> mcar)
+    <*> aopt textField "Color" (carColor <$> mcar)
+
+carForm :: Html -> Form Synopsis Synopsis (FormResult Car, Widget)
+carForm = renderTable $ carAForm $ Just $ Car "Forte" 2010 $ Just "gray"
+
+
+

Validation

+

Let's say we only want to accept cars created after 1990. How would we go about limiting + things? If you remember, we said above that the Field itself contained the information on what is + a valid entry. So all we need to do is write a new Field, right? Well, that would be a bit + tedious. Instead, let's just modify an existing one:

+
carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt textField "Color" (carColor <$> mcar)
+  where
+    errorMessage :: Text
+    errorMessage = "Your car is too old, get a new one!"
+
+    carYearField = check validateYear intField
+
+    validateYear y
+        | y < 1990 = Left errorMessage
+        | otherwise = Right y
+
+

The trick here is the check function. It takes a function (validateYear) that returns either an + error message or a modified field value. In this example, we haven't modified the value at all. + That is usually going to be the case. Of course, this kind of checking is very common, so we have + a shortcut:

+
carYearField = checkBool (>= 1990) errorMessage intField
+
+

checkBool takes two parameters: a condition that must be fulfilled, and an error message to be + displayed if it was not.

+ +

This is great to make sure the car isn't too old. But what if we want to make sure that the + year specified is not from the future? In order to look up the year, we'll need to run some IO. + For such circumstances, we'll need checkM:

+
carYearField = checkM inPast $ checkBool (>= 1990) errorMessage intField
+
+    inPast y = do
+        thisYear <- liftIO getCurrentYear
+        return $ if y <= thisYear
+            then Right y
+            else Left ("You have a time machine!" :: Text)
+
+getCurrentYear :: IO Int
+getCurrentYear = do
+    now <- getCurrentTime
+    let today = utctDay now
+    let (year, _, _) = toGregorian today
+    return $ fromInteger year
+
+

inPast is a simple function that will return an Either result. However, it uses a Handler + monad. We use liftIO getCurrentYear to get the current year and then compare it + against the user-supplied year. Also, notice how we can easily chain together multiple + validators.

+ +

More sophiticated fields

+

Our color entry field is nice, but it's not exactly user-friendly. What we really want is a + dropdown list.

+
Drop-down lists
+ +
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectField colors) "Color" (carColor <$> mcar)
+  where
+    colors = [("Red", Red), ("Blue", Blue), ("Gray", Gray), ("Black", Black)]
+
+
+

selectField takes a list of pairs. The first item in the pair is the text displayed to the user + in the drop-down list, and the second item is the actual Haskell value. Of course, the code above + looks really repetitive; we can get the same result using the Enum and Bounded instance GHC + automatically derives for us.

+
Uses Enum and Bounded
+ +
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectField colors) "Color" (carColor <$> mcar)
+  where
+    colors = map (pack . show &&& id) $ [minBound..maxBound]
+
+
+

[minBound..maxBound] gives us a list of all the different Color values. We then apply a map and + &&& to turn that into a list of pairs.

+

Of course, some people prefer radio buttons to drop-down lists. Fortunately, this is just a + one-word change.

+
Radio buttons
+ +
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (radioField colors) "Color" (carColor <$> mcar)
+  where
+    colors = map (pack . show &&& id) $ [minBound..maxBound]
+
+
+

Running forms

+

At some point, we're going to need to take our beautiful forms and produce some results. There + are a number of different functions available for this, each with its own purpose. I'll go + through them, starting with the most common.

+
runFormPost
+
This will run your form against any submitted POST parameter. If this is not a POST + submission, it will return a FormMissing. This automatically inserts a nonce as a hidden form + field to avoid CSRF attacks.
+ + +
runFormGet
+
Same as runFormPost, for GET submissions. In order to distinguish a normal GET page load + from a GET submission, it includes an extra _hasdata hidden field in the form.
+ + +
runFormPostNoNonce
+
Same as runFormPost, but does not include (or require) the CSRF nonce.
+ + +
generateFormPost
+
Instead of binding to existing POST parameters, acts as if there are none. This can be + useful when you want to generate a new form after a previous form was submitted, such as in a + wizard.
+ + +
generateFormGet
+
Same as generateFormPost, but for GET.
+ +
+

The return type from the first three is ((FormResult a, Widget), Enctype). The + Widget will already have any valdiation errors and previously submitted values.

+

i18n

+

There have been a few references to i18n in this chapter. The topic will get more thorough + coverage in its own chapter, but since it has such a profound effect on yesod-form, I wanted to + give a brief overview. The idea behind i18n in Yesod is to have data types represent messages. + Each site can have an instance of RenderMessage for a given datatype which will translate that + message based on a list of languages the user accepts. As a result of all this, there are a few + things you should be aware of:

+
  • There is an automatic instance of RenderMessage for Text in every site, so you can just use + plain strings if you don't care about i18n support. However, you may need to use explicit type + signatures occasionally.
  • +
  • yesod-form expresses all of its messages in terms of the FormMessage datatype. Therefore, to + use yesod-form, you'll need to have an appropriate RenderMessage instance. A simple one that + uses the default English translations would + be:
    instance RenderMessage MyApp FormMessage where
    +    renderMessage _ _ = defaultFormMessage
    +
    This + is provided automatically by the scaffolded site.
  • +
  • In order to allow multiple different message types to co-exist, we use an existential type + called SomeMessage. You will occasionally need to wrap your values inside of it, though this is + not common in normal library use.
  • +
+

TODO

+

This chapter still needs a bit more work. In particular, it should cover:

+
  • Monadic forms
  • +
  • Input forms
  • +
  • Creating new fields
  • +
+

If you can think of anything else it's missing, please leave a comment.

+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/pdf-book.html b/public/blog/2011/08/pdf-book.html new file mode 100644 index 00000000..0ad1cd35 --- /dev/null +++ b/public/blog/2011/08/pdf-book.html @@ -0,0 +1,17 @@ + PDF version of the Yesod book +

PDF version of the Yesod book

August 1, 2011

GravatarBy Michael Snoyman

The PDF is available for immediate download. It has an ugly styling, hasn't been checked much, and probably has a bunch of other problems, but it's there.

+

For those interested in the mechanics behind this: the book is written in an XML format called DITA. This site stores the book as DITA as well. I just added a feature to export the book contents into a ZIP file, which allows me to use existing DITA solutions for PDF creation.

+

Next step: write some nice Haskell-based DITA tools and try to get some automatic PDF generation in place.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/perils-partially-powered-languages.html b/public/blog/2011/08/perils-partially-powered-languages.html new file mode 100644 index 00000000..a79c7203 --- /dev/null +++ b/public/blog/2011/08/perils-partially-powered-languages.html @@ -0,0 +1,249 @@ + The Perils of Partially Powered Languages +

The Perils of Partially Powered Languages

August 21, 2011

GravatarBy Michael Snoyman

Let's make an ePub

+

I was recently tasked with setting up a system for one of our clients that would take their + internal XML document format and produce an ePub. This isn't a particularly difficult task, and + is something I've done before. The one restriction I was given was that the solution had to be + implemented in Java.

+

Anyone who reads this blog at all should realize I'm a Haskell programmer, and as such, Java is + not my language of choice. Nonetheless, I'm used to clients requiring work be done with it, on + the chance that they ever want to look at the code. (To my knowledge, this has never actually + happened.) This time around, I'm very happy I did it in Java, as it gave me a perfect real-life + situation for expressing a point I've wanted to make for a while.

+

What is Java?

+

Before getting into the meat of this, we need to analyze the client requirement a bit more: + what is Java, and why do they want me to use it? There are really two distinct things that get + the name Java: the programmer language, and the virtual machine. The requirement to use Java then + stems from two completely different goals:

+
  • Requiring Java-the-language has to do with the idea that "we know Java, we'll understand Java + code."
  • +
  • Requiring the JVM is usually about ease of deployment, increased security, etc.
  • +
+

In this case, it was made clear that the goals were the first one: they wanted any one of their + ten Java programmers to be able to understand the code I was writing. So in a situation like + this, you would assume that every line of code needs to be written in Java itself, for maximum + comprehension. (They actually mentioned a possible interest in using Clojure, but that would have + been a stretch for them.)

+

However, as you'll see in a bit, most of the code I wrote was not Java, and they were + perfectly happy with this.

+

Meet the Partially Powered Languages

+

So if not Java, then what? Well, there were actually three additional languages used in the + development of the project, with one of them being replaced halfway through:

+
  • XSLT, used for transforming XML.
  • +
  • XProc, an XML pipeline language. More on this later.
  • +
  • Ant, used to orchestrate all this.
  • +
+

This list may seem a bit arbitrary, but it's not. One of the major tools used in the document + services industry is the DITA-OT, and it is a polyglot of Java, XSLT and Ant. There is a lot of + interest in migrating to XProc for both simplification and performance improvements, and the + client specifically mentioned an interest in XProc since they are using it elsewhere.

+

Why do I call these partially powered languages? Quite simply, each language is good for its + specific task, but can't (easily) do anything else. Take XProc: it's a language designed to piece + together a number of XML-transforming operations. The approach often used today by tools like the + DITA-OT is to use Ant to call out to XSLT for each pipeline step, storing the intermediate + results in a temp folder. As you can imagine, all of that file I/O, parsing and rendering carries + a performance penalty.

+

So I thought that XProc would be perfect here... until I found out there was no direct way to + create a ZIP file from it. (ePubs are just glorified ZIP files.) This isn't a bug in XProc, or + something for which the library has not yet been written, but simply a fact of the language: its + goals do not include producing ZIP files. And that's precisely the problem: it's not very common + that someones needs fit perfectly into the use case of the tool in question.

+

Here's another example from this project. XSLT turned out to be fairly straight-forward for + most of the implementation. (Besides the fact that there are a whole bunch of issues with + relative file paths waiting to rear their ugly heads... but for now, the XML is simple, so we + took the easy route.) And using XSLT 2.0 allowed us to write this thing to generate multiple + output files from a single input file, meaning a single XSLT call to produce all the output, + avoiding all those temporary XML files I mentioned earlier. Hurray!

+

And all was well... until I got to the end. One of the XML files in an ePub (content.opf) + requires a list of all the files contained in the ZIP file. No problem, I can generate a list of + all the HTML files I generated. But I don't have a list of all the static files, like CSS and + images, that were used. In theory, I could glean all the images referenced from the XML directly, + but I don't know about any images referenced from the CSS. And XSLT can't parse CSS, so that's + not an option.

+

There's actually a very simple solution: just get a list of all the files in the static folder. + Heck, I can use that for the HTML too if I want to be lazy! All I need to do is use that XSLT + directory listing function... that function that doesn't exist. You see, getting a list of files + in a folder is not in the purview of XSLT. It knows how to read, manipulate, and generate XML. So + how did I solve this? Well, the standard way we do things in the DITA-OT:

+
  1. Write some Java code that lists files in folder and outputs them as an XML file.
  2. +
  3. Call that code before calling XSLT.
  4. +
  5. Pass in a referenced to that generated file when calling the XSLT. Let XSLT do its normal + stuff at that point.
  6. +
+

As for Ant... well, anyone familiar with it will know its powers. It can easily do stuff like + copy files, compile Java, and call XSLT. And it can easily be extended with new features, by + defining new Ant tasks- in Java. Which just proves the point: it can do some stuff itself, but + not everything.

+

So what's the problem?

+

Just to review, let's see how our little ePub generator works:

+
  1. We use Ant to orchestrate everything. It's what the user calls (perhaps ironically via a Bash + script), it accepts parameters, and calls out to everything else.
  2. +
  3. There's one piece of Java code to generate an XML file containing the list of files.
  4. +
  5. XSLT takes the input XML files and produces the XHTML and XML files required by the + ePub.
  6. +
  7. Ant does some file copying as necessary, then ZIPs everything up into an ePub.
  8. +
+

Doesn't seem too bad, right? Well, remember that issue with relative paths I mentioned? It's + something I've solved before on different projects. It's nothing too complicated, you just need + to (1) parse file paths properly and (2) make sure you have a consistent mapping from source + files to output files. This was really easy to solve in Haskell, and even in Java: you just use a + datastructure like a map or hashtable and some basic string parsing.

+

And here's the problem: both of those are difficult to impossible in XSLT. XSLT isn't designed + for maps. It certainly doesn't have the ability to get the canonical filename for a relative path + and use it as a key in a dictionary. While that's just 1 or 2 lines in Java, Haskell, and + probably a dozen other languages, it's not the case in XSLT. (By the way, this is an existing, + long-standing bug in the DITA-OT already. The solution there is "don't set up your source XML + that way.")

+

There's also the fact that this whole call-out-to-Java approach for the file listing is + ridiculous. It's inefficient for certain, but more importantly, it's counter-intuitive. And it's + something that can be easily broken in maintenance.

+

But then there's the really big issue: the whole reason to use Java was so that all of their + Java programmers would understand the code. And we haven't achieved that goal at all. The + Java code was the most innocuous part of the project. Now in order to understand what's going on, + they need to know Java, Ant and XSLT.

+ +

There's one more issue that I've seen, that hasn't affected us here yet, but does affect other + big system designed in this way: modularity. Ant and XSLT are not designed for modularity. They + aren't general purpose languages, and they were designed with specific goals. You can see this + limitation greatly in the DITA-OT: in needed to create an entire plugin system which generates + Ant files from templates just to accomplish something simple like passing parameters. (It's a bit + more complicated than that, I don't want to go into the gory details here.) And the worst part is + that it doesn't even solve the problem well: you can't have two sets of modifications to HTML + output in the DITA-OT without essentially duplicating all of the template files.

+

Why did you let this happen?

+

It seems like the fault in all this rests squarely on my shoulders: I knew that Ant and XSLT + had shortcomings for this project, and I used them anyway. I should have just manned up and used + Java for the whole thing. Everything would have been in a single language that the client can + understand, and adding future features like better file reference support would have been an + evolutionary change.

+

The problem is that there's a budget to consider. I've tried once before to replace our normal + XSLT usage with pure Java, and the result was horrible: it took me significantly longer to write + than just doing the same thing in XSLT. And there are two reasons for this: Java-the-language, + and Java-the-libraries. I don't want to speak about this in the abstract, so let's have a + real-world comparison to a Haskell library: xml-enumerator. I'm sure many + other languages could make a similar comparison with their tools, but this is the one I'm most + familiar with.

+

Let's start with a simple question: what's an XML document? In xml-enumerator, that's very + simple: a datatype called Document that contains some stuff + (processing instructions and comments) before and after the root element, a doctype statement, + and the root element itself. What's an element? Why it's the tag name, a list of attributes and a + list of children nodes. And a node is either an element, some text, a processing + instruction, or a comment. It's all right there in the data type definitions, which I think are + even understable by someone without any Haskell experience.

+

Contrast this with the DOM model from Java. We have a Node interface, which has child interfaces for elements, processing + instructions, text and comments. So far, pretty similar to Haskell. Oh, and it has subinterfaces + for documents, attributes, and notations. (Note: notations don't actually exist in the XML.) Now, + this Node interface declares a whole bunch of functions, such as getNodeName. So you can guess what that does for an + element, but what does it do for a comment? Well, it seems that it returns the contents of the + comment... right.

+

Here's some more fun: try understanding how that API deals with XML namespaces. In + xml-enumerator, we have a special datatype called Name, that contains the local name, namespace + and prefix for an identifier. This models the XML specification for namespaces precisely. And for + convenience, you can even just write them as normal strings in your Haskell source code, so most + of the time you don't even need to think about namespaces. In Java? There are two versions for + most functions, one that deals with namespaces, and one that doesn't.

+

Let's compare traversing the DOM. In Haskell, I can easily add an attribute to every element in + a tree like + so:

addAttr :: Name -> Text -> Node -> Node
+addAttr name value (NodeElement (Element tag attrs children)) =
+    NodeElement (Element tag ((name, value) : attrs) (map (addAttr name value) children))
+addAttr _ _ node = node
Here, + we get to leverage pattern matching extensively, as well as use persistent data structures + easily. The equivalent in Java + is:
public static void addAttr(String name, String value, Node node) {
+    if (node.getNodeType() != Node.ELEMENT_NODE) return;
+    Element element = (Element) node;
+    element.setAttribute(name, value);
+
+    NodeList nl = element.getChildNodes();
+    for (int i = 0; i < nl.getLength(); ++i) {
+        addAttr(name, value, nl.item(i));
+    }
+}
Note + that we have to use a type cast, which if not checked correctly could result in a runtime + exception. We're also modifying the original value instead of generating a new one, and we have + no support for namespaces.

+

The magic features that's missing here is sum types. There is no good replacement in Java, and + it precisely the right tool to model nodes in XML. Added to that the requirement to use mutable + data structures here and it's painful. And don't even get me started on the difference between a + null string and an empty string in the API.

+

I've only scratched the surface here, but I'm sure you can extrapolate from here how painful + writing this kind of code is in Java. And I was surprised to find this out: Java is after all + known for its prowess as manipulating XML.

+

Which now explains the situation fully: Java is a painful language for the majority of the code + we need to write. So instead, we use a bunch of languages designed for those specific tasks to + avoid writing that Java code. As a result, we have messy codebases using lots of sub-powered + languages that results in spaghetti code that no one can read.

+

So... we should only use full-fledged languages?

+

You might think that I'm advocating never using a non-general-purpose programming language. + That's not the case. If you look at Yesod, we have a number of those floating around, in + particular Hamlet, an HTML templating language. Let's compare Hamlet to XSLT in the context of + this conversation:

+
  • Hamlet is nothing more than simple, syntactic sugar. You can easily trace everything it does + into the corresponding Haskell code. XSLT, on the other hand, is a very powerful language, for + which it is very difficult to reproduce features in Java.
  • +
  • Hamlet only has meaning within the context of a Haskell program. It reads variables from the + surrounding code and returns a value, which must be used by Haskell. XSLT can exist entirely + outside of a Java program.
  • +
  • Hamlet has absolutely no side effects. XSLT can be used to generate as many output files as + it wants.
  • +
+

You might be surprised at what I'm saying: Hamlet's advantage is that it's less powerful + than XSLT! That's precisely the point. Having add-on languages like Hamlet are great for + simplifying code, removing line noise, and making it easier to maintain the codebase. XSLT, on + the other hand, is used as an alternative to Java.

+

Going back to my file reference example: with a language like Hamlet, there's no problem. We + can call back to Haskell to handle the heavy lifting that Hamlet doesn't support. It's so tightly + bound with the surrounding program that there is virtually no overhead to this. Also, there would + be no problem using the strong data types we know and love within Hamlet; in XSLT, we're forced + to switch back to weak datatypes. (Example: my Java code uses the File type for getting the file + listing; XSLT just sees strings.)

+

If I was to write an ePub generator in Haskell (which I'll likely be doing in the + not-too-distant future), I would use the xml-hamlet package. That would allow + me to easily and concisely produce XML values. In other words, I could + replace:

[ NodeElement $ Element "foo" [("bar", "baz")]
+    [ NodeContent "some content"
+    , NodeElement $ Element "bin" [] []
+    ]
+]
with
[xml|
+<foo bar=baz>
+    some content
+    <bin>
+|]
Of + course, both of those are infinitely more pleasant than the Java + equivalent:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+DocumentBuilder db = dbf.newDocumentBuilder();
+Document doc = db.newDocument();
+Element foo = doc.createElement("foo");
+doc.appendChild(foo);
+foo.setAttribute("bar", "baz");
+foo.appendChild(doc.createTextNode("some content"));
+foo.appendChild(doc.createElement("bin"));
And + just wait till you have to write this to a file. In Haskell, it's the writeFile + function. In Java, I pity you.

+

Conclusion

+
  • Java-the-language and Java-the-libraries make a number of simple tasks very complicated.
  • +
  • Therefore, we have a number of helper languages, like XSLT and Ant, to avoid the pain of a + pure Java solution.
  • +
  • However, these tools are all purposely missing some features we'll inevitably need.
  • +
  • The result will be either missing proper handling of some features, a polyglot that will miss + the point of using Java in the first place, or most likely both.
  • +
  • There are great use cases for non-general-purpose languages, but they should have tight + integration with a real language.
  • +
  • Haskell's flexible syntax and great typing make it very powerful here.
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/persistent-0-6-0.html b/public/blog/2011/08/persistent-0-6-0.html new file mode 100644 index 00000000..9cf4f283 --- /dev/null +++ b/public/blog/2011/08/persistent-0-6-0.html @@ -0,0 +1,756 @@ + Persistent 0.6.0 +

Persistent 0.6.0

August 26, 2011

GravatarBy Michael Snoyman

Persistent

Forms deal with the boundary between the user and the application. Another boundary we need to deal with is between the application and the storage layer. Whether it be a SQL database, a YAML file, or a binary blob, odds are you have to work to get your storage layer to accept your application datatypes. Persistent is Yesod's answer to data storage- a type-safe universal data store interface for Haskell. +

Haskell has many different database bindings available. However, most of these have little knowledge of a schema and therefore do not provide useful static guarantees and force database-dependent interfaces and data structures on the programmer. Haskellers have attempted a more revolutionary route of creating Haskell specific data stores to get around these flaws that allow one to easily store any Haskell type. These options are great for certain use cases, but they constrain one to the storage techniques provided by the library, do not interface well with other languages, and the flexibility can also mean one must write reams of code for querying data. In contrast, Persistent allows us to choose among existing databases that are highly tuned for different data storage use cases, interoperate with other programming languages, and to use a safe and productive query interface. +

Persistent follows the guiding principles of type safety and concise, declarative syntax. Some other nice features are:

  • Database-agnostic. There is first class support for PostgreSQL, SQLite and MongoDB.
  • By being non-relational in nature, we simultaneously are able to support a wider number of storage layers and are not constrained by some of the performance bottlenecks incurred through joins.
  • A major source of frustration in dealing with SQL databases is changes to the schema. Persistent can automatically perform database migrations.
+ + +

Synopsis

{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}
+{-# LANGUAGE GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Control.Monad.IO.Class (liftIO)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+BlogPost
+    title String
+    authorId PersonId
+|]
+
+main :: IO ()
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+    janeId <- insert $ Person "Jane Doe" Nothing
+
+    insert $ BlogPost "My fr1st p0st" johnId
+    insert $ BlogPost "One more for good measure" johnId
+
+    oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+    liftIO $ print (oneJohnPost :: [(BlogPostId, BlogPost)])
+
+    john <- get johnId
+    liftIO $ print (john :: Maybe Person)
+
+    delete janeId
+    deleteWhere [BlogPostAuthorId ==. johnId]
+
+

Solving the boundary issue

Let's say you are storing information on people in a SQL database. Your table might look something like:

CREATE TABLE Person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)

And if you are using a database like PostgreSQL, you can be guaranteed that the database will never store some arbitrary text in your age field. (The same cannot be said of SQLite, but let's forget about that for now.) To mirror this database table, you would likely create a Haskell datatype that looks something like:

data Person = Person
+    { personName :: String
+    , personAge :: Int
+    }
+

It looks like everything is type safe: the database schema matches our Haskell datatypes, the database ensures that invalid data can never make it into our data store, and everything is generally awesome. Well, until:

  • You want to pull data from the database, and the database layer gives you the data in an untyped format.
  • You want to find everyone older than 32, and you accidently write "thirtytwo" in your SQL statement. Guess what: that will compile just fine, and you won't find out you have a problem until runtime.
  • You decide you want to do something as simple as find the first 10 people alphabetically. No problem... until you make a typo in your SQL. Once again, you don't find out until runtime.

In dynamic languages, the answers to these issues is unit testing. For everything that can go wrong, make sure you write a test case. But as I am sure you are aware by now, that doesn't jive well with the Yesod approach to things. We like to take advantage of Haskell's strong typing to save us wherever possible, and data storage is no exception.

So the question remains: how can we use Haskell's type system to save the day?

Types

Like routing, there is nothing intrinsically difficult about type-safe data access. It + just requires a lot of monotonous, error prone, boiler plate code. As usual, this means + we can use the type system to keep us honest. And to avoid some of the drudgery, we'll + use a sprinkling of Template Haskell.

+ +

PersistValue is the basic building block of Persistent. It is a very simple datatype that can represent data that gets sent to and from a database. Its definition is:

data PersistValue = PersistText Text
+                  | PersistByteString ByteString
+                  | PersistInt64 Int64
+                  | PersistDouble Double
+                  | PersistBool Bool
+                  | PersistDay Day
+                  | PersistTimeOfDay TimeOfDay
+                  | PersistUTCTime UTCTime
+                  | PersistNull
+                  | PersistList [PersistValue]
+                  | PersistMap [(T.Text, PersistValue)]
+                  | PersistForeignKey ByteString -- ^ intended especially for MongoDB backend
+

Each Persistent backend needs to know how to translate the relevant values into something the database can understand. However, it would be awkward do have to express all of our data simply in terms of these basic types. The next layer is the PersistField typeclass, which defines how an arbitrary Haskell datatype can be marshaled to and from a PersistValue. A PersistField correlates to a column in a SQL database. In our person example above, name and age would be our PersistFields.

To tie up the user side of the code, our last typeclass is PersistEntity. An instance of PersistEntity correlates with a table in a + SQL database. This typeclass defines a number of functions and some associated types. To + review, we have the following correspondence between Persistent and SQL:

+ + + + + + + + + + + + +
SQLPersistent
Datatypes (VARCHAR, INTEGER, etc)PersistValue
TablePersistEntity
ColumnPersistField

Code Generation

In order to ensure that the PersistEntity instances match up properly with your Haskell datatypes, Persistent takes responsibility for both. This is also good from a DRY (Don't Repeat Yourslef) perspective: you only need to define your entities once. Let's see a quick example:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+
+mkPersist sqlSettings [persist|
+Person
+    name String
+    age Int
+|]
+

We use a combination of Template Haskell and Quasi-Quotation (like when defining + routes): persistent-template:Database.Persist.TH:persist is a + quasi-quoter which converts a whitespace-sensitive syntax into a list of entity + definitions. (You can also declare your entities in a separate file using + persistent-template:Database.Persist.TH:persistFile.) + persistent-template:Database.Persist.TH:mkPersist takes that list + of entities and declares:

  • One Haskell datatype for each entity.
  • A PersistEntity instance for each datatype defined.

Of course, the interesting part is how to use this datatype once it is defined.

main = withSqliteConn ":memory:" $ runSqlConn $ do
+    michaelId <- insert $ Person "Michael" 26
+    michael <- get michaelId
+    liftIO $ print michael
+

We start off with some standard database connection code. In this case, we used the single-connection functions. Persistent also comes built in with connection pool functions, which we will generally want to use in production.

In this example, we have seen two functions: insert creates a new record in the database and returns its ID. Like everything else in Persistent, IDs are type safe. We'll get into more details of how these IDs work later. So when you call insert $ Person "Michael" 25, it gives you a value back of type PersonId.

+

The next function we see is get, which attempts to load a value from the database using an Id. In Persistent, you never need to worry that you are using the key from the wrong table: trying to load up a different entity (like House) using a PersonId will never compile.

PersistBackend

One last detail is left unexplained from the previous example: what are those withSqliteConn and runSqlConn functions doing, and what is that monad that our database actions are running in?

All database actions need to occur within an instance of + PersistBackend. As its name implies, every backend (PostgreSQL, SQLite, + MongoDB) has an instance of PersistBackend. This is where all the translations from + PersistValue to database-specific values occur, where SQL query + generation happens, and so on.

withSqliteConn creates a single connection to a database using its supplied connection string. For our test cases, we will use ":memory:", which simply uses an in-memory database. runSqlConn uses that connection to run the inner action, in this case, SqlPersist. Both SQLite and PostgreSQL share the same instance of PersistBackend.

One important thing to note is that everything which occurs inside a single call to runSqlConn runs in a single transaction. This has two important implications:

  • For many databases, committing a transaction can be a costly activity. By putting multiple steps into a single transaction, you can speed up code dramatically.
  • If an exception is thrown anywhere inside a single call to runSqlConn, all actions will be rolled back.

Migrations

I'm sorry to tell you, but so far I have lied to you a bit: the example from the previous section does not actually work. If you try to run it, you will get an error message about a missing table.

For SQL databases, one of the major pains can be managing schema changes. Instead of leaving this to the user, Persistent steps in to help, but you have to ask it to help. Let's see what this looks like:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+mkPersist sqlSettings [persist|
+Person
+    name String
+    age Int
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration $ migrate (undefined :: Person) -- this line added: that's it!
+    michaelId <- insert $ Person "Michael" 26
+    michael <- get michaelId
+    liftIO $ print michael
+

With this one little code change, Persistent will automatically create your Person table for you. This split between runMigration and migrate allows you to migrate multiple tables simultaneously.

This works when dealing with just a few entities, but can quickly get tiresome once we are dealing with a dozen entities. Instead of repeating yourself, Persistent provides a helper function:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int
+Car
+    color String
+    make String
+    model String
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+

mkMigrate is a Template Haskell function which creates a new function that will automatically call migrate on all entities defined in the persist block. The share function is just a little helper that passes the information from the persist block to each Template Haskell function and concatenates the results.

Persistent has very conservative rules about what it will do during a migration. It starts by loading up table information from the database, complete with all defined SQL datatypes. It then compares that against the entity definition given in the code. For simple cases, it will automatically alter the schema:

  • The datatype of a field changed. However, the database may object to this modification if the data cannot be translated.
  • A field was added. However, if the field is not null, no default value is supplied (we'll discuss defaults later) and there is already data in the database, the database will not allow this to happen.
  • A field is converted from not null to null. In the opposite case, Persistent will attempt the conversion, contingent upon the database's approval.
  • A brand new entity is added.

However, there are a number of cases that Persistent will not handle:

  • Field or entity renames: Persistent has no way of knowing that "name" has now been renamed to "fullName": all it sees is an old field called name and a new field called fullName.
  • Field removals: since this can result in data loss, Persistent by default will refuse to perform the action (you can force the issue by using runMigrationUnsafe instead of runMigration, though it is not recommended).

runMigration will print out the migrations it is running on stderr (you can bypass this by using runMigrationSilent). Whenever possible, it uses ALTER TABLE calls. However, in SQLite, ALTER TABLE has very limited abilities, and therefore Persistent must resort to copying the data from one table to another.

Finally, if instead of performing a migration, you just want Persistent to give you hints about what migrations are necessary, use the printMigration function. This function will print out the migrations which runMigration would perform for you. This may be useful for performing migrations that Persistent is not capable of, for adding arbitrary SQL to a migration, or just to log what migrations occurred.

Uniqueness

In addition to declaring fields within an entity, you can also declare uniqueness + constraints. A typical example would be requiring that a username be unique.

+
Unique Username
+ +
User
+    username Text
+    UniqueUsername username
+
+

While each field name must begin with a lowercase letter, the uniqueness constraints must + begin with an uppercase letter.

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+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"] [persist|
+Person
+    firstName String
+    lastName String
+    age Int
+    PersonName firstName lastName
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+    insert $ Person "Michael" "Snoyman" 26
+    michael <- getBy $ PersonName "Michael" "Snoyman"
+    liftIO $ print michael
+

To declare a unique combination of fields, we add an extra line to our declaration. Persistent knows that it is defining a unique constructor, since the line begins with a capital letter. Each following word must be a field in this entity.

The main restriction on uniqueness is that it can only be applied non-null fields. The reason for this is that the SQL standard is ambiguous on how uniqueness should be applied to NULL (eg, is NULL=NULL true or false?). Besides that ambiguity, most SQL engines in fact implement rules which would be contrary to what the Haskell datatypes anticipate (eg, PostgreSQL says that NULL=NULL is false, whereas Haskell says Nothing == Nothing is True).

+

In addition to providing nice guarantees at the database level about consistency of your + data, uniqueness constraints can also be used to perfect some specific queries within + your Haskell code, like the getBy demonstrated above. This happens via the Unique + associated type. In the example above, we end up with a new constructor:

+
PersonName :: String -> String -> Unique Person
+

Queries

Fetching by ID

+

The simplest query you can perform in Persistent is getting based on an ID. Since this value + may or may not exist, its return type is wrapped in a Maybe.

+
Using get
+ +
personId <- insert $ Person "Michael" "Snoyman" 26
+    maybePerson <- get personId
+    case maybePerson of
+        Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+        Just person -> liftIO $ print person
+
+
+

This can be very useful for sites that provide URLs like /person/5. + However, in such a case, we don't usually care about the Maybe wrapper, and just want the value, + returning a 404 message if it is not found. Fortunately, the + get404 function helps us out here. We'll go into + more details when we see integration with Yesod.

+

Fetching by unique constraint

+

getBy is almost identical to get, except it takes a uniqueness constraint instead of an ID it + takes a Unique value.

+
Using getBy
+ +
personId <- insert $ Person "Michael" "Snoyman" 26
+    maybePerson <- getBy $ UniqueName "Michael" "Snoyman"
+    case maybePerson of
+        Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+        Just person -> liftIO $ print person
+
+
+

Select functions

+

But likely, you're going to want more powerful queries. You'll want to find everyone over a + certain age; all cars available in blue; all users without a registered email address. For this, + you need one of the select functions.

+

All the select functions use a similar interface, with slightly different outputs:

+ + + + + + + + + + + + + + + +
FunctionReturns
selectEnumAn Enumerator containing all the IDs and values from the database.
selectListA list containing all the IDs and values from the database.
selectFirstTakes just the first ID and value from the database, if available
selectKeysReturns only the keys, without the values, as an Enumerator
+

selectList is the most commonly used, so we will cover it specifically. But understanding the + others should be trivial after that.

+

selectList takes two arguments: a list of Filters, and a list of SelectOpts. The former is what + limits your results based on characteristics; it allows for equals, less than, is member of, and + such. SelectOpts provides for three different features: sorting, limiting output to a certain + number of rows, and offsetting results by a certain number of rows.

+ +

Let's jump straight into an example of filtering, and then analyze it.

+
people <- selectList [PersonAge >. 25, PersonAge <=. 30] []
+    liftIO $ print people
+
+

As simple as that example is, we really need to cover three points:

+
  1. PersonAge is a constructor for an associated phantom type. That might sound scary, but what's + important is that it uniquely identifies the "age" column of the "person" table, and that it + knows that the age field is an Int. (That's the phantom part.)
  2. +
  3. We have a bunch of Persistent filtering operators. They're all pretty straight-forward: just + tack a period to the end of what you'd expect. There are three gotchas here, I'll explain + below.
  4. +
  5. The list of filters is ANDed together, so that our constraint means "age is greater than 25 + AND age is less than or equal to 30". We'll describe ORing later.
  6. +
+

The one operator that's surprisingly named is the not equals one. We use !=., since /=. is used + for updates (described later). Don't worry: if you use the wrong one, the compiler will catch + you. The other two surprising operators are the "is member" and "is not member". They are, + respectively, <-. and /<-. (both end with a period).

+

And regarding ORs, we use the ||. operator. Let's see an example:

+
people <- selectList
+        (       [PersonAge >. 25, PersonAge <=. 30]
+            ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
+            ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
+        )
+        []
+    liftIO $ print people
+
+

This (completely nonsensical) example means: find people who are 26-30, inclusive, OR whose + names are neither Adam or Bonny, OR whose age is either 50 or 60.

+

SelectOpt

+

All of our selectList calls have included an empty list as the second parameter. That specifies + no options, meaning: sort however the database wants, return all results, and don't skip any + results. A SelectOpt has four constructors that can be used to change all that.

+
Asc
+
Sort by the given column in ascending order. This uses the same phantom type as filtering, + such as PersonAge.
+ + +
Desc
+
Same as Asc, in descending order.
+ + +
LimitTo
+
Takes an Int argument. Only return up to the specified number of results.
+ + +
OffsetBy
+
Takes an Int argument. Skip the specified number of results.
+ +
+

The following code defines a function that will break down results into pages. It returns all + people aged 18 and over, and then sorts them by age (oldest person first). For people with the + same age, they are sorted alphabetically by last name, then first name.

+
resultsForPage pageNumber = do
+    let resultsPerPage = 10
+    selectList
+        [ PersonAge >=. 18
+        ]
+        [ Desc PersonAge
+        , Asc PersonLastName
+        , Asc PersonFirstName
+        , LimitTo resultsPerPage
+        , OffsetBy $ (pageNumber - 1) * resultsPerPage
+        ]
+
+

Manipulation

Insert

It's all well and good to be able to play with data in the database, but how does it get there + in the first place? The answer is the insert function. Its usage is incredibly simple, as you've + seen already. You just give it a value, and it gives back an ID.

+

At this point, it makes sense to explain a it of the philosophy behind Persistent. In many + other ORM solutions, the datatypes used to hold data is opaque: you need to go through their + defined interfaces to get at and modify the data. That's not the case with Persistent: we're + using plain old Algebraic Data Types for the whole thing. This means you still get all the great + benefits of pattern matching, currying and everything else you're used to.

+

However, there are a few things we can't do. For one, there's no way to automatically + update values in the database every time the record is updated in Haskell. Of course, with + Haskell's normal stance of purity and immutability, this wouldn't make much sense anyway, so I + don't shed any tears over it.

+

However, there is one issue that newcomers are often bothered by: why are IDs and values + completely separate? It seems like it would be very logical to embed the ID inside the value. In + other words, instead of + having:

data Person = Person { name :: String }
+
have
data Person = Person { personId :: PersonId, name :: String }
+

+

Well, there's one problem with this right off the bat: how do we do an insert? If a Person + needs to have an ID, and we get the ID by inserting, and an insert needs a Person, we have an + impossible loop. Now, we could solve this with undefined, but that's just asking for trouble.

+

OK, you say, let's try something a bit + safer:

data Person = Person { personId :: Maybe PersonId, name :: String }
+
I + definitely prefer insert $ Person Nothing "Michael" to insert $ Person + undefined "Michael". And now our types will be much simpler, right? For example, + selectList could return a simple [Person] instead of that ugly + [(PersonId, Person)].

+

The problem is that the "ugliness" is incredibly useful. Having (PersonId, + Person) makes it obvious, at the type level, that we're dealing with a value that + exists in the database. Let's say we want to create a link to another page that requires the + PersonId (not an uncommon occurrence as we'll discuss later). The (PersonId, + Person) form gives us unambiguous access to that information; embedding PersonId within Person + with a Maybe wrapper means an extra runtime check for Just, instead of a more error-proof compile + time check.

+

Finally, there's a semantic mismatch with embedding the ID within the value. The Person is the + value. Two people are identical (in the context of a database) if all their fields are the same. + By embedding the ID in the value, we're no longer talking about a person, but about a row in the + database. Equality is no longer really equality, it's identity: is this the same person. + as opposed to equivalent person.

+

In other words, there are some annoyances with having the ID separated out, but overall, it's + the right approach, which in the grand scheme of things leads to better, less buggy + code.

+

Update

Now, in the context of that discussion, let's think about updating. The simplest way to update + is:

let michael = Person "Michael" 26
+    michaelAfterBirthday = michael { personAge = 27 }
+
But + that's not actually updating anything, it's just creating a new person value based on the old + one. When we say update, we're not talking about modifications to the values in Haskell. + (We better not be of course, since Haskell data types are immutable.)

+

Instead, we're looking at ways of modifying rows in a table. And the simplest way to do that is + with the update function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+    update personId [PersonAge =. 27]
+
+

update simply takes two arguments: an ID, and a list of Updates. The simplest update is + assignment, but it's not always the best. What if you want to increase someones age by 1, but you + don't have their current age? Persistent has you covered:

+
haveBirthday personId = update personId [PersonAge +=. 1]
+
+

And as you might expect, we have all the basic mathematical operators: +=., -=., *=., and /=. + (full stop). These can be convenient for updating a single record, but they are also essential + for proper ACID guarantees. Imagine the alternative: pull out a Person, increment the age, and + update the new value. If you have two threads/processes working on this database at the same + time, you're in for a world of hurt (hint: race conditions).

+

Sometimes you'll want to update many fields at once (give all your employees a 5% pay increase, + for example). updateWhere takes two parameters: a list of filters, and a list of updates to + apply.

+
updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- it's been a long day
+
+

Occasionally, you'll just want to completely replace the value in a database with a different + value. For that, you use (surprise) the replace function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+    replace personId $ Person "John" "Doe" 20
+
+

Delete

+

As much as it pains us, sometimes we must part with our data. To do so, we have three + functions:

+
delete
+
Delete based on an ID
+ + +
deleteBy
+
Delete based on a unique constraint
+ + +
deleteWhere
+
Delete based on a set of filters
+ +
+
personId <- insert $ Person "Michael" "Snoyman" 26
+    delete personId
+    deleteBy $ UniqueName "Michael" "Snoyman"
+    deleteWhere [PersonFirstName ==. "Michael"]
+
+

We can even use deleteWhere to wipe out all the records in a table, we just need to give some + hints to GHC as to what table we're interested in:

+
deleteWhere ([] :: [Filter Person])
+
+

Attributes

So far, we have seen a very simple syntax for our persist blocks: a line for the name of our entities, and then an indented line for each field with two words: the name of the field and the datatype of the field. Persistent handles more than this: you can assign an arbitrary list of attributes after the first two words on a line.

Let's say that we want to have a Person entity with an (optional) age and the timestamp of when he/she was added to the system. For entities already in the database, we want to just use the current date-time for that timestamp.

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+import Control.Monad.IO.Class
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=now()
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    time <- liftIO getCurrentTime
+    runMigration migrateAll
+    insert $ Person "Michael" (Just 26) time
+    insert $ Person "Greg" Nothing time
+

Maybe is a built in, single word attribute. It makes the + field optional. In Haskell, this means it is wrapped in a Maybe. In SQL, it makes the + column nullable.

+

The default attribute is backend specific, and uses whatever + syntax is understood by the database. In this case, it uses the database's built-in + now() function. Let's say we now want to add a field for favorite programming + language:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=now()
+    language String default='Haskell'
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+ +

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 let's say we want to set someone's default home country to the El Salvador:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=now()
+    language String default='Haskell'
+    country String "default='El Salvador'"
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+

One last trick you can do with attributes is to specify the names to be used for the SQL + tables and columns. This can be very convenient when interacting with existing + databases.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person sql=the-person-table
+    firstName String sql=first_name
+    lastName String sql=fldLastName
+    age Int Gt Desc "sql=The Age of the Person"
+    UniqueName firstName lastName
+|]
+

Relations

Persistent allows references between your data types in a manner that is consistent with + supporting non-SQL databases. We do this by embedding an ID in the related entity. So if + a person has many cars:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+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"] [persist|
+Person
+    name String
+Car
+    ownerId PersonId Eq
+    name String
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+    bruce <- insert $ Person "Bruce Wayne"
+    insert $ Car bruce "Bat Mobile"
+    insert $ Car bruce "Porsche"
+    -- this could go on a while
+    cars <- selectList [CarOwnerId ==. bruce] []
+    liftIO $ print cars
+

Using this technique, it's very easy to define one-to-many relationships. To define many-to-many relationships, we need a join entity, which has a one-to-many relationship with each of the original tables. It is also a good idea to use uniqueness constraints on these. For example, to model a situation where we want to track which people have shopped in which stores:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+Store
+    name String
+PersonStore
+    personId PersonId
+    storeId StoreId
+    UniquePersonStore personId storeId
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+    bruce <- insert $ Person "Bruce Wayne"
+    michael <- insert $ Person "Michael"
+
+    target <- insert $ Store "Target"
+    gucci <- insert $ Store "Gucci"
+    sevenEleven <- insert $ Store "7-11"
+
+    insert $ PersonStore bruce gucci
+    insert $ PersonStore bruce sevenEleven
+
+    insert $ PersonStore michael target
+    insert $ PersonStore michael sevenEleven
+

Closer look at types

+

So far, we've spoken about Person and PersonId without really explaining what they are. In the + simplest sense, for a SQL-only system, the PersonId could just be type PersonId = + Int64. However, that means there is nothing binding a PersonId at the type level to the + Person entity. Instead, it would be very easy to accidently use a PersonId and get a Car. In + order to model this relationship, we use phantom types. So, our next naive step would be:

+
newtype Key entity = Key Int64
+type PersonId = Key Person
+
+ +

And that works out really well, until you get to a backend that doesn't use Int64 for its IDs. + And that's not just a theoretical question; MongoDB uses ByteStrings instead. So what we need is + a key value that can contain an Int and a ByteString. Seems like a great time for a sum type:

+
data Key entity = KeyInt Int64 | KeyByteString ByteString
+
+

But that's just asking for trouble. Next we'll have a backend that uses timestamps, so we'll + need to add something to Key. This could go on for a while. Fortunately, we already have a sum + type intended for representing arbitrary data: PersistValue. So now, we can use:

+
newtype Key entity = Key PersistValue
+
+

But this has another problem. Let's say we have a web application that takes an ID as a + parameter from the user. It will need to receive that parameter as Text and then try to convert + it to a Key. Well, that's simple: write a function to convert a Text to a PersistValue, and then + wrap the result in the Key constructor, right?

+

Wrong. We tried this, and there's a big problem with it. We end up getting Keys that could + never be. For example, if we're dealing with SQL, a key must be an integer. But the approach + described above would allow arbitrary textual data in. The result was a bunch of 500 server + errors as the database choked on comparing an integer column to text.

+

So what we need is a way to convert text to a Key, but have it dependent on the rules of the + backend in question. And once phrased that way, the answer is simple: just add another phantom. + The real, actual definition of Key in Persistent is:

+
newtype Key backend entity = Key { unKey :: PersistValue }
+
+

This works great: we can have a Text -> Key MongoDB entity function and a + Text -> Key SqlPersist entity function, and everything runs smoothly. But now + we have a new problem: relations. Let's say we want to represent blogs and blog posts. We would + use the entity definition:

+
Blog
+    title Text
+Post
+    title Text
+    blogId BlogId
+

But what would that look like in terms of our Key datatype?

+
data Blog = Blog { blogTitle :: Text }
+data Post = Post { postTitle :: Text, postBlogId :: Key <what goes here?> Blog }
+
+

We need something to fill in as the backend. In theory, we could hardcode this to SqlPersist, + or Mongo, but then our datatypes will only work for a single backend. For an individual + application, that might be acceptable, but what about libraries defining datatypes to be used by + multiple applications, using multiple backends?

+

So thinks got a little more complicated. Our types are actually:

+
data BlogGeneric backend = Blog { blogTitle :: Text }
+data PostGeneric backend = Post { postTitle :: Text, postBlogId :: Key backend (BlogGeneric backend) }
+
+

Notice that we still keep the short names for the constructors and the records. Finally, to + give a simple interface for normal code, we define some type synonyms:

+
type Blog = BlogGeneric SqlPersist
+type BlogId = Key SqlPersist Blog
+type Post = PostGeneric SqlPersist
+type PostId = Key SqlPersist Post
+
+

And no, SqlPersist isn't hard-coded into Persistent anywhere. That sqlSettings parameter you've + been passing to mkPersist is what tells us to use SqlPersist. Mongo code will use mongoSettings + instead.

+

This might be quite complicated under the surface, but user code hardly ever touches this. Look + back through this whole chapter: not once did we need to deal with the Key or Generic stuff + directly. The most common place for it to pop up is in compiler error messages. So it's important + to be aware that this exists, but it shouldn't affect you on a day-to-day basis.

+

Custom Fields

Occasionally, you will want to define a custom field to be used in your datastore. The most common case is an enumeration, such as employment status. For this, Persistent provides a helper Template Haskell function:

{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+data Employment = Employed | Unemployed | Retired
+    deriving (Show, Read, Eq)
+derivePersistField "Employment"
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    employment Employment
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+    insert $ Person "Bruce Wayne" Retired
+    insert $ Person "Peter Parker" Unemployed
+    insert $ Person "Michael" Employed
+

derivePersistField stores the data in the database using a string field, and performs marshaling using the Show and Read instances of the datatype. This may not be as efficient as storing via an integer, but it is much more future proof: even if you add extra constructors in the future, your data will still be valid.

Persistent: Raw SQL

The Persistent package provides a type safe interface to data stores. It tries to be + backend-agnostic, such as not relying on relational features of SQL. My experience has + been you can easily perform 95% of what you need to do with the high-level interface. + (In fact, most of my web apps use the high level interface exclusively.)

+

But occasionally you'll want to use a feature that's specific to a backend. One feature I've + used in the past is full text search. In this case, we'll use the SQL "LIKE" operator, which is + not modeled in Persistent. We'll get all people with the last name "Snoyman" and print the + records out.

+ +
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE GADTs #-}
+import Database.Persist.Sqlite (withSqliteConn)
+import Database.Persist.TH (mkPersist, persist, share, mkMigrate, sqlSettings)
+import Database.Persist.GenericSql (runSqlConn, runMigration, SqlPersist)
+import Database.Persist.GenericSql.Raw (withStmt)
+import Database.Persist.GenericSql.Internal (RowPopper)
+import Data.Text (Text)
+import Database.Persist
+import Control.Monad.IO.Class (liftIO)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name Text
+|]
+
+main :: IO ()
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+    insert $ Person "Michael Snoyman"
+    insert $ Person "Miriam Snoyman"
+    insert $ Person "Eliezer Snoyman"
+    insert $ Person "Gavriella Snoyman"
+    insert $ Person "Greg Weber"
+    insert $ Person "Rick Richardson"
+
+    -- 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'"
+    withStmt sql [] withPopper
+
+-- A popper returns one row at a time. We loop over it until it returns Nothing.
+withPopper :: RowPopper (SqlPersist IO) -> SqlPersist IO ()
+withPopper popper =
+    loop
+  where
+    loop = do
+        mrow <- popper
+        case mrow of
+            Nothing -> return ()
+            Just row -> liftIO (print row) >> loop
+
+

Integration with Yesod

+

So you've been convinced of the power of Persistent. How do you integrate it with your Yesod + application? Well, if you use the scaffolding, most of the work is done for you already. But as + we normally do, we'll build up everything manually here to point out how it works under the + surface.

+

The yesod-persistent package provides the meeting point between Persistent + and Yesod. It provides the YesodPersist typeclass, to standardize access to the database via the + runDB method. Let's see this in action.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE TemplateHaskell, OverloadedStrings, GADTs, MultiParamTypeClasses #-}
+import Yesod
+import Database.Persist.Sqlite
+
+-- Define our entities as usual
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    firstName String
+    lastName String
+    age Int Gt Desc
+|]
+
+-- We keep our connection pool in the foundation. At program initialization, we
+-- create our initial pool, and each time we need to perform an action we check
+-- out a single connection from the pool.
+data PersistTest = PersistTest ConnectionPool
+
+-- We'll create a single route, to access a person. It's a very common
+-- occurrence to use an Id type in routes.
+mkYesod "PersistTest" [parseRoutes|
+/person/#PersonId PersonR GET
+|]
+
+-- Nothing special here
+instance Yesod PersistTest where
+    approot _ = ""
+
+-- Now we need to define a YesodPersist instance, which will keep track of
+-- which backend we're using and how to run an action.
+instance YesodPersist PersistTest where
+    type YesodPersistBackend PersistTest = SqlPersist
+
+    -- This is a bit complicated, but liftIOHandler is necessary for proper
+    -- handling of exceptions.
+    runDB action = liftIOHandler $ do
+        PersistTest pool <- getYesod
+        runSqlPool action pool
+
+-- We'll just return the show value of a person, or a 404 if the Person doesn't
+-- exist.
+getPersonR :: PersonId -> Handler RepPlain
+getPersonR personId = do
+    person <- runDB $ get404 personId
+    return $ RepPlain $ toContent $ show person
+
+openConnectionCount :: Int
+openConnectionCount = 10
+
+main :: IO ()
+main = withSqlitePool "test.db3" openConnectionCount $ \pool -> do
+    runSqlPool (runMigration migrateAll) pool
+    runSqlPool (insert $ Person "Michael" "Snoyman" 26) pool
+    warpDebug 3000 $ PersistTest pool
+
+

There are two important pieces here for general use. runDB is used to run a DB action from + within a Handler. That liftIOHandler is a little scary, but it can be safely ignored. It is + necessary in order to properly catch exceptions and rollback changes. Within the runDB, you can + use any of the functions we've spoken about so far, such as insert and selectList.

+

The other new feature is get404. It works just like get, but instead of returning a Nothing + when a result can't be found, it returns a 404 message page. The getPersonR function is a very + common approach used in real-world Yesod applications: get404 a value and then return a response + based on it.

+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/shakespeare.html b/public/blog/2011/08/shakespeare.html new file mode 100644 index 00000000..5fe4a924 --- /dev/null +++ b/public/blog/2011/08/shakespeare.html @@ -0,0 +1,570 @@ + New Shakespeare Chapter +

New Shakespeare Chapter

August 19, 2011

GravatarBy Michael Snoyman

Status Update

We're coming along very nicely on the Yesod 0.9 release. The goal is to have the release candidate out on Monday. However, at this point, the code in the repo is in active use in a number of different sites, and we aren't running into any issues any more. So all signs indicate that this will be the most solid release of Yesod to date.

+

One thing that's still necessary is updating the book for the changes in the packages. There are four chapters that need to be reworked: templates, widgets, forms and persistent. I present to you the first of these revised chapters; as usual, feedback appreciated.

Shakespearean Templates

+

Yesod uses the Shakespearean family of template languages as its standard approach to HTML, CSS + and Javascript creation. This language family shares some common syntax, as well as overarching + principles:

  • As little interference to the underlying template language as possible, while providing + conveniences where possible.
  • +
  • Compile-time guarantees on well-formed content.
  • +
  • Strict type safety, extending to the prevention of XSS (cross-site scripting) attacks.
  • +
  • Automated checking of valid URLs, whenever possible, through type-safe + URLs.
  • +

+

There's is nothing inherently tying Yesod to these languages, or the other way around: each can + be used independently of the other. This chapter will address these template languages on their + own, while the remainder of the book will use them to enhance Yesod application development.

+

Synopsis

Hamlet (HTML)

+
!!!
+<html>
+    <head>
+        <title>#{pageTitle} - My Site
+        <link rel="stylesheet" href=@{Stylesheet}
+    <body>
+        <h1 .page-title>#{pageTitle}
+        <p>Here is a list of your friends:
+        $if null friends
+            <p>Sorry, I lied, you don't have any friends.
+        $else
+            <ul>
+                $forall friend <- friends
+                    <li>#{friendName friend} (#{show $ friendAge friend} years old)
+        <footer>^{copyright}
+
+

Cassius (CSS)

+
#myid
+    color: #{red}
+    font-size: #{bodyFontSize}
+foo bar baz
+    background-image: url(@{MyBackgroundR})
+

Lucius (CSS)

+
section.blog {
+    padding: 1em;
+    border: 1px solid #000;
+    h1 {
+        color: #{headingColor};
+    }
+}
+

Julius (Javascript)

+
$(function(){
+    $("section.#{sectionClass}").hide();
+    $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
+    ^{addBling}
+});
+
+

Types

Before we jump into syntax, let's take a look at the various types involved. We mentioned in + the introduction that types help protect us from XSS attacks. For example, let's say that we have + an HTML template that should display someone's name; it might look like + this:

<p>Hello, my name is #{name}
+

+

What should happen to name, and what should its datatype be? A naive + approach would be to use a Text value, and insert it verbatim. But that would give us quite a + problem when name="<script src='http://nefarious.com/evil.js'></script>". + What we want is to be able to entity-encode the name, so that "<" becomes "&lt;".

+

An equally naive approach is to simply entity-encode every piece of text that gets + embedded. What happens when you have some preexisting HTML generated from another process? For + example, in this book, all Haskell code snippets are run through a colorizing that wraps up words + in appropriate span tags. If we entity escaped everything, code snippets would + be completely unreadable!

+

Instead, we have an Html datatype. In order to generate an Html value, we have two options for + APIs: the ToHtml typeclass provides a way to convert String and Text values into Html, via its + toHtml function, automatically escaping entities along the way. This would be the approach we'd + want for the name above. For the code snippet example, we would use the preEscaped family of + functions.

+

When you use variable interpolation in Hamlet (the HTML Shakespeare language), it automatically + applies a toHtml call to the value inside. So if you interpolate a String, it will be + entity-escaped. But if you provide an Html value, it will appear unmodified. In the code snippet + example, we might interpolate with something like #{preEscapedText + myHaskellHtml}.

+ +

Similarly, we have Css/ToCss, as well as Javascript/ToJavascript. In those cases, the goal is + not really security, as it is very uncommon to be interpolating user input to CSS and Javascript. + Instead, this just provides some nice compile-time guarantees that we haven't accidently stuck + some HTML in our CSS.

+ +

Type-safe URLs

+

One of the biggest unique features in Yesod is type-safe URLs, and the ability to use them + conveniently is provided directly by Shakespeare. Usage is very similar to variable + interpolation, we just use the at-sign (@) instead. We'll cover the syntax later; first, let's + clarify the intuition.

+

Let's say that we've got an application with two routes: + http://example.com/profile/home is the homepage, and + http://example.com/display/time displays the current time. And let's say we + want to link from the homepage to the time. I can think of three different ways of constructing + the URL:

  1. As a relative link: ../display/time
  2. +
  3. As an absolute link, without a domain: /display/time
  4. +
  5. As an absolute link, with a domain: + http://example.com/display/time
  6. +

+

There are problems with each approach: the first will break if either URL changes. Also, it's + not suitable for all use cases; RSS and Atom feeds, for instance, require absolute URLs. The + second is more resilient to change than the first, but still won't be acceptable for RSS and + Atom. And while the third works fine for all use cases, you'll need to update every single URL in + your application whenever your domain name changes. You think that doesn't happen often? Just + wait till you move from your development to staging and finally production server.

+

But more importantly, there is one huge problem with all approaches: if you change your routes + at all, the compiler won't warn you about the broken links. Not to mention that typos can wreak + havoc as well.

+

The goal of type-safe URLs is to let the compiler check things for us as much as possible. In + order to facilitate this, our first step must be to move away from plain old text, which the + compiler doesn't understand, to some well defined datatypes. For our simple application, let's + model our routes with a simple sum type:

data MyRoute = Home | Time
+

+

Now instead of placing a link like /display/time in our template, we can just use the Time + constructor. But at the end of the day, HTML is made up of text, not data types, so we need some + way to convert these values to text. We call this a URL rendering function, and a + simple one + is:

renderMyRoute :: MyRoute -> Text
+renderMyRoute Home = "http://example.com/profile/home"
+renderMyRoute Time = "http://example.com/display/time"
+

+

URL rendering functions are actually a bit more complicated than this. They need to address + query string parameters, handle records within the constructor, more intelligently handle the + domain name. But in practice, you don't need to worry about this, since Yesod will automatically + create your render functions. The one thing to point out is that the type signature is actually a + little more complicated to handle query + strings:

type Query = [(Text, Text)]
+type Render url :: url -> Query -> Text
+renderMyRoute :: Render MyRoute
+renderMyRoute Home _ = ...
+

+

OK, we have our render function, and we have type-safe URLs embedded in the templates. How does + this fit together exactly? Well, instead of generating an Html (or Css or Javascript) value + directly, Shakespearean templates actually produce a function, that takes this render function + and produces HTML. To see this better, let's have a quick (fake) peek at how Hamlet would work + under the surface. Supposing we had a + template:

<a href=@{Time}>The time
+
this would translate roughly into the + Haskell + code:
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
+

+

Syntax

Hamlet Syntax

Tags

Obviously tags will play an important part of any HTML template language. In Hamlet, we try to + stick very close to existing HTML syntax to make the language more comfortable. However, instead + of using closing tags to denote nesting, we use indentation. So something like this in + HTML:

<body>
+<p>Some paragraph.</p>
+<ul>
+<li>Item 1</li>
+<li>Item 2</li>
+</ul>
+</body>
+
would + be
<body>
+    <p>Some paragraph.
+    <ul>
+        <li>Item 1
+        <li>Item 2
+

+

In general, we find this to be easier to follow than HTML once you get accustomed to it. The + only tricky part comes with dealing with whitespace before and after tags. For example, let's say + you want to create the + HTML

<p>Paragraph <i>italic</i> end.</p>
+
We want to make sure + that there is a whitespace preserved after the word "Paragraph" and before the word "end". To do + so, we use two simple escape + characters:
<p>
+    Paragraph #
+    <i>italic
+    \ end.
+
The + whitespace escape rules are actually very simple:
  1. If the first non-space character in a line is a backslash, the backslash is ignored.
  2. +
  3. If the last character in a line is a hash, it is ignored.
  4. +

+

One other thing. Hamlet does not escape entities within its content. This is done on + purpose to allow existing HTML to be more easily copied in. So the example above could also be + written as:

<p>Paragraph <i>italic</i> end.
+
Notice that the first + tag will be automatically closed by Hamlet, while the inner "i" tag will not. You are free to use + whichever approach you want, there is no penalty for either choice.

+

Interpolation

What we have so far is a nice, simplified HTML, but it doesn't let us interact with our Haskell + code at all. How do we pass in variables? Simple: with + interpolation:

<head>
+    <title>#{title}
+
The hash followed be a pair + of braces denotes variable interpolation. In the case above, the title + variable from the scope in which the template was called will be used. Let me state that again: + Hamlet automatically has access to the variables in scope when it's called. There is no need to + specifically pass variables in.

+

But it gets better still. You can apply functions within an interpolation. You can use string + and numeric literals in an interpolation. You can use qualified modules. Both parentheses and the + dollar sign can be used to group statements together. And at the end, the toHtml function is + applied to the result, meaning any instance of ToHtml can be interpolated.

+
Variable Interpolation
+ +
-- Just ignore the quasiquote stuff for now, and that shamlet thing.
+-- It will be explained later.
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (shamlet)
+import Text.Blaze.Renderer.String (renderHtml)
+import Data.Char (toLower)
+import Data.List (sort)
+
+data Person = Person
+    { name :: String
+    , age  :: Int
+    }
+
+main :: IO ()
+main = putStrLn $ renderHtml [shamlet|
+<p>Hello, my name is #{name person} and I am #{show $ age person}.
+<p>
+    Let's do some funny stuff with my name: #
+    <b>#{sort $ map toLower (name person)}
+<p>Oh, and in 5 years I'll be #{show $ (+) 5 (age person)} years old.
+|]
+  where
+    person = Person "Michael" 26
+
+
+

What about our much-touted type-safe URLs? They are almost identical to variable interpolation + in every way, except they start with an at-sign (@) instead. In addition, there is embedding via + a caret (^) which allows you to embed another template of the same type.

+
URL Interpolation and Embedding
+ +
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Renderer.String (renderHtml)
+import Data.Text (Text)
+
+data MyRoute = Home
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+
+footer :: HtmlUrl MyRoute
+footer = [hamlet|
+<footer>
+    Return to #
+    <a href=@{Home}>Homepage
+    .
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ [hamlet|
+<body>
+    <p>This is my page.
+    ^{footer}
+|] render
+
+
+

Attributes

+

If you paid close attention there, you'll have noticed that we put an href attribute on the "a" + tag. That syntax is pretty straight-forward, but let's clarify some things:

  • You can have interpolations on the right-hand-side of the equals sign.
  • +
  • The equals sign and value for an attribute are optional, just like in HTML. So + <input type=checkbox checked> is perfectly valid.
  • +
  • There are two convenience attributes: for id, you can use the hash, and for classes, the + period. In other words, <p #paragraphid .class1 .class2>.
  • +
  • While quotes around the attribute value are optional, they are required if you want to embed + spaces.
  • +
  • You can add an attribute optionally by using colons. To check a checkbox only checked if the + variable isChecked is True, you would write <input type=checkbox + :isChecked:checked>. To have a paragraph be optionally red, you could use + <p :isRed:style="color:red">.
  • +

+

Conditionals

+

Eventually, you'll want to put in some logic in your page. The goal of Hamlet is to make the + logic as minimalistic as possible, pushing the heavy lifting into Haskell. As such, our logical + statements are very basic... so basic, that it's if, elseif, and + else.

$if isAdmin
+    <p>Welcome to the admin section.
+$elseif isLoggedIn
+    <p>You are not the administrator.
+$else
+    <p>I don't know who you are. Please log in so I can decide if you get access.
+
All + the same rules of normal interpolation apply to the content of the conditionals.

+

Maybe

+

Similarly, we have a special construct for dealing with Maybe values. This could technically be + dealt with using if, isJust and fromJust, but this is more convenient and avoids partial + functions.

$maybe name <- maybeName
+    <p>Your name is #{name}
+$nothing
+    <p>I don't know your name.
+
The + left hand side of the <- must be a simple identifier; the right can be anything that goes in + an interpolation.

+

Forall

+

And what about looping over lists? We have you covered there + too:

$if null people
+    <p>No people.
+$else
+    <ul>
+        $forall person <- people
+            <li>#{person}
+

+

With

Rounding out our statements, we have with. It's basically just a convenience for declaring a + synonym for a long + expression.

$with foo <- some very (long ugly) expression that $ should only $ happen once
+    <p>But I'm going to use #{foo} multiple times. #{foo}
+

+

Doctype

+

Last bit of syntactic sugar: the doctype statement. Just use the triple exclamation point on a + line by itself. Hamlet uses HTML5 by default, so the generated doctype is <!DOCTYPE + html>.

!!!
+<html>
+    <head>
+        <title>Hamlet is Awesome
+    <body>
+        <p>All done.
+

+

Cassius Syntax

+

Cassius is the original CSS template language. It uses simple whitespace rules to delimit + blocks, making braces and semicolons unnecessary. It supports both variable and URL + interpolation, but not embedding. The syntax is very + straight-forward:

#banner
+    border: 1px solid #{bannerColor}
+    background-image: url(@{BannerImageR})

+

Lucius Syntax

+

While Cassius uses a modified, whitespace-sensitive syntax for CSS, Lucius is true to the + original. You can take almost any CSS file out there and it will be a valid Lucius file. There + are, however, two additions to Lucius:

  • Like Cassius, we allow both variable and URL interpolation.
  • +
  • CSS blocks are allowed to nest.
  • +

+

That second point requires a bit of explanation. Let's say you want to have some special + styling for some tags within your article. In plain ol' CSS, you'd have to + write:

article code { background-color: grey; }
+article p { text-indent: 2em; }
+article a { text-decoration: none; }
+
In + this case, there aren't that many clauses, but having to type out article each time is still a + bit of a nuisance. Imagine if you had a dozen or so of these. Not the worst thing in the world, + but a bit of an annoyance. Lucius helps you out + here:
article {
+    code { background-color: grey; }
+    p { text-indent: 2em; }
+    a { text-decoration: none; }
+}

+

Other than that, Lucius is identical to CSS.

+

Julius Syntax

+

Julius is the simplest of the languages discussed here. In fact, some might even say it's not + really its own language. Julius allows the three forms of interpolation we've mentioned so far, + and otherwise applies no transformations to your content.

+

Calling Shakespeare

The question of course arises at some point: how do I actually use this stuff? There are three + different ways to call out to Shakespeare from your Haskell code:

+
Quasiquotes
+
Quasiquotes allow you to embed arbitrary content within your Haskell, and for it to be + converted into Haskell code at compile time.
+ + +
External file
+
In this case, the template code is in a separate file which is referenced via Template + Haskell.
+ + +
Debug mode
+
Both of the above modes require a full recompile to see any changes. In debug mode, your + template is kept in a separate file and referenced via Template Haskell. But at runtime, the + external file is reparsed from scratch each time.
+ +
+

One of the first two approaches should be used in production. They both embed the entirety of + the template in the final executable, simplifying deployment and increasing performance. The + advantage of the quasiquoter is the simplicity: everything stays in a single file. For short + templates, this can be a very good fit. However, in general, the external file approach is + recommended because:

  • It follows nicely in the tradition of separate logic from presentation.
  • +
  • You can easily switch between external file and debug mode with some simple CPP macros, + meaning you can keep rapid development and still achieve high performance in production.
  • +

+

Since these are special quasiquoters and TH functions, you need to be sure to enable the + appropriate language extensions and use correct syntax. A simple example of each.

+
Quasiquoter
+ +
{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Data.Text (Text)
+import Text.Blaze.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: Text -> HtmlUrl MyRoute
+template title = [hamlet|
+!!!
+<html>
+    <head>
+        <title>#{title}
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1>#{title}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ template "My Title" render
+
+
+
External file
+ +
{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-} -- to control production versus debug
+import Text.Lucius (CssUrl, luciusFile, luciusFileDebug, renderCss)
+import Data.Text (Text)
+import qualified Data.Text.Lazy.IO as TLIO
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: CssUrl MyRoute
+#if PRODUCTION
+template = $(luciusFile "template.lucius")
+#else
+template = $(luciusFileDebug "template.lucius")
+#endif
+
+main :: IO ()
+main = TLIO.putStrLn $ renderCss $ template render
+
+
-- @template.lucius
+foo { bar: baz }
+
+

The naming scheme for the functions is very consistent.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageQuasiquoterExternal fileDebug
HamlethamlethamletFileN/A
CassiuscassiuscassiusFilecassiusFileDebug
LuciusluciusluciusFileluciusFileDebug
JuliusjuliusjuliusFilejuliusFileDebug
+

Alternate Hamlet Types

+

So far, we've seen how to generate an HtmlUrl value from Hamlet, which is a + piece of HTML with embedded type-safe URLs. There are currently three other values we can + generate using Hamlet: plain HTML, HTML with URLs and internationalized messages, and + widgets. That last one will be covered in the widgets chapter.

+

To generate plain HTML without any embedded URLs, we use "simplified Hamlet". There are a few + changes:

  • We use a different set of functions, prefixed with an "s". So the quasiquoter is + shamlet and the external file function is shamletFile. How + we pronounce those is still up for debate.
  • +
  • No URL interpolation is allowed. Doing so will result in a compile-time error.
  • +
  • Embedding (the caret-interpolator) no longer allows arbitrary HtmlUrl + values. The rule is that the embedded value must have the same type as the template itself, so + in this case it must be Html. That means that for shamlet, embedding can be + completely replaced with normal variable interpolation (with a hash).
  • +

+

Internationalized (from now on, i18n) is a bit more complicated than normal Hamlet. This is + based around the idea of a message datatype, very similar in concept and implementation to a + type-safe URL. As a motivating example, let's say we want to have an application that tells you + hello and how many apples you have eaten. We could represent those messages with a + datatype.

data Msg = Hello | Apples Int
+
Next, we would + want to be able to convert that into something human-readable, so we define some render + functions:
renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+
Now + we want to interpolate those Msg values directly in the template. For that, we use underscore + interpolation.
!!!
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+

+

This kind of a template now needs some way to turn those values into HTML. So just like + type-safe URLs, we pass in a render function. To represent this, we define a new type + synonym:

type Render url = url -> [(Text, Text)] -> Text
+type Translate msg = msg -> Html
+type HtmlUrlI18n msg url = Translate msg -> Render url -> Html
+
At + this point, you can pass off renderEnglish, renderSpanish, or renderKlingon to this template, and + it will generate nicely translated output (depending, of course, on the quality of your + translators). A full sample program follows.

+
i18n Example
+ +
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Data.Text (Text)
+import qualified Data.Text as T
+import Text.Hamlet (HtmlUrlI18n, ihamlet)
+import Text.Blaze (toHtml)
+import Text.Blaze.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+renderUrl :: MyRoute -> [(Text, Text)] -> Text
+renderUrl Home _ = "/home"
+renderUrl Time _ = "/time"
+renderUrl Stylesheet _ = "/style.css"
+
+data Msg = Hello | Apples Int
+
+renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+
+template :: Int -> HtmlUrlI18n Msg MyRoute
+template count = [ihamlet|
+!!!
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml
+     $ (template 5) (toHtml . renderEnglish) renderUrl
+
+
+

General Recommendations

Here are some general hints from the Yesod community on how to get the most out of Shakespeare. (If you have more, please add them in a comment to this paragraph:

+
  • For actual sites, use external files. For libraries, it's OK to use quasiquoters, assuming they aren't too long.
  • +
  • Patrick Brisbin has put together a Vim code highlighter that can help out immensely.
  • +
  • You should almost always start Hamlet tags on their own line instead of embedding start/end tags after an existing tag. The only exception to this is the occasional <i> or <b> tag inside a large block of text.
  • +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/yesod-0-9-release-candidate.html b/public/blog/2011/08/yesod-0-9-release-candidate.html new file mode 100644 index 00000000..a0fa8c01 --- /dev/null +++ b/public/blog/2011/08/yesod-0-9-release-candidate.html @@ -0,0 +1,150 @@ + Announcing Yesod 0.9.0 Release Candidate +

Announcing Yesod 0.9.0 Release Candidate

August 23, 2011

GravatarBy Michael Snoyman

What's new?

+

You can get a taste for the new stuff in the 0.9.0 release from the ChangeLog + page. In general, this is a very stable, well-tested release. The beta is being actively + used in a number of production sites, including this one, with no reported ill effects. The Yesod + team feels very strongly that this is an upgrade worth making as soon as possible.

+

The purpose of the release candidate is to try to get any last-minute input on bugs or bad + decisions before they are set in stone with the official release. I highly encourage everyone + using Yesod to install the release candidate and test it out on a few of your sites.

+

Get set up for Yackage

+

The 0.9 release candidate is available on the Yesod Yackage server. Note that the version numbers are + the same as will be released with the final release. That means that, if you install the RC, it's + highly recommended to wipe your .ghc folder after the official release comes out.

+

In order to install the release candidate, follow these steps:

+
  1. Add the following line to your ~/.cabal/config + file:
    remote-repo: yesod-yackage:http://yackage.yesodweb.com
  2. +
  3. Run cabal update
  4. +
  5. Run cabal install yesod-0.9.0
  6. +
  7. Get a beer and congratulate yourself on a job well done.
  8. +
+

Migration guide: Yesod 0.8 to 0.9

As has become a bit of a custom, we're going to go through the necessary changes to the + Haskellers codebase to get it to work with the new release. Fortunately, there are many less + steps in this release. You can see the entire commit on Github. To give you an idea of time + investment, it took me about two hours to migrate Haskellers, write this guide, and apply a few + minor tweaks to the framework in the process.

+
  1. Fix up your cabal file. For the most part, just change yesod to version 0.9 and watch the + fallout. Pay particular attention to the version bounds on related packages like hamlet and + persistent. Also:
    • hamlet has been split out into multiple packages. You'll likely get build errors telling + you to add shakespeare-css and shakespeare-js
    • +
    • Due to issues with the newest aeson and Template Haskell, we had to create a fork. The API + is identical, the only difference is in how doubles are rendered. Just updated any references + to aeson to be aeson-native.
    • +
  2. +
  3. Goodbye "Helpers" module namespace. This affects Yesod.Helpers.Static, Yesod.Helpers.Auth.*, + Yesod.Helpers.Feed.* and others. All you need to do is remove the word Helpers.
  4. +
  5. The datatypes from hamlet have been renamed. This likely doesn't affect much code, but just + be aware of it. + + + + + + + + + + + + + + +
    Old nameNew name
    HamletHtmlUrl
    Cassius/LuciusCssUrl
    JuliusJavascriptUrl
    IHamletHtmlUrlI18n
  6. +
  7. Forms have had a significant overhaul. This will be the most labor-intensive part of the + migration. I'll address this later.
  8. +
  9. In your Model.hs file, mkPersist now takes an extra parameter to give it settings. For now + use sqlSettings, we'll have some more options in the future.
  10. +
  11. Model.hs needs the GADTs language extension enabled.
  12. +
  13. In your application file (e.g., Haskellers.hs), remove the type synonyms for Handler and + Widget. This is now provided by the TH code. Also, instead of Widget (), just say Widget.
  14. +
  15. In the YesodPersist instance, change "YesodDB" to "YesodPersistBackend".
  16. +
  17. The other big change: the filter API for persistent became much simpler. Instead of + using constructors like "PersonNameEq", we now just use operators. (Thanks to Boris for the + groundhog inspiration here.) Some simple examples: + + + + + + + + + + + +
    Old codeNew code
    PersonNameEq "Michael"PersonName ==. "Michael"
    PersonAgeLe 30PersonAge <=. 30
    UserEmailNe $ Just "foo@bar.com"UserEmail !=. Just "foo@bar.com"
    There will be a bit of manual effort involved in this transition, but the result + is well worth it.
  18. +
  19. select syntax has changed. Instead of having those two hanging integers at the end, we have a + list of options. An example should suffice:
    Old
    +
    selectList [PersonNameEq "Michael"] [PersonAgeDesc] 5 10
    + + +
    New
    +
    selectList [PersonName ==. "Michael"] [Desc PersonAge, LimitTo 5, OffsetBy 10]
    + +
    If you had a 0 for either of those trailing integers, they can just be ignored.
  20. +
  21. Updates are similarly changed.
    Old
    +
    update personId [PersonName "Michael"]
    + + +
    New
    +
    update personId [PersonName =. "Michael"]
    + +
  22. +
  23. And a great sigh of relief was heard: polymorphic hamlet is gone. For the most part, this + just means you'll need to preface a few hamlet calls with toWidget. Unfortunately, the error + messages are confusing; just email the list if you get stuff on somthing.
  24. +
  25. There have been some renames in the templates. It's best to just read the chapter for details.
  26. +
  27. We added i18n support for form messages. You'll need to add this to your application file + (e.g., + Haskellers.hs)
    instance RenderMessage Haskellers FormMessage where
    +    renderMessage _ _ = defaultFormMessage
    This + uses the default values, which are all English. You can modify these yourself if you want, + expect some blog posts in the next few weeks focusing on how i18n works.
  28. +
  29. Under some strange conditions, you'll have to give your ID variables explicit type + signatures. This is exceedingly rare.
  30. +
+

Forms

+

Forms got a huge cleanup in this release. I'm hoping to get the chapter up-to-date soon, but + the migration guide isn't enough to fully explain what's happened. I'll just give a few quick + translations that I've used in Haskellers:

+ + + + + + + + + + + + + + + + + + + + + + + + +
Old codeNew code
runFormPost'runInputPost
maybeStringInputiopt textField
stringInputireq textField
runFormGet'runInputGet
maybeIntInputiopt intField
maybeStringFieldaopt textField
fieldsToTablerenderTable
+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/08/yesod-form-overhaul.html b/public/blog/2011/08/yesod-form-overhaul.html new file mode 100644 index 00000000..c674c991 --- /dev/null +++ b/public/blog/2011/08/yesod-form-overhaul.html @@ -0,0 +1,142 @@ + Yesod form overhaul +

Yesod form overhaul

August 3, 2011

GravatarBy Michael Snoyman

Motivation

+

We're about ready for the Yesod 0.9 release candidate, and we're trying to polish all the rough + edges. Greg and I have been trying to get feedback on what's still missing and what could be done + better. Luite Stegeman did a good job pointing out some shortcomings in the yesod-form package. + I've made some major changes, some not directly related to his comments.

+

Let's start off by identifying the issues we needed to solve.

+
  • No direct way to do validations like "a number between 1 and 10". It was possible, just not + easy.
  • +
  • No way at all to perform monadic validations (is this username taken? is this date in the + past?).
  • +
  • To support i18n, we have the concept of message types. However, it hasn't been possible to + mix different types for the most part.
  • +
  • Type signatures very quickly become unwieldy.
  • +
+

SomeMessage

+

A quick review of i18n in Yesod: we have a typeclass called RenderMessage + defined + as

class RenderMessage master message where
+    renderMessage :: master
+                  -> [Text] -- ^ languages
+                  -> message
+                  -> Text
This + lets each application (distinguished by the master datatype) have its own sets of translations. + Within yesod-form, we would end up with a msg type variable in a few places, such as for error + messages from a field parser. And yesod-form defines a datatype FormMessage + which it uses for all its messages.

+

But let's say we want to have an integer field for numbers between 1 and 10. There are two + things that can go wrong:

  • the user could submit a non-integer. For this we want to use the built-in error message from + yesod-form of type FormMessage
  • +
  • the user could enter an integer outside the range. Here we would want to use the application + specific message type.
  • +

+

The question is how do we combine two different datatypes together. The answer: + existentials.

data SomeMessage master = forall msg. RenderMessage master msg => SomeMessage msg
We + now have a datatype that represents any message that can be translated for our application. By + using this datatype in place of a msg type variable, we get to both achieve our primary goal and + make our type signatures a bit shorter.

+

Less General Types

+

Once we are in the swing of cleaning up our type signatures, let's go for the gold. yesod-form + was originally designed based on formlets, which uses very general type signatures. formlets + doesn't specify how the html should be stored or in what monad the form should run. Instead, + those are all given as type parameters. And up until now, yesod-form has done the same thing.

+

However, this doesn't really make much sense for Yesod, where we know the user will be + representing their view as Widgets and running in the Handler monad. So instead of leaving these + to type parameters, we've now fixed them in the types themselves. Now the types in yesod-form + match up much more closely with the rest of the Yesod framework, taking parameters for the + subsite, master site, and the contained value.

+

Hopefully this will make the library easier to use and make compiler error messages + clearer.

+

Monadic field parser

+

Now with that maintenance overhead out of the way, we can start to focus on Luite's issues + directly. Let's start by looking at the definition of the fieldParse record in a Field from a few + days ago:

[Text] -> Either msg (Maybe a)
Fairly simple: take a list of + parameters (remember, multiple values can be submitted for each field, like with multi-select + fields) and either return an error message, Nothing if the input is missing, or the parsed value + on success. (Missing input may or may not be an error, depending on if the field is required or + optional.)

+

Now what we want is an easy way to add extra restriction onto that "a". Let's take our number + example from before. An intField from yesod-form would have a fieldParse looking like + [Text] -> Either FormMessage (Maybe Int). We now want to tack on some code that + can logically be expressed + as:

data MyAppMessage = BelowOne | AboveTen
+withinRange :: Int -> Either MyAppMessage Int
+withRange i
+    | i < 1 = Left BelowOne
+    | i > 10 = Left AboveTen
+    | otherwise = Right i
All + we need is a way to attach our withinRange to our fieldParse.

+

The first thing to point out is that our SomeMessage trick from earlier is absolutely necessary + at this point. Without it, we would be forced to either include every possible error message in + FormMessage, or redefine our built-in fields to use our custom error message type. Instead, we + can now wrap up the error messages in a SomeMessage constructor and easily compose these two + functions.

+

But let's take the slightly more complicated case: a non-pure validator. There is no provision + in fieldParse for monadic side effects. The solution for this is actually very simple: wrap the + return in a + monad:

data Field sub master a = Field
+    { fieldParse :: [Text] -> GGHandler sub master IO (Either (SomeMessage master) (Maybe a))
+    , fieldView :: ...
+    }

+

For our built-in fields the only change necessary is an added return.

+

The check functions

+

Now that the structure has been corrected, furnishing is easy. At this point, I'll let the code + speak for itself:

+
check :: RenderMessage master msg
+      => (a -> Either msg a) -> Field sub master a -> Field sub master a
+check f = checkM $ return . f
+
+-- | Return the given error message if the predicate is false.
+checkBool :: RenderMessage master msg
+          => (a -> Bool) -> msg -> Field sub master a -> Field sub master a
+checkBool b s = check $ \x -> if b x then Right x else Left s
+
+checkM :: RenderMessage master msg
+       => (a -> GGHandler sub master IO (Either msg a))
+       -> Field sub master a
+       -> Field sub master a
+checkM f field = field
+    { fieldParse = \ts -> do
+        e1 <- fieldParse field ts
+        case e1 of
+            Left msg -> return $ Left msg
+            Right Nothing -> return $ Right Nothing
+            Right (Just a) -> fmap (either (Left . SomeMessage) (Right . Just)) $ f a
+    }
+

And just to motivate this a bit more, the use code from the hello-forms.hs sample. Note that this code simply uses Text values for + its validation messages. If you're not concerned with i18n, this is always an option in your + applications.

+
myValidForm = runFormGet $ renderTable $ pure (,,)
+    <*> areq (check (\x ->
+            if T.length x < 3
+                then Left ("Need at least 3 letters" :: Text)
+                else Right x
+              ) textField)
+            "Name" Nothing
+    <*> areq (checkBool (>= 18) ("Must be 18 or older" :: Text) intField)
+            "Age" Nothing
+    <*> areq (checkM inPast dayField) "Anniversary" Nothing
+  where
+    inPast x = do
+        now <- liftIO getCurrentTime
+        return $ if utctDay now < x
+                    then Left ("Need a date in the past" :: Text)
+                    else Right x
+ +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/09/case-study-sphinx.html b/public/blog/2011/09/case-study-sphinx.html new file mode 100644 index 00000000..14001085 --- /dev/null +++ b/public/blog/2011/09/case-study-sphinx.html @@ -0,0 +1,664 @@ + Case Study: Sphinx-based Search +

Case Study: Sphinx-based Search

September 19, 2011

GravatarBy Michael Snoyman

Case Study: Sphinx-based Search

+

Sphinx is a search + server, and powers the search feature on many sites, including Yesod's own site. While the actual + code necessary to integrate Yesod with Sphinx is relatively short, it touches on a number of + complicated topics, and is therefore a great case study in how to play with some of the + under-the-surface details of Yesod.

+

There are essentially three different pieces at play here:

+
  • Storing the content we wish to search. This is fairly straight-forward Persistent code, and + we won't dwell on it much in this chapter.
  • +
  • Accessing Sphinx search results from inside Yesod. Thanks to the sphinx + package, this is actually very easy.
  • +
  • Providing the document content to Sphinx. This is where the interesting stuff happens, and + will show how to deal with streaming content from a database directly to XML, which gets sent + directly over the wire to the client.
  • +
+

Sphinx Setup

+

Unlike many of our other examples, to start with here we'll need to actually configure + and run our external Sphinx server. I'm not going to go into all the details of Sphinx, partly + because it's not relevant to our point here, and mostly because I'm not an expert on Sphinx.

+

Sphinx provides three main command line utilities: searchd + is the actual search daemon that receives requests from the client (in this case, our web app) + and returns the search results. indexer parses the set of documents and + creates the search index. search is a debugging utility that will run + simple queries against Sphinx.

+

There are two important settings: the source and the index. The source tells Sphinx + where to read document information from. It has direct support for MySQL and PostgreSQL, as well + as a more general XML format known as xmlpipe2. We're going to use the last one. This not only + will give us more flexibility with choosing Persistent backends, but will also demonstrate some + more powerful Yesod concepts.

+

The second setting is the index. Sphinx can handle multiple indices simultaneously, + which allows it to provide search for multiple services at once. Each index will have a source it + pulls from.

+

In our case, we're going to provide a URL from our application (/search/xmlpipe) that provides + the XML file required by Sphinx, and then pipe that through to the indexer. So we'll add the + following to our Sphinx config file:

+
source searcher_src
+{
+	type = xmlpipe2
+	xmlpipe_command = curl http://localhost:3000/search/xmlpipe
+}
+
+index searcher
+{
+	source = searcher_src
+	path = /var/data/searcher
+	docinfo = extern
+	charset_type = utf-8
+}
+

In order to build your search index, you would run indexer searcher. Obviously + this won't work until you have your web app running. For a production site, it would make sense + to run this command via a crontab script so the index is regularly updated.

+

Basic Yesod Setup

+

Let's get our basic Yesod setup going. We're going to have a single table in the database for + holding documents, which consist of a title and content. We'll store this in a SQLite database, + and provide routes for searching, adding documents, viewing documents and providing the xmlpipe + file to Sphinx.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher ConnectionPool
+
+mkYesod "Searcher" [parseRoutes|
+/ RootR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlPersist
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+

Hopefully all of this looks pretty familiar by now. Next we'll define some forms: one for + creating documents, and one for searching:

+
addDocForm :: Html -> MForm Searcher Searcher (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Searcher Searcher (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+

The True parameter to searchField makes the field auto-focus on page load. + Finally, we have some standard handlers for the homepage (shows the add document form and the + search form), the document display, and adding a document.

+
getRootR :: Handler RepHtml
+getRootR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout [whamlet|
+<p>Welcome to the search application. #{docs}
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+|]
+
+postAddDocR :: Handler RepHtml
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout [whamlet|
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+|]
+
+getDocR :: DocId -> Handler RepHtml
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout $
+        [whamlet|
+<h1>#{docTitle doc}
+<div .content>#{docContent doc}
+|]
+
+

Searching

+

Now that we've got the boring stuff out of the way, let's jump into the actual searching. We're + going to need three pieces of information for displaying a result: the document ID it comes from, + the title of that document, and the excerpts. Excerpts are the highlighted portions + of the document which contain the search term.

+
Search Result
+ +
+

So let's start off by defining a Result datatype:

+
data Result = Result
+    { resultId :: DocId
+    , resultTitle :: Text
+    , resultExcerpt :: Html
+    }
+
+

Next we'll look at the search handler:

+
getSearchR :: Handler RepHtml
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        addLucius [lucius|
+.excerpt {
+    color: green; font-style: italic
+}
+.match {
+    background-color: yellow;
+}
+|]
+        [whamlet|
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+$if not $ null searchResults
+    <h1>Results
+    $forall result <- searchResults
+        <div .result>
+            <a href=@{DocR $ resultId result}>#{resultTitle result}
+            <div .excerpt>#{resultExcerpt result}
+|]
+
+

Nothing magical here, we're just relying on the searchForm defined + above, and the getResults function which hasn't been defined yet. This + function just takes a search string, and returns a list of results. This is where we + first interact with the Sphinx API. We'll be using two functions: query + will return a list of matches, and buildExcerpts will return the + highlighted excerpts. Let's first look at query:

+
getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" (unpack qstring)
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (Key . PersistInt64 . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+

query takes three parameters: the configuration options, the index + to search against (searcher in this case) and the search string. It returns a list of + document IDs that contain the search string. The tricky bit here is that those documents + are returned as Int64 values, whereas we need DocIds. We're taking advantage of the fact that the SQL + Persistent backends use a PersistInt64 constructor for their IDs, and + simply wrap up the values appropriately.

+ +

We then loop over the resulting IDs to get a [Maybe Result] value, and use + catMaybes to turn it into a [Result]. In the where clause, we + define our local settings, which override the default port and set up the search to work when + any term matches the document.

+

Let's finally look at the getResult function:

+
getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [T.unpack $ escape $ docContent doc]
+        "searcher"
+        (unpack qstring)
+    let excerpt =
+            case excerpt' of
+                ST.Ok bss -> preEscapedLazyText $ decodeUtf8With ignore $ L.concat bss
+                _ -> return ()
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+
+

buildExcerpts takes four parameters: the configuration options, the + textual contents of the document, the search index and the search term. The interesting + bit is that we entity escape the text content. Sphinx won't automatically escape these + for us, so we must do it explicitly.

+

Similarly, the result from Sphinx is a list of lazy ByteStrings. But of course, we'd rather + have Html. So we concat that list into a single lazy ByteString, decode it to a lazy text + (ignoring invalid UTF-8 character sequences), and use preEscapedLazyText to make sure that the + tags inserted for matches are not escaped. A sample of this HTML is:

+
&#8230; Departments.  The President shall have <span class='match'>Power</span> to fill up all Vacancies
+&#8230;  people. Amendment 11 The Judicial <span class='match'>power</span> of the United States shall
+&#8230; jurisdiction. 2. Congress shall have <span class='match'>power</span> to enforce this article by
+&#8230; 5. The Congress shall have <span class='match'>power</span> to enforce, by appropriate legislation
+&#8230;
+
+

Streaming xmlpipe output

+

We've saved the best for last. For the majority of Yesod handlers, the recommended approach is + to load up the database results into memory and then produce the output document based on that. + It's simpler to work with, but more importantly it's more resilient to exceptions. If there's a + problem loading the data from the database, the user will get a proper 500 response code.

+ +

However, generating the xmlpipe output is a perfect example of the alternative. There are + potentially a huge number of documents (the yesodweb.com code handles tens of thousands of + these), and documents could easily be 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? As we cover in the WAI chapter, we have a ResponseSource constructor that + uses a stream of blaze-builder Builders. From the Yesod side, we can + avoid the normal Yesod response procedure and send a WAI response directly using the sendWaiResponse function. So there are at least two of the pieces of this + puzzle.

+

Now we know we want to create a stream of Builders from some XML + content. Fortunately, the xml-conduit package provides this + interface directly. xml-conduit provides some high-level interfaces for dealing + with documents as a whole, but in our case, we're going to need to use the low-level Event + interface to ensure minimal memory impact. So the function we're interested in is:

+
renderBuilder :: Resource m => RenderSettings -> Conduit Event m Builder b
+
+

In plain English, that means renderBytes takes some settings (we'll just use the + defaults), and will then convert a stream of Events to a stream of + Builders. This is looking pretty good, all we need now is a stream of + Events.

+

Speaking of which, what should our XML document actually look like? It's pretty + simple, we have a sphinx:docset root element, a sphinx:schema element containing a single sphinx:field + (which defines the content field), and then a sphinx:document for each + document in our database. That last element will have an id attribute and a + child content element.

+
Sample xmlpipe document
+ +
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">
+    <sphinx:schema>
+        <sphinx:field name="content"/>
+    </sphinx:schema>
+    <sphinx:document id="1">
+        <content>bar</content>
+    </sphinx:document>
+    <sphinx:document id="2">
+        <content>foo bar baz</content>
+    </sphinx:document>
+</sphinx:docset>
+
+
+

Every document is going to start off with the same events (start the docset, start + the schema, etc) and end with the same event (end the docset). We'll start off by defining + those:

+
toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+
+

Now that we have the shell of our document, we need to get the Events for each individual document. This is actually a fairly simple function:

+
entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+
+

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.

+

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 also provides the (more powerful) selectSourceConn function. So we + end up with the function:

+
docSource :: Connection -> C.Source IO X.Event
+docSource conn = selectSourceConn conn [] [] C.$= CL.concatMap entityToEvents
+
+

The $= operator joins together a source and a conduit into a new source. 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 Monoid instance, this is a piece + of cake:

+
fullDocSource :: Connection -> C.Source IO X.Event
+fullDocSource conn = mconcat
+    [ CL.sourceList startEvents
+    , docSource conn
+    , CL.sourceList endEvents
+    ]
+
+

We're almost there, now we just need to tie it together in + getXmlpipeR. We need to get a database connection to be used. Normally, + database connections are taken and returned automatically via the runDB + function. In our case, we want to check out a connection and keep it available until the response + body is completely sent. To do this, we use the takeResource function, which + registers a cleanup action with the ResourceT monad.

+ +

By default, a resource will not be returned to the pool. This has to do with proper + exception handling, but is not relevant for our use case. Therefore, we need to force the + connection to be returned to the pool.

+
getXmlpipeR :: Handler RepXml
+getXmlpipeR = do
+    Searcher pool <- getYesod
+    let headers = [("Content-Type", "text/xml")]
+    managedConn <- lift $ takeResource pool
+    let conn = mrValue managedConn
+    lift $ mrReuse managedConn True let source = fullDocSource conn C.$= renderBuilder def
+    sendWaiResponse $ ResponseSource status200 headers source
+
+

We get our connection pool from the foundation variable, then send a WAI response. We + use the ResponseSource constructor, and provide it the status code, response + headers, and body.

+

Full code

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
+QuasiQuotes, MultiParamTypeClasses, GADTs, FlexibleContexts
+#-}
+import Yesod
+import Data.Text (Text, unpack)
+import Control.Applicative ((<$>), (<*>))
+import Database.Persist.Sqlite
+import Database.Persist.Query.GenericSql (selectSourceConn)
+import Database.Persist.Store (PersistValue (PersistInt64))
+import qualified Text.Search.Sphinx as S
+import qualified Text.Search.Sphinx.Types as ST
+import qualified Text.Search.Sphinx.ExcerptConfiguration as E
+import qualified Data.ByteString.Lazy as L
+import Data.Text.Lazy.Encoding (decodeUtf8With)
+import Data.Text.Encoding.Error (ignore)
+import Data.Maybe (catMaybes)
+import Control.Monad (forM)
+import qualified Data.Text as T
+import Text.Blaze (preEscapedLazyText)
+import qualified Data.Conduit as C
+import qualified Data.Conduit.List as CL
+import qualified Data.XML.Types as X
+import Network.Wai (Response (ResponseSource))
+import Network.HTTP.Types (status200)
+import Text.XML.Stream.Render (renderBuilder, def)
+import Data.Monoid (mconcat)
+import Data.Conduit.Pool (takeResource, mrValue, mrReuse)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher ConnectionPool
+
+mkYesod "Searcher" [parseRoutes|
+/ RootR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlPersist
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+addDocForm :: Html -> MForm Searcher Searcher (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Searcher Searcher (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+getRootR :: Handler RepHtml
+getRootR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout [whamlet|
+<p>Welcome to the search application. #{docs}
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+|]
+
+postAddDocR :: Handler RepHtml
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout [whamlet|
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+|]
+
+getDocR :: DocId -> Handler RepHtml
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout $
+        [whamlet|
+<h1>#{docTitle doc}
+<div .content>#{docContent doc}
+|]
+
+data Result = Result
+    { resultId :: DocId
+    , resultTitle :: Text
+    , resultExcerpt :: Html
+    }
+
+getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [T.unpack $ escape $ docContent doc]
+        "searcher"
+        (unpack qstring)
+    let excerpt =
+            case excerpt' of
+                ST.Ok bss -> preEscapedLazyText $ decodeUtf8With ignore $ L.concat bss
+                _ -> return ()
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+
+getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" (unpack qstring)
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (Key . PersistInt64 . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+getSearchR :: Handler RepHtml
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        addLucius [lucius|
+.excerpt {
+    color: green; font-style: italic
+}
+.match {
+    background-color: yellow;
+}
+|]
+        [whamlet|
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+$if not $ null searchResults
+    <h1>Results
+    $forall result <- searchResults
+        <div .result>
+            <a href=@{DocR $ resultId result}>#{resultTitle result}
+            <div .excerpt>#{resultExcerpt result}
+|]
+
+getXmlpipeR :: Handler RepXml
+getXmlpipeR = do
+    Searcher pool <- getYesod
+    let headers = [("Content-Type", "text/xml")]
+    managedConn <- lift $ takeResource pool
+    let conn = mrValue managedConn
+    lift $ mrReuse managedConn True
+    let source = fullDocSource conn C.$= renderBuilder def
+        flushSource = fmap C.Chunk source
+    sendWaiResponse $ ResponseSource status200 headers flushSource
+
+entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+
+fullDocSource :: Connection -> C.Source IO X.Event
+fullDocSource conn = mconcat
+    [ CL.sourceList startEvents
+    , docSource conn
+    , CL.sourceList endEvents
+    ]
+
+docSource :: Connection -> C.Source IO X.Event
+docSource conn = selectSourceConn conn [] [] C.$= CL.concatMap entityToEvents
+
+toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+
+main :: IO ()
+main = withSqlitePool "searcher.db3" 10 $ \pool -> do
+    runSqlPool (runMigration migrateAll) pool
+    warpDebug 3000 $ Searcher pool
+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/09/limitations-of-haskell.html b/public/blog/2011/09/limitations-of-haskell.html new file mode 100644 index 00000000..f3de34c1 --- /dev/null +++ b/public/blog/2011/09/limitations-of-haskell.html @@ -0,0 +1,49 @@ + The Limitations of Haskell +

The Limitations of Haskell

September 4, 2011

GravatarBy Greg Weber

Haskell is an amazing programming language that has helped us create an amazing web framework. We leverage Haskell's capabilities wherever we can, but we want to go over some of its biggest weaknesses. In this post, any mention of Haskell really means Haskell using the GHC compiler.

+

Yesod is in a somewhat unique position of being a highly used Haskell project with many dependencies. And we haven't just created a web framework- we have created a new set of template languages and Haskell's first "ORM". We are pushing the boundaries of the language in every area, and mostly finding that Haskell was well designed for all of this. There are certainly more complaints that could be make about Haskell, but we want to focus on practical issues that we have found in Yesod that are the most difficult to workaround.

+

Limitations with unknown solutions

+

Error messages from Complex types

+

In Yesod we have some complex types in a few key places. And we are fine with having complex types internally. However, exposing them in an interface is a very delicate matter- users have to be able to understand error messages. Yesod has abandoned certain uses of polymorphism due to this issue. And we would still like to make error messages better.

+

Reading error messages is an interface through which programmers learn Haskell. Is there a way to make error messages better? The most interesting approach I can think of is to somehow let the library writer specify certain error messages.

+

Limitations with partial solutions

+

no stack traces

+

This is less of a bother than one would think when coming from another language, but still a problem. Simon Marlow recently stated he may be able to come up with a solution, which was very exciting news. Yesod now supports logging statements that contain file and line number information using Template Haskell. I released a package that applies the same technique. This allows the programmer to record the file and line number location, but not a stack trace. There is also a package that is designed to give a stack trace for an exception.

+

code reloading

+

Invoking yesod devel will automatically recompile a Yesod project with cabal as files change, and start the new program in place of the old. However, the re-linking time dominates this and makes for slower reloading that we would like. There is also a plugins package for Haskell for re-loading the code that Happstack has recently been improving. And Snap has some techniques for re-loading Handlers.

+

We would really like what some dynamic languages have achieved- only code relevant to what has changed get re-interpreted, and this is done quickly and relatively reliably. But between the various Haskell web frameworks it seems that we are coming up with decent solutions.

+

Limitations that can be solved now

+

Dependency Hell and using unreleased (beta or internal) code.

+

Dependency Hell is when you type cabal update && cabal install and cabal informs you it can't install the packages, even though your package specifications are correct.

+

Ever since the Bundler Ruby tool matured, I have never once been on a Ruby project where the libraries could not be immediately installed. This is what Haskell is competing against, and we shouldn't settle for less.

+

cabal-dev has been a great stride towards solving these dependency issues, and I feel like Haskell is getting closer to the goal of flawless installs.

+

The other issue with cabal is specifying using code that isn't on Hackage. I should be able to specify in a cabal file to use a package version that is on the file system or under version control on github. This makes beta releasing dead simple- just have users to edit their cabal file. It also makes having local changes very simple- just point the package in the cabal file to your local file system.

+

There is also a new attempt to give Haskell a more reliable interface than just version numbers. I think of this as focused on preventing installations that fail to compile, whereas I view dependency hell as mostly being about failing to configure and start the installation.

+

Template Haskell re-loading

+

Template Haskell (TH) that performs IO does not automatically recompile itself when it should. In Yesod we have proved the value of having compile-time templates in external files. This puts Haskell on par with dynamic languages in terms of ease of using templates. It actually puts Haskell in a much better position because the templates are evaluated for errors at compile time. But when an external hamlet template changes, it won't automatically get recompiled by ghc. We are able to work around this in our development environment by watching for changes to external templates. However, this is a frail solution, and we believe that GHC should provide this capability for us. We want to have a function loadQQ, which takes a quasi-quoter and a file path. loadQQ will automatically recompile the hs file when the quasi-quoted file has changed.

+

Namespace clashing, particularly for record fields

+
data Record = Record {a :: String }
data RecordClash = RecordClash {a :: String }
+

Compiling this file results in:

+
record.hs:2:34:
+    Multiple declarations of `Main.a'
+    Declared at: record.hs:1:24
+                 record.hs:2:34
+
+

In the Persistent data store library, we work around the issue by having the standard of prefixing every record field with the record name (recordA and recordClashA). But besides being extremely verbose, it also limits us from experimenting with more advanced features like a partial record projection or an unsaved and saved record type.

+

The verbose name-spacing required is an in-your-face, glaring weakness telling you there is something wrong with Haskell. This issue has been solved in almost every modern programming languages, and there are plenty of possible solutions available to Haskell.

+

Here is one solution. The Haskell community is generally wonderful at advancing the language (or at least advancing GHC). Records is one place where this process has completely fallen apart, and Haskell has become a backwards language.

+

So we want to start a discussion: how can we solve Haskell name-spacing, at least for records? From Yesod's perspective we don't care what the solution is, just as long as we have one. Let us know what your thoughts are here, or on Reddit, and we are going to take this to GHC HQ and figure out a way to finally solve this issue.

+

Lets make Haskell better!

+

I want to stress that Haskell is an amazing language that solves many problems that other languages will be eternally stuck with. The great thing is that all the problems listed here can eventually be solved. We love using Haskell for Yesod. We just want to make it even better.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/09/new-book-content.html b/public/blog/2011/09/new-book-content.html new file mode 100644 index 00000000..305be3a7 --- /dev/null +++ b/public/blog/2011/09/new-book-content.html @@ -0,0 +1,341 @@ + New Book Content +

New Book Content

September 23, 2011

GravatarBy Michael Snoyman

Joins

+

As we said in the introduction to this chapter, Persistent is non-relational: it works + perfectly well with either SQL or non-SQL, and there is nothing inherent in its design which + requires relations. On the other hand, this + very chapter gave advice on modeling relations. What gives?

+

Both statements are true: you're not tied to relations, but they're available if you want to + use them. And when the time comes, Persistent provides you with the tools to easily create + efficient relational queries, or in SQL terms: table joins.

+

To play along with our existing no-SQL slant, the basic approach to doing joins in Persistent + does not actually use any joins in SQL. This means that a Persistent join will work perfectly + well with MongoDB, even though Mongo doesn't natively support joins. However, when dealing with a + SQL database, most of the time you'll want to use the database's join abilities. And for this, + Persistent provides an alternate module that works just that way. (Obviously, that module is + incompatible with Mongo.)

+

The best part? These two modules have an identical API. All you have to do is swap out which + runJoin function you import, and the behavior changes as well.

+

So how does this joining work? Let's look at a one-to-many relationship, such as a car/owner + example above. Every car has an owner, and every owner has zero or more cars. The + Database.Persist.Query.Join module provides a datatype, SelectOneMany, that contains a bunch of join + settings, such as how to sort the owners (somOrderOne) and how to filter the cars + (somFitlterMany).

+

In addition, there is a selectOneMany function, which will fill in defaults for all the + settings except two. This function needs to be told how to filter the cars based on an owner, and + how to determine the owner from a car value.

+ +

When you run a SelectOneMany, it will return something with a bit of a crazy type signature: + [((PersonId, Person), [(CarId, Car)])]. This might look intimidating, but lets + simplify it just a bit:

+
type PersonPair = (PersonId, Person)
+type CarPair = (CarId, Car)
+type Result = [(PersonPair, [CarPair])]
+
+

In other words, all this means is a grouped list of people to their cars.

+

What happens if a person doesn't have a car? By default, they won't show up in the output, + though you can override this with the somIncludeNoMatch record. The default behavior matches the + behavior of a SQL inner join. Overriding this matches the behavior of a SQL left join.

+

One other note: while the somOrderOne field is optional, you'll almost always want to provide + it. Without it, there is no guarantee that the cars will be grouped appropriately. You might end + up with multiple records for a single person.

+
{-# LANGUAGE TypeFamilies, TemplateHaskell, MultiParamTypeClasses,
+GADTs, QuasiQuotes, OverloadedStrings, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Database.Persist.Query.Join (SelectOneMany (..), selectOneMany)
+import Control.Monad.IO.Class (liftIO)
+
+-- We'll use the SQL-enhanced joins. If you want the in-application join
+-- behavior instead, just import runJoin from Database.Persist.Query.Join
+import Database.Persist.Query.Join.Sql (runJoin)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+Car
+    owner PersonId
+    name String
+|]
+
+main :: IO ()
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+    bruce <- insert $ Person "Bruce Wayne"
+    insert $ Car bruce "Bat Mobile"
+    insert $ Car bruce "Porsche"
+    -- this could go on a while
+
+    peter <- insert $ Person "Peter Parker"
+    -- poor Spidey, no car
+
+    logan <- insert $ Person "James Logan" -- Wolverine
+    insert $ Car logan "Harley"
+
+    britt <- insert $ Person "Britt Reid" -- The Green Hornet
+    insert $ Car britt "The Black Beauty"
+
+    results <- runJoin (selectOneMany (CarOwner <-.) carOwner)
+        { somOrderOne = [Asc PersonName]
+        }
+
+    liftIO $ printResults results
+
+printResults :: [(Entity Person, [Entity Car])] -> IO ()
+printResults =
+    mapM_ goPerson
+  where
+    goPerson :: (Entity Person, [Entity Car]) -> IO ()
+    goPerson ((Entity _personid person), cars) = do
+        putStrLn $ personName person
+        mapM_ goCar cars
+        putStrLn ""
+
+    goCar :: (Entity Car) -> IO ()
+    goCar (Entity _carid car) = putStrLn $ "    " ++ carName car
+
+

Monadic Forms

+

Often times, a simple form layout is adequate, and applicative forms excel at + this approach. Sometimes, however, you'll want to have a more customized look to your + form.

+
A non-standard form layout
+ +
+

For these use cases, monadic forms fit the bill. They are a bit more verbose + than their applicative cousins, but this verbosity allows you to have complete control + over what the form will look like. In order to generate the form above, we could code + something like this.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+TemplateHaskell, MultiParamTypeClasses #-}
+import Yesod
+import Control.Applicative
+import Data.Text (Text)
+
+data Monadic = Monadic
+
+mkYesod "Monadic" [parseRoutes|
+/ RootR GET
+|]
+
+instance Yesod Monadic
+
+instance RenderMessage Monadic FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person { personName :: Text, personAge :: Int }
+    deriving Show
+
+personForm :: Html -> MForm Monadic Monadic (FormResult Person, Widget)
+personForm extra = do
+    (nameRes, nameView) <- mreq textField "this is not used" Nothing
+    (ageRes, ageView) <- mreq intField "neither is this" Nothing
+    let personRes = Person <$> nameRes <*> ageRes
+    let widget = do
+            toWidget [lucius|
+##{fvId ageView} {
+    width: 3em;
+}
+|]
+            [whamlet|
+#{extra}
+<p>
+    Hello, my name is #
+    ^{fvInput nameView}
+    \ and I am #
+    ^{fvInput ageView}
+    \ years old. #
+    <input type=submit value="Introduce myself">
+|]
+    return (personRes, widget)
+
+getRootR :: Handler RepHtml
+getRootR = do
+    ((res, widget), enctype) <- runFormGet personForm
+    defaultLayout [whamlet|
+<p>Result: #{show res}
+<form enctype=#{enctype}>
+    ^{widget}
+|]
+
+main :: IO ()
+main = warpDebug 3000 Monadic
+
+

Similar to the applicative areq, we use mreq for monadic forms. (And yes, there's also + mopt for optional fields.) But there's a big difference: + mreq gives us back a pair of values. Instead of hiding away the + FieldView value and + automatically inserting it into a widget, we get the control to insert it as we see + fit.

+

FieldView has a number of pieces of information. The most + important is fvInput, which is the actual form field. In this example, + we also use fvId, which gives us back the HTML id + attribute of the input tag. In our example, we use that to specify the width of the + field.

+

You might be wondering what the story is with the "this is not used" and + "neither is this" values. mreq takes a FieldSettings as its second argument. Since FieldSettings + provides an IsString instance, the strings are essentially expanded by + the compiler + to:

fromString "this is not used" == FieldSettings
+    { fsLabel = "this is not used"
+    , fsTooltip = Nothing
+    , fsId = Nothing
+    , fsName = Nothing
+    , fsClass = []
+    }
+
In + the case of applicative forms, the fsLabel and + fsTooltip values are used when constructing your HTML. In the case + of monadic forms, Yesod does not generate any of the "wrapper" HTML for you, and + therefore these values are ignored.

+

The other interesting bit is the extra value. + GET forms include an extra field to indicate that they have been + submitted, and POST forms include a security tokens to prevent CSRF + attacks. If you don't include this extra hidden field in your form, Yesod will not + accept it.

+

Other than that, things are pretty straight-forward. We create our + personRes value by combining together the nameRes + and ageRes values, and then return a tuple of the person and the + widget. And in the getRootR function, everything looks just like an + applicative form. In fact, you could swap out our monadic form with an applicative one + and the code would still work.

+ +

Input forms

+

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 forms.

+

These work mostly the same as applicative and monadic forms, with some differences:

+
  • You use runInputPost and runInputGet.
  • +
  • You use ireq and iopt. These functions now only + take two arguments: the field type and the name (i.e., HTML name attribute) of + the field in question.
  • +
  • After running a form, it returns the value. It doesn't return a widget or an + encoding type.
  • +
  • If there are any validation errors, the page returns an "invalid arguments" error page.
  • +
+

You can use input forms to recreate the previous example. Note, however, that the + input version is less user friendly. If you make a mistake in an applicative or monadic form, you + will be brought back to the same page, with your previously entered values in the form, and an + error message explaning what you need to correct. With input forms, the user simply gets an error + message.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+TemplateHaskell, MultiParamTypeClasses #-}
+import Yesod
+import Control.Applicative
+import Data.Text (Text)
+
+data Input = Input
+
+mkYesod "Input" [parseRoutes|
+/ RootR GET
+/input InputR GET
+|]
+
+instance Yesod Input
+
+instance RenderMessage Input FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person { personName :: Text, personAge :: Int }
+    deriving Show
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<form action=@{InputR}>
+    <p>
+        My name is #
+        <input type=text name=name>
+        \ and I am #
+        <input type=text name=age>
+        \ years old. #
+        <input type=submit value="Introduce myself">
+|]
+
+getInputR :: Handler RepHtml
+getInputR = do
+    person <- runInputGet $ Person
+                <$> ireq textField "name"
+                <*> ireq intField "age"
+    defaultLayout [whamlet|<p>#{show person}|]
+
+main :: IO ()
+main = warpDebug 3000 Input
+
+

Custom fields

+

The fields that come built-in with Yesod will likely cover the vast majority of your + form needs. But occasionally, you'll need something more specialized. Fortunately, you can + create new forms in Yesod yourself. The Field datatype has two records: + fieldParse takes a list of values submitted by the user and returns one of + three results:

+
  • An error message saying validation failed.
  • +
  • The parsed value.
  • +
  • Nothing, indicating that no data was supplied.
  • +
+

That last case might sound surprising: shouldn't Yesod automatically know that no information + is supplied when the input list is empty? Well, no actually. Checkboxes, for instance, indicate + an unchecked state by sending in an empty list.

+

Also, what's up with the list? Shouldn't it be a Maybe? Well, that's + also not the case. With grouped checkboxes and multi-select lists, you'll have multiple widgets + with the same name. We also use this trick in our example below.

+

The second record is fieldView, and it renders a widget to display to + the user. This function has four arguments: the id attribute, the + name attribute, the result and a Bool indicating if the field + is required.

+

What did I mean by result? It's actually an Either, giving either + the unparsed input (when parsing failed) or the successfully parsed value. + intField is a great example of how this works. If you type in 42, the value of result will be Right 42. But + if you type in turtle, the result will be Left + "turtle". This lets you put in a value attribute on your input tag that will give the + user a consistent experience.

+

As a small example, we'll create a new field type that is a password confirm field. + This field has two text inputs- both with the same name attribute- and returns an error + message if the values don't match. Note that, unlike most fields, it does not provide a value attribute on the input tags, as you don't want to send back + a user-entered password in your HTML ever.

+
passwordConfirmField :: Field sub master Text
+passwordConfirmField = Field
+    { fieldParse = \rawVals ->
+        case rawVals of
+            [a, b]
+                | a == b -> return $ Right $ Just a
+                | otherwise -> return $ Left "Passwords don't match"
+            [] -> return $ Right Nothing
+            _ -> return $ Left "You must enter two values"
+    , fieldView = \idAttr nameAttr _ eResult isReq -> [whamlet|
+<input id=#{idAttr} name=#{nameAttr} type=password>
+<div>Confirm:
+<input id=#{idAttr}-confirm name=#{nameAttr} type=password>
+|]
+    }
+
+getRootR :: Handler RepHtml
+getRootR = do
+    ((res, widget), enctype) <- runFormGet $ renderDivs
+        $ areq passwordConfirmField "Password" Nothing
+    defaultLayout [whamlet|
+<p>Result: #{show res}
+<form enctype=#{enctype}>
+    ^{widget}
+    <input type=submit value="Change password">
+|]
+
+ +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/09/yesod-0.9.2.html b/public/blog/2011/09/yesod-0.9.2.html new file mode 100644 index 00000000..3bfc167f --- /dev/null +++ b/public/blog/2011/09/yesod-0.9.2.html @@ -0,0 +1,33 @@ + Yesod 0.9.2 - Everything Just Works +

Yesod 0.9.2 - Everything Just Works

September 11, 2011

GravatarBy Greg Weber

We are pleased to announce the release of Yesod version 0.9.2. This release includes some important new features.

+

Re-compiling your application

+

One of the worst aspects of developing a web application in Haskell is constantly re-compiling. Our solution to this in Yesod is the "yesod devel" server. It automatically re-compiles your application when a file changes and restarts your application. Unfortunately it always had problems, particularly on Windows. Luite Stegeman committed an improved version that should just work. Let us know if you have any issues. The new devel server takes a --dev flag to use cabal-dev, which is what I was most excited about. Yesod users can finally work on one application without worrying about breaking another. Using this feature requires no changes to your application code, and nothing different than using the existing yesod devel server. Just cabal update && cabal install yesod.

+

One outstanding issue is re-compiling (shakeapeare/hamlet) compile-time templates. This should work fine as long as you use the new yesod devel and stick to the defaults, but it is still brittle. So we are solving this issue in GHC. The Simons have proposed a solution and believe this to be a small and relatively easy to implement change. They are still hoping for us to step up and make it, and we are so busy pushing Yesod along that we are hoping someone else might feel like learning a little bit about GHC. If you are interested in helping out let us know, but we will make sure this gets done one way or another.

+

Happstack has also come up with some good techniques to reload an application. They can detect file changes with inotify on Linux, whereas we use polling in Yesod. Happstack also uses plugins to reload code. This has always been a fragile approach, but Hapsstack has recently made some improvements on it. Yesod's approach is cross-platform and reliable, but Happstack's approach performs better when it works. We are going to work together with the Happstack maintainers to combine our efforts in this area.

+

MongoDB

+

The Persistent MongoDB backend has always been on tenuous grounds. Today it finally just works. There is even a scaffolding option you can use to generate a site that uses MongoDB as a backend.

+

MongoDB is a high performance database designed for scalability. SQL is a much more flexible way to store data- you can always join one table to another to perform complex ad-hoc queries- this is particularly useful for creating reports and summarizing your application data. However, reporting or other reasons for ad-hoc queries are not a strong requirement for most applications. If this is the case then one can design a schema in MongoDB where the query patterns are frozen. Instead of joining data structures, one embeds a data structure within another. Embedding is very fast and easier to scale. And you can embed lists or maps, something sorely missing from most SQL databases. This persistent release still has limitations in its support for embedding. You can embed Haskell lists and maps, but not records.

+

Recent discussions

+

Records discussion

+

I enjoyed the discussion around the last blog post. My hope is that we can keep the records discussion alive and solve that issue in the Haskell language. I certainly learned a lot more about the issue. Various techniques have been proposed for dealing with the existing records situation. They all start with each record declared in its own file and imported (qualified when necessary) instead of being prefixed. In Yesod (Persistent actually), records represent database information and we prefix them with the table name. However, this was mostly for reasons that no longer matter after the 0.9 release. So we are going to look into declaring records in different files and some of the techniques to make records play more nicely with each other.

+

Yesod discussions

+

Expect some improvements in Yesod's Internationalization for determining which languages are supported. There have also been a fair amount of discussions around forms lately on how to satisfy complex use-cases where fields are dependent upon eachother. Unfortunately there are no elegant solutions to this yet.

+

Websockets and Event-Stream

+

These seem to be on people's mind. And for good reason, because Haskell is the best language to support these highly concurrent technologies. We are looking forward to see what people come up with and eager to help them out.

+

Yesod has very few bugs

+

Now that yesod devel and MongoDB work properly, I feel like Yesod is finally stable now. No more buggy or broken things hanging around. Yesod encompasses a lot of code and a lot of features. The 0.9 release introduced a lot of changes and features, and has already seen a lot of users. With all these changes in a large code-base, we saw only 2 bugs in the framework reported, and neither of them were new to this release. Besides those 2 framework bugs, the rest of the bugs were in the code generated from the scaffolding tool (code generator for new sites). The scaffolding system is inherently fragile, but an indispensable convenience.

+

Yesod's increasing maturity is helping with stability. The maturing process represents more users exercising our code, but also a maturing of our tooling and development approach. Most of the Yesod code base now has solid testing behind it. We always view the type system and compiler as our first line of defense. However, correctness is usually only encoded in the type system at a low-level - functions work properly and interact well with data and one-another. When we need increased assurance at this level QuickCheck is an amazing tool. But generally we skimp on low-level unit tests, and write more api level integration tests. We find this gives us great value for the amount of effort put into testing. The new cabal test feature is our ally in this effort, and hspec helps us write well described tests with the minimum amount of cruft.

+

On the whole, I think the stability Yesod has achieved is amazing! Haskell lets us release some rock solid code.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/10/code-generation-conversation.html b/public/blog/2011/10/code-generation-conversation.html new file mode 100644 index 00000000..f76f9368 --- /dev/null +++ b/public/blog/2011/10/code-generation-conversation.html @@ -0,0 +1,72 @@ + Code that writes code and conversation about conversations +

Code that writes code and conversation about conversations

October 26, 2011

GravatarBy Greg Weber

In a previous post I talked about the importance of frameworks and also made some very vague criticism of framework critics. This comment was made of the post:

+
Concerning Yesod, the criticism that I harbor is that it seems (seems!) to rely too much on Template Haskell and Quasi Quoting where a plain old combinator library would do.
+
+

I am very sorry to be picking on an individual. I appreciate the statement being made directly to me instead of just passing it along to someone else, and I hope he forgives me for feeling the need to use this as a specific example.

+

The idea that Template Haskell (TH) and Quasi Quoting (QQ) is bad and combinators should be used instead betrays a lack of knowledge about the purpose of TH/QQ in its use in Yesod: the same level of type-safety and conciseness simply cannot be achieved with combinators. So now we have a criticism that arises from a lack of knowledge about the subject at hand. To the comment's credit, it is qualified with "it seems". It might seem that way because the criticism was repeated from or reinforced by other really smart Haskellers, but none of the people making the statement (including the commentator in this case) have ever used Yesod.

+

For some Haskellers there is such a dogma of aversion to Template Haskell and Quasi Quoting and love of combinators that smart people are making uninformed statements. I am hoping we can all acknowledge that frameworks require code generation to reach high levels of type-safety, abstraction, and productivity. I am not asking for a free pass, but instead of harboring criticism, ask the important question: "Why is it that Yesod uses TH/QQ for routing?". If the answer is not satisfactory, then please criticize, but a first step in a healthy dialogue is missing here, after which we can have useful informed criticism.

+

I know this paragraph is going to sound really pedantic, but it bears repeating, at least to remind myself to do it more. When we express an opinion or make a statement we are not going to want to later admit that we are wrong. In the case of criticism it is much easier on everyone to ask questions when possible and otherwise keep things well qualified. The one being critiqued won't get as defensive. If the idea behind the questioning is shown to be invalid, the critic does not have to eat humble pie, but can instead be thankful for a good answer.

+

Those that contribute to and use Yesod are well aware that TH/QQ is a trade-off - it does give up the familiarity, ease of modification, and composability of normal Haskell code. Lets see what benefits they provide that cannot be achieved with combinators alone.

+

Explanation of Template Haskell & Quasi-Quoting use

+

Routing

+

Our goal which requires Template Haskell is type-safe URLs. They make a site much more robust, especially in the face of changes. If you change your URL scheme, just recompile, and GHC will tell you every single place in your application that needs to be fixed.

+

Our simple example application just looks up (blog) posts.

+
newtype PostId = PostId Int -- some kind of database ID
+

If you use Yesod, you use its routing:

+
/             HomeR GET
+/post/#PostId PostR GET
+
+

And we are done routing! We just need to define our handler functions

+
getHomeR :: Handler RepHtml
+getHomeR = ...
+getPostR :: PostId -> Handler RepHtml
+getPostR postId = ...
+
+

Lets talk over what went on here. A data declaration was created for each URL:

+
data PostRoute = HomeR | PostR PostId
+
+

If you want to link to a specific post, you don't need to start splicing together strings, dealing with how to display a PostId, or anything else. You just type in "PostR postId". If you accidently use a String instead of a PostId, your code won't compile. So you can't accidently link to "/post/my-blog-title". So we now have type-safe urls. We can pattern match on them, as is done in Yesod to authorize a user for certain routes. We can also stick urls in our templates and know that they are valid.

+

So Yesod is generating code similar to this:

+
render HomeR = "/"
render (PostR postId) = "/post/" ++ (toString postId)
+

But Yesod is also parsing and dispatching. Route parameters are automatically parsed - in this case an Int. If a user enters "abc" as a post id, the parsing fails and an appropriate error response is automatically returned. The String from the route is automatically converted to an Int. So there is some parsing/dispatching code like this:

+
dispatch ("/":[]) = return getHomeR
dispatch ("/post/":postId:[]) = parseInt postId >>= return . getPostR
+

In order to make this system work, you need several components: a data type that dispatches to a function, a parse function, and a render function. Writing all of this code manually is both tedious and error-prone: it's very easy to accidently let your parse and render functions get out-of-sync. This is why Yesod uses TH: by having a single, well-tested piece of code do the whole thing, we avoid a lot of boilerplate and sidestep bugs.

+

It is actually possible to create type-safe urls with routing combinators by using Jeremy Shaw's recently released web-routes-boomerang package. While this package does not use Quasi Quoting it still requires separately declaring route data types and then using Template Haskell to avoid some of the boilerplate. That template Haskell creates references for the separate route parser. You still have to connect the route data types to functions to dispatch to in a dispatch function. It is a very neat use of combinators and the boomerang parsing concept. However, at the end of the day there is still more boilerplate and mental re-combining - it is not any simpler to use than Yesod's routing.

+

Templates

+

Technically you don't have to use the Shakespeare family of compile-time templates with Yesod, but users love them and we see little desire to try to use any alternatives.

+
<html>
<head>
<linkrel="stylesheet"href=@{StaticRnormalize_css}>
<title>If you know html, you know Hamlet
<body>
<ahref=@{HomeR}> Go Home
<ul>
<li> Insert your Haskell #{variable} with the ease of use of dynamic languages
<li> But with the guarantee that it exists at compile time
<li> and it will be XSS escaped if needed.
+

This is a compile-time template. Notice the link that refers to HomeR. This is from our route declaration, and compiling this template will fail unless the route exists. By default we also generate routes for Static files (the StaticR above). This means you don't have to load up the browse and check the debugger tool to see if it failed to load a stylesheet - if the template compiles it works.

+

Any variables interpolated exist as normal Haskell code. This avoids a needless error-prone step of creating a name-value mapping for a run-time template. The variable insertion expands to something like:

+
(Builder "<li> Insert your Haskell") `mappend` (toBuilder . toHtml variable) `mappend` (Builder "with the ease of use of dynamic languages")
+

Under the hood, the very efficient blaze-builder is being used, but that is an encapsulated implementation detail. toHtml is called on every insertions. This lets us guarantee safety against XSS attacks. If your value is already XSS escaped, you simply use an Html type that has a ToHtml instance defined that does not escape it.

+

The alternative systems that Haskell has offered have not been as compelling. blaze-html is a combinator library to define your entire HTML file as Haskell code. But html is an xml-like standard to be read by a browser. It is the combinator library that is adding complexity by taking something which is designed as text and turning it into code. Combinators compose into functions, which is very good, but Hamlet composes into Widgets, which work just as well. Moreover, Hamlet is not just designed for Haskellers - it is designed for web designers. Designers love using Hamlet because it is just HTML with simple ways to insert Haskell values.

+

Persistent

+

One can create a Yesod site without using the Persistent library. We provide a 'tiny' scaffolding option for this. However, we recommend using the Persistent library for data storage. The goal of Persistent is to provide type-safe database queries and automatically marshal data. This means that unlike other Haskell database libraries, mistyping the name of a column is a compilation error, and you get to work directly with regular Haskell records. This is all facilitated by declaring a quasi-quoted schema.

+
Person
+    name String
+    age Int Maybe
+BlogPost
+    title String
+    authorId PersonId
+
+

The original version of Persistent had a lot more Template Haskell generation than it does now. We now use combinators for Persistent queries, but we used to use generated data constructors, simply because we didn't realize there was an alternative way to implement Persistent using existential types. So on the one hand, those that are broadly critical of the use of Template Haskell would be correct in that case, but on the other hand there wasn't any meaningful dialogue. We would have loved for a Template Haskell hater to tell us: "but you don't need TH, you can implement this with existential types this way". But insteaad we had to wait a long time for a similar library with existential types to show up, and then we switched to that style of implementation. I should also note that we have considered not using Quasi Quoting now that the implementation has changed - it makes QQ less necessary, but at a minimum Template Haskell is still required to maintain conciseness.

+

Template Haskell and Quasi-Quoting can be easy for the end user

+

Easy to use and type-safe templates and urls require Template Haskell and Quasi Quoting, and are among the most important features in Yesod, and they are what set it apart from all other web frameworks that we know of. But new features means limited horizons: those that haven't used them are very unlikely to recognize the difference, and it is nearly impossible to appreciate the benefits without using it yourself.

+

Template Haskell and Quasi-Quoting provide type-safety and features not otherwise possible without a lot of boilerplate. If you would like, you can get the exact same result by manually typing out the boilerplate. However, this is a meaningless exercise most of the time that risks creating bugs.

+

The 3 Yesod Quasi-Quoting cases are ridiculously easy to learn. There is a fake Haskell user that is sometimes used for the sake of argument. This user is not intelligent enough to figure out how to use a dead simple routing syntax, but is somehow comfortable learning routing combinator libraries. In fact, learning a routing syntax consisting of a total of 5 different tokens specialized for the task at hand is likely easier than learning a combinator library. The combinator library can compose functions, but it is hard to translate that into a meaningful advantage.

+

As simple as the routing DSL is, the majority of real Haskell programmers are probably uncomfortable modifying the code behind it. That is a tradeoff we are ok with - routing is a limited domain that the framework can readily handle - the programmer should be spending time on custom application code or enhancing other areas of the framework that have much greater variation.

+

Conclusion

+

I hope this explains some of the benefits of Yesod to those who haven't used it yet, and the utility behind Yesod's uses of Template Haskell and Quasi Quoting. I also hope we can move from dogma and uninformed criticism into meaningful dialogue. Intelligent conversation is one the best reasons to be a part of the Haskell community.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/10/frameworks-drive.html b/public/blog/2011/10/frameworks-drive.html new file mode 100644 index 00000000..209f79ce --- /dev/null +++ b/public/blog/2011/10/frameworks-drive.html @@ -0,0 +1,37 @@ + Frameworks drive language adoption +

Frameworks drive language adoption

October 24, 2011

GravatarBy Greg Weber

Hypothesis: Frameworks are the drivers of adoption of new programming languages. Without frameworks the inertia of the existing languages are too great to overcome.

+

Others have explored this hypothesis from different angles, sometimes with actual data!

+ +

Programming in recent times has been dominated by C, C++ and Java. These computer efficient but verbose and limited languages left a gaping hole for languages more focused on developer productivity. Perl and Python helped to fill that space. There are many other languages that are used in large part because they are official languages of a platform - C#, Visual Basic, Objective-C, and Javascript.

+

It is very difficult for a new language to overcome the cost of switching from an existing language. PHP never had much to offer in terms of great language features over Perl, which it was originally created from. But it was designed to be able to quickly create web pages as the web was taking off, so it was able to fill that specific niche by offering something more productive than alternatives. PHP was like a framework - it excelled at a specific problem domain. And there are incredibly popular PHP frameworks like WordPress that helped drive and assure its continued use today.

+

Ruby started with very small scripting use, argued by some as better than Perl or Python for that domain. But then Ruby offered Ruby on Rails, a much more productive way of doing web development than what existed at the time, and it began an exponential growth. And now Ruby is also getting increased usage in the system administration domain because it has two good frameworks for that: Puppet and Chef.

+

Scala is on the rise. One of the biggest reasons is because Java has now become a platform itself, and Scala can be used as a better Java. Scala offers many great language features that make for a much more productive programming language than Java. But as David Pollak points out, that alone is not enough to overcome the effort required to switch languages. Pollak makes the case that Scala needs to focus on specific industries and problem domains where it offers enormous benefits over Java.

+

Pollak created the Lift web framework, which has been a driver of Scala adoption. The main advertised benefit of Lift is for interactive web applications. However, most other programming languages are now trying to compete in this area, and in particular javascript with node.js is a strong competitor. While Lift certainly brings in new Scala users, the increased competition may have stopped Lift from becoming an exponential driver of Scala use. Scala is also seeing interest due to the Akka framework which provides Erlang-like actors and process management, along with STM for creating robust distributed systems.

+

A language being good on its own merits may not be enough to drive new adoption anymore unless it is highly compatible with an existing popular language. And in the case of Scala, that compatibility does not seem to be enough for explosive growth. I think Pollak is correct that Scala needs more focus, but I think it could focus in many more areas than he thinks, so long as it first develops great frameworks in those areas. The most impressive exponential growth of a platform independent language in recent times has been from Ruby, and it was driven almost entirely by the Ruby on Rails framework.

+

I am a big fan of Paul Graham's writings and doings. Graham writes in relation to startups. He has written about the importance of programming language, and he has the experience to back it up. Graham wrote the first web application using a Lisp programming language, and it was very successful for him and all that were involved. Graham attributes much of his success to the choice of Lisp, and asserts that it is a superior language. However, Graham was creating the first web application. If he had to compete against someone using a modern web framework today, it wouldn't be the same level playing field it was when he created his first startup. He would also be at a greater disadvantage in terms of available and easy to install libraries relative to a modern popular programming language user.

+

This entire line of thought follows from a broader principle: the key to creating software quickly is to re-use good existing code. Existing languages remain popular in large part because they build up a large collection of re-usable libraries, and a system for conveniently installing and using them. However, libraries alone are not enough any more. An even greater productivity boost are collections of libraries that work extremely well together: frameworks. A framework takes care of as much code common to the problem domain as possible and provides developers with re-usable patterns to develop their custom software. Popular languages build up frameworks also. But new languages can compete at the framework level, in part because the cost of switching frameworks can be very high. Switching to a new language is now a piece of the equation that also includes the benefits of the new framework.

+

If frameworks are the key, why aren't there a ton of great frameworks in every language? One reason is that not every problem domain lends itself as well to frameworks - hard constraints make for frameworks - server-side web application development most often involves using web frameworks because there will always be the constraints of HTTP.

+

But I suspect the biggest reason for a lack of great frameworks is that creating a great framework is hard. Hard in a design sense means that most frameworks won't be designed well. But the natural evolution of the problem space would still lead to a lot of well designed frameworks. The real difficulty in creating a framework comes from the amount of effort required for a long duration. A good programmer can write a useful library in a day to satisfy a need. Good frameworks do come from companies that invest programmer time in creating frameworks. However, many open source frameworks start with the unpaid efforts of a single programmer. That pace is very limited - the key to making something good is for the programmer to stick with it. Just the initial framework creation can easily consume a programmer's free-time for a month. Making a really good framework is a process of continual creation and adjustment from feedback of users - a process that can easily take a year of dedicated effort. It is difficult for a programmer to directly recoup the cost to their time that they have put into an open source framework. If a programmer is creating a framework in an unpopular language that they don't get to use in their day job, that is even more the case. Time and money are working against the framework creator, particularly in the case of open source and unpopular languages. Perhaps the most important aspect of a framework is the community of users that grow around it. I tend to think this of this as an organic process. However, supporting a new community of users adds on to the skillset and work required to create a great framework before the threshold of community contributions makes up for that effort. In the long-term, frameworks can gain a lot of advantage by making it easy for the community to contribute re-usable code that is not officially part of the framework (plugins).

+

Lets put on our Haskell hat!

+

The Haskell programming language has an amazing set of technical merits that are unmatched by other programming languages. The type system lets one declaratively model data and catches an amazing number of bugs. The GHC runtime has incredible concurrency and parallelism capabilities.

+

But technical merits do not translate directly to writing software quickly - libraries are required. Haskell is in a great position to share libraries - its installation infrastructure is starting to mature. The strong-typing means it is much easier to use a library correctly, and there is much more certainty that the code works.

+

Certainly Haskell is lacking in specific libraries, but there are now thousands of libraries available to help get you started on your task. Some of these libraries (Parsec, QuickCheck) are incredibly good in comparison to what popular languages normally offer. However, parsing and testing are usually just a portion of a larger problem domain. The largest effect these libraries are having with those outside the Haskell community is causing them to implement these libraries in the language they are using. Many Haskell libraries are nowhere near as remarkable, with many not of great quality or are at least lacking in documentation. But even a large set of good libraries is not enough to drive adoption. Haskell needs frameworks that make developers really productive in established problem domains.

+

Yesod is such a framework for web development. Warp, the preferred web server for Yesod (and now other frameworks), leverages the GHC runtime for concurrency performance not possible in other languages. Yesod uses the type system to catch errors that can't be caught in popular programming languages. For instance, all application URLs are known to be valid at compile time. There are other web frameworks for Haskell with their own merit, none of them have gone as far in type safety, levels of abastraction, and ease of use. Yesod is bringing new users into Haskell, or at least helping new users stick with Haskell by making them productive at web development.

+

A running joke of Haskell is to “Avoid Success at All Costs!”. If frameworks are necessary for success, then some members of the Haskell community seemed to be taking things too seriously. Particularly when Yesod first started, some in the community seemed to be criticizing Yesod essentially for being a framework (saying things like "Yesod is its own world"). On the one hand this is true to a certain extent of all frameworks, but on the other hand Yesod is highly modular and new web frameworks have already been created in Haskell largely by re-using its libraries. Other criticisms were made, but none of those offering negative opinions about Yesod had actually tried using it. I think this peculiar behavior reflects cultural issues with the Haskell community. I think Haskell is partly a reaction against the normal inadequate ways of programming. One of the normal ways of programming is to use frustrating frameworks, so anything ambitiously venturing into framework territory risks guilt by association. One of the reasons Haskell is great is because the community is amazingly knowledgeable. However, this amazingly smart group of people tends to know very little about web programming. Haskellers have not been doing much web development because there haven't been good libraries to support their effort. This lack of experience has not always stopped some from being opinionated. However, now that the library and framework situation is turning around in Haskell I expect this situation to change. Haskellers are learning web development and web developers are learning Haskell.

+

A good framework becomes a language dialect of its own, and pushes the host programming language to its limits. For Haskell to be successful, we need the community to be supportive of these kinds of efforts. But we also need more great frameworks! In every domain, Haskell has a chance to beat out existing popular frameworks because of its strong typing, efficient code, powerful runtime, and support for creating declarative code. Haskell is already very good at embedded domain specific languages, but you can also crank things up a notch and use Template Haskell and Quasi Quoting when the regular language features are not enough.

+

There are many great ways to help out the Haskell community. In particular, those that work on the compiler, core infrastructure, and quality libraries are all heroes. They help ensure Haskell stays a technically amazing and easy to use programming language. Haskell needs to keep this momentum going. But if the goal is to drive adoption, building and supporting great frameworks should be one of the greatest concerns.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/10/settings-types.html b/public/blog/2011/10/settings-types.html new file mode 100644 index 00000000..371d99cf --- /dev/null +++ b/public/blog/2011/10/settings-types.html @@ -0,0 +1,86 @@ + Settings Types +

Settings Types

October 5, 2011

GravatarBy Michael Snoyman

+

Let's say you're writing a webserver. You want the server to take a port to listen on, and an + application to run. So you create the following function:

+
run :: Int -> Application -> IO ()
+
+

But suddenly you realize that some people will want to customize their timeout durations. So + you modify your API:

+
run :: Int -> Int -> Application -> IO ()
+
+

So, which Int is the timeout, and which is the port? Well, you could create + some type aliases, or comment your code. But there's another problem creeping into our code: this + run function is getting unmanageable. Soon we'll need to take an extra + parameter to indicate how exceptions should be handled, and then another one to control which + host to bind to, and so on.

+

So a more extensible solution is to introduce a settings datatype:

+
data Settings = Settings
+    { settingsPort :: Int
+    , settingsHost :: String
+    , settingsTimeout :: Int
+    }
+
+

And this makes the calling code almost self-documenting:

+
run Settings
+    { settingsPort = 8080
+    , settingsHost = "127.0.0.1"
+    , settingsTimeout = 30
+    } myApp
+
+

Great, couldn't be clearer, right? True, but what happens when you have 50 settings to your + webserver. Do you really want to have to specify all of those each time? Of course not. So + instead, the webserver should provide a set of defaults:

+
defaultSettings = Settings 3000 "127.0.0.1" 30
+
+

And now, instead of needing to write that long bit of code above, we can get away with:

+
run defaultSettings { settingsPort = 8080 } myApp -- (1)
+
+

This is great, except for one minor hitch. Let's say we now decide to add an extra record to + Settings. Any code out in the wild looking like + this:

run (Settings 8080 "127.0.0.1" 30) myApp -- (2)
+
will + be broken, since the Settings constructor now takes 4 arguments. The proper + thing to do would be to bump the major version number so that dependent packages don't get + broken. But having to change major versions for every minor setting you add is a nuisance. The + solution? Don't export the Settings constructor:

+
module MyServer
+    ( Settings
+    , settingsPort
+    , settingsHost
+    , settingsTimeout
+    , run
+    , defaultSettings
+    ) where
+
+

With this approach, no one can write code like (2), so you can freely add new records without + any fear of code breaking.

+

The one downside of this approach is that it's not immediately obvious from the Haddocks that + you can actually change the settings via record syntax. That's the point of this chapter: to give + everyone using this approach a page to link to that will explain to users what's going on.

+

I personally use this technique in a few places, feel free to have a look at the Haddocks to + see what I mean.

+ +

As a tangential issue, http-conduit and xml-conduit actually + create instances of the Default typeclass instead of + declaring a brand new identifier. This means you can just type def instead of + defaultParserSettings.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/10/xml-enumerator.html b/public/blog/2011/10/xml-enumerator.html new file mode 100644 index 00000000..67b46001 --- /dev/null +++ b/public/blog/2011/10/xml-enumerator.html @@ -0,0 +1,615 @@ + xml-enumerator +

xml-enumerator

October 18, 2011

GravatarBy Michael Snoyman

xml-conduit

+

Many developers cringe at the thought of dealing with XML files. XML has the reputation of + having a complicated data model, with obfuscated libraries and huge layers of complexity sitting + between you and your goal. I'd like to posit that a lot of that pain is actually a language and + library issue, not inherent to XML.

+

Once again, Haskell's type system allows us to easily break down the problem to its most basic + form. The xml-types package neatly deconstructs the XML data model (both a + streaming and DOM-based approach) into some simple ADTs. Haskell's standard immutable data + structures make it easier to apply transforms to documents, and a simple set of functions makes + parsing and rendering a breeze.

+

We're going to be covering the xml-conduit package. Under + the surface, this package uses a lot of the approaches Yesod in general does for high + performance: blaze-builder, text, + conduit and attoparsec. But from a + user perspective, it provides everything from the simplest APIs + (readFile/writeFile) through full control of XML event + streams.

+

In addition to xml-conduit, there are a few related packages that + come into play, like xml-hamlet and xml2html. We'll cover both how to use all these packages, and when they should be + used.

+

Synopsis

+
Input XML file
+ +
<document title="My Title">
+    <para>This is a paragraph. It has <em>emphasized</em> and <strong>strong</strong> words.</para>
+    <image href="myimage.png"/>
+</document>
+
+
+
Haskell code
+ +
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile, writeFile)
+import Text.XML
+import Text.Hamlet.XML
+
+main :: IO ()
+main = do
+    -- readFile will throw any parse errors as runtime exceptions
+    -- def uses the default settings
+    Document prologue root epilogue <- readFile def "input.xml"
+
+    -- root is the root element of the document, let's modify it
+    let root' = transform root
+
+    -- And now we write out. Let's indent our output
+    writeFile def
+        { rsPretty = True
+        } "output.html" $ Document prologue root' epilogue
+
+-- We'll turn out <document> into an XHTML document
+transform :: Element -> Element
+transform (Element _name attrs children) = Element "html" [] [xml|
+<head>
+    <title>
+        $maybe title <- lookup "title" attrs
+            \#{title}
+        $nothing
+            Untitled Document
+<body>
+    $forall child <- children
+        ^{goNode child}
+|]
+
+goNode :: Node -> [Node]
+goNode (NodeElement e) = [NodeElement $ goElem e]
+goNode (NodeContent t) = [NodeContent t]
+goNode (NodeComment _) = [] -- hide comments
+goNode (NodeInstruction _) = [] -- and hide processing instructions too
+
+-- convert each source element to its XHTML equivalent
+goElem :: Element -> Element
+goElem (Element "para" attrs children) =
+    Element "p" attrs $ concatMap goNode children
+goElem (Element "em" attrs children) =
+    Element "i" attrs $ concatMap goNode children
+goElem (Element "strong" attrs children) =
+    Element "b" attrs $ concatMap goNode children
+goElem (Element "image" attrs _children) =
+    Element "img" (map fixAttr attrs) [] -- images can't have children
+  where
+    fixAttr ("href", value) = ("src", value)
+    fixAttr x = x
+goElem (Element name attrs children) =
+    -- don't know what to do, just pass it through...
+    Element name attrs $ concatMap goNode children
+
+
+
Output XHTML
+ +
<?xml version="1.0" encoding="UTF-8"?>
+<html>
+    <head>
+        <title>
+            My Title
+        </title>
+    </head>
+    <body>
+        <p>
+            This is a paragraph. It has 
+            <i>
+                emphasized
+            </i>
+            and 
+            <b>
+                strong
+            </b>
+            words.
+        </p>
+        <img src="myimage.png"/>
+    </body>
+</html>
+
+
+

Types

+

Let's take a bottom-up approach to analyzing types. This section will also serve as a primer on + the XML data model itself, so don't worry if you're not completely familiar with it.

+

I think the first place where Haskell really shows its strength is with the + Name datatype. Many languages (like Java) struggle + with properly expressing names. The issue is that there are in fact three components to a name: + its local name, its namespace (optional), and its prefix (also optional). Let's look at some XML + to explain:

+
<no-namespace/>
+<no-prefix xmlns="first-namespace" first-attr="value1"/>
+<foo:with-prefix xmlns:foo="second-namespace" foo:second-attr="value2"/>
+
+

The first tag has a local name of no-namespace, and no + namespace or prefix. The second tag (local name: no-prefix) also has no prefix, but it does have a namespace (first-namespace). first-attr, however, does not + inherit that namespace: attribute namespaces must always be explicitly set with a prefix.

+ +

The third tag has a local name of with-prefix, a prefix of + foo and a namespace of second-namespace. + Its attribute has a second-attr local name and the same prefix and namespace. + The xmlns and xmlns:foo attributes are part of the namespace + specification, and are not considered attributes of their respective elements.

+

So let's review what we need from a name: every name has a local name, and it can optionally + have a prefix and namespace. Seems like a simple fit for a record type:

+
data Name = Name
+    { nameLocalName :: Text
+    , nameNamespace :: Maybe Text
+    , namePrefix :: Maybe Text
+    }
+
+

According the the XML namespace standard, two names are considered equivalent if they + have the same localname and namespace. In other words, the prefix is not important. Therefore, + xml-types defines Eq and Ord instances that + ignore the prefix.

+

The last class instance worth mentioning is IsString. It would be + very tedious to have to manually type out Name "p" Nothing Nothing + every time we want a paragraph. If you turn on OverloadedStrings, + "p" will resolve to that all by itself! In addition, the + IsString instance recognizes something called Clark notation, which allows you + to prefix the namespace surrounded in curly brackets. In other words:

+
"{namespace}element" == Name "element" (Just "namespace") Nothing
+"element" == Name "element" Nothing Nothing
+
+

The Four Types of Nodes

+

XML documents are a tree of nested nodes. There are in fact four different types of nodes + allowed: elements, content (i.e., text), comments, and processing instructions.

+ +

Since processing instructions have two pieces of text associated with them (the target and the + data), we have a simple data type:

+
data Instruction = Instruction
+    { instructionTarget :: Text
+    , instructionData :: Text
+    }
+
+

Comments have no special datatype, since they are just text. But content is an + interesting one: it could contain either plain text or unresolved entities (e.g., + &copyright-statement;). xml-types keeps those + unresolved entities in all the data types in order to completely match the spec. However, in + practice, it can be very tedious to program against those data types. And in most use cases, an + unresolved entity is going to end up as an error anyway.

+

So the Text.XML module defines its own set + of datatypes for nodes, elements and documents that removes all unresolved entities. If you need + to deal with unresolved entities instead, you should use the Text.XML.Unresolved module. From now on, we'll be focusing only on the + Text.XML data types, though they are almost identical to the + xml-types versions.

+

Anyway, after that detour: content is just a piece of text, and therefore it too does + not have a special datatype. The last node type is an element, which contains three pieces of + information: a name, a list of attributes and a list of children nodes. An attribute has two + pieces of information: a name and a value. (In xml-types, this value could + contain unresolved entities as well.) So our Element is defined as:

+
data Element = Element
+    { elementName :: Name
+    , elementAttributes :: [(Name, Text)]
+    , elementNodes :: [Node]
+    }
+
+

Which of course begs the question: what does a Node look like? This + is where Haskell really shines: its sum types model the XML data model perfectly.

+
data Node
+    = NodeElement Element
+    | NodeInstruction Instruction
+    | NodeContent Text
+    | NodeComment Text
+
+

Documents

+

So now we have elements and nodes, but what about an entire document? Let's just lay out the + datatypes:

+
data Document = Document
+    { documentPrologue :: Prologue
+    , documentRoot :: Element
+    , documentEpilogue :: [Miscellaneous]
+    }
+
+data Prologue = Prologue
+    { prologueBefore :: [Miscellaneous]
+    , prologueDoctype :: Maybe Doctype
+    , prologueAfter :: [Miscellaneous]
+    }
+
+data Miscellaneous
+    = MiscInstruction Instruction
+    | MiscComment Text
+
+data Doctype = Doctype
+    { doctypeName :: Text
+    , doctypeID :: Maybe ExternalID
+    }
+
+data ExternalID
+    = SystemID Text
+    | PublicID Text Text
+
+

The XML spec says that a document has a single root element + (documentRoot). It also has an optional doctype statement. Before and after + both the doctype and the root element, you are allowed to have comments and processing + instructions. (You can also have whitespace, but that is ignored in the parsing.)

+

So what's up with the doctype? Well, it specifies the root element of the document, and then + optional public and system identifiers. These are used to refer to DTD files, which give more + information about the file (e.g., validation rules, default attributes, entity resolution). Let's + see some examples:

+
<!DOCTYPE root> <!-- no external identifier -->
+<!DOCTYPE root SYSTEM "root.dtd"> <!-- a system identifier -->
+<!DOCTYPE root PUBLIC "My Root Public Identifier" "root.dtd"> <!-- public identifiers have a system ID as well -->
+
+

And that, my friends, is the entire XML data model. For many parsing purposes, you'll + be able to simply ignore the entire Document datatype and go immediately to the + documentRoot.

+

Events

+

In addition to the document API, xml-types defines an Event datatype. This can be used for constructing + streaming tools, which can be much more memory efficient for certain kinds of processing (eg, + adding an extra attribute to all elements). We will not be covering the streaming API currently, + though it should look very familiar after analyzing the document API.

+ +

Text.XML

+

The recommended entry point to xml-conduit is the Text.XML module. This module exports all of the datatypes you'll need to + manipulate XML in a DOM fashion, as well as a number of different approaches for parsing and + rendering XML content. Let's start with the simple + ones:

readFile  :: ParseSettings  -> FilePath -> IO Document
+writeFile :: RenderSettings -> FilePath -> Document -> IO ()
+
This + introduces the ParseSettings and RenderSettings datatypes. You can use these to modify the behavior of the parser and + renderer, such as adding character entities and turning on pretty (i.e., indented) output. Both + these types are instances of the Default + typeclass, so you can simply use def when these need to be supplied. + That is how we will supply these values through the rest of the chapter; please see the API docs + for more information.

+

It's worth pointing out that in addition to the file-based API, there is also a text- and + bytestring-based API. The bytestring-powered functions all perform intelligent encoding + detections, and support UTF-8, UTF-16 and UTF-32, in either big or little endian, with and + without a Byte-Order Marker (BOM). All output is generated in UTF-8.

+

For complex data lookups, we recommend using the higher-level cursors API. The + standard Text.XML API not only forms the basis for that higher level, but is + also a great API for simple XML transformations and for XML generation. See the synopsis for an + example.

+

A note about file paths

+

In the type signature above, we have a type FilePath. However, this isn't + Prelude.FilePath. The standard Prelude defines a type + synonym type FilePath = [Char]. Unfortunately, there are many limitations to + using such an approach, including confusion of filename character encodings and differences in + path separators.

+

Instead, xml-conduit uses the system-filepath package, + which defines an abstract FilePath type. I've personally found this to be a much + nicer approach to work with. The package is fairly easy to follow, so I won't go into details + here. But I do want to give a few quick explanations of how to use it:

+
  • Since a FilePath is an instance of IsString, you can type + in regular strings and they will be treated properly, as long as the + OverloadedStrings extension is enabled. (I highly recommend enabling it + anyway, as it makes dealing with Text values much more pleasant.)
  • +
  • If you need to explicitly convert to or from Prelude's + FilePath, you should use the + encodeString and + decodeString, respectively. This + takes into account file path encodings.
  • +
  • Instead of manually splicing together directory names and file names with extensions, use the + operators in the Filesystem.Path.CurrentOS module, e.g. myfolder </> + filename <.> extension.
  • +
+

Cursor

+

Suppose you want to pull the title out of an XHTML document. You could do so with the + Text.XML interface we just described, using standard pattern matching on the + children of elements. But that would get very tedious, very quickly. Probably the gold standard + for these kinds of lookups is XPath, where you would be able to write /html/head/title. And that's exactly what inspired the design of the Text.XML.Cursor combinators.

+

A cursor is an XML node that knows its location in the tree; it's able to traverse + upwards, sideways, and downwards. (Under the surface, this is achieved by tying + the knot.) There are two functions available for creating cursors from + Text.XML types: fromDocument and + fromNode.

+

We also have the concept of an Axis, defined as type Axis = Cursor -> [Cursor]. It's easiest to get started by looking at + example axes: child returns zero or more cursors that are the child of the current one, parent + returns the single parent cursor of the input, or an empty list if the input is the root element, + and so on.

+

In addition, there are some axes that take predicates. element is a commonly + used function that filters down to only elements which match the given name. For example, + element "title" will return the input element if its name is "title", or an + empty list otherwise.

+

Another common function which isn't quite an axis is content :: Cursor -> + [Text]. For all content nodes, it returns the contained text; otherwise, it returns an + empty list.

+

And thanks to the monad instance for lists, it's easy to string all of these + together. For example, to do our title lookup, we would write the following program:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+            child cursor >>= element "head" >>= child
+                         >>= element "title" >>= descendant >>= content
+
+

What this says is:

+
  1. Get me all the child nodes of the root element
  2. +
  3. Filter down to only the elements named "head"
  4. +
  5. Get all the children of all those head elements
  6. +
  7. Filter down to only the elements named "title"
  8. +
  9. Get all the descendants of all those title elements. (A descendant is a child, or a + descendant of a child. Yes, that was a recursive definition.)
  10. +
  11. Get only the text nodes.
  12. +
+

So for the input document:

+
<html>
+    <head>
+        <title>My <b>Title</b></title>
+    </head>
+    <body>
+        <p>Foo bar baz</p>
+    </body>
+</html>
+
+

We end up with the output My Title. This is all well and good, but it's much + more verbose than the XPath solution. To combat this verbosity, Aristid Breitkreuz added a set of + operators to the Cursor module to handle many common cases. So we can rewrite our example as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $/ element "head" &/ element "title" &// content
+
+

$/ says to apply the axis on the right to the cursor on + the left. &/ is almost identical, but is instead used to combine + two axes together. This is a general rule in Text.XML.Cursor: operators + beginning with $ directly apply an axis, while & will combine two together. &// is used for applying an axis to all descendants.

+

Let's go for a more complex, if more contrived, example. We have a document that looks + like:

+
<html>
+    <head>
+        <title>Headings</title>
+    </head>
+    <body>
+        <hgroup>
+            <h1>Heading 1 foo</h1>
+            <h2 class="foo">Heading 2 foo</h2>
+        </hgroup>
+        <hgroup>
+            <h1>Heading 1 bar</h1>
+            <h2 class="bar">Heading 2 bar</h2>
+        </hgroup>
+    </body>
+</html>
+
+

We want to get the content of all the h1 tags which precede an + h2 tag with a class attribute of "bar". To perform this + convoluted lookup, we can write:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test2.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $// element "h2"
+               >=> attributeIs "class" "bar"
+               >=> precedingSibling
+               >=> element "h1"
+               &// content
+
+

Let's step through that. First we get all h2 elements in the document. + ($// gets all descendants of the root element.) Then we filter out only those + with class=bar. That >=> operator is actually + the standard operator from Control.Monad; yet another advantage + of the monad instance of lists. precedingSibling finds all nodes that come + before our node and share the same parent. (There is also a preceding axis which takes all elements earlier in the tree.) We then take just the + h1 elements, and then grab their content.

+ +

While the cursor API isn't quite as succinct as XPath, it has the advantages of being standard + Haskell code, and of type safety.

+

xml-hamlet

+

Thanks to the simplicity of Haskell's data type system, creating + XML content with the Text.XML API is easy, if a bit verbose. The + following code:

+
{-# LANGUAGE OverloadedStrings #-}
+import Text.XML
+import Prelude hiding (writeFile)
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" []
+        [ NodeElement $ Element "head" []
+            [ NodeElement $ Element "title" []
+                [ NodeContent "My "
+                , NodeElement $ Element "b" []
+                    [ NodeContent "Title"
+                    ]
+                ]
+            ]
+        , NodeElement $ Element "body" []
+            [ NodeElement $ Element "p" []
+                [ NodeContent "foo bar baz"
+                ]
+            ]
+        ]
+
+

produces

+
<?xml version="1.0" encoding="UTF-8"?>
+<html><head><title>My <b>Title</b></title></head><body><p>foo bar baz</p></body></html>
+
+

This is leaps and bounds easier than having to deal with an imperative, mutable-value-based API + (cough, Java, cough), but it's far from pleasant, and obscures what we're really trying to + achieve. To simplify things, we have the xml-hamlet package, which using + Quasi-Quotation to allow you to type in your XML in a natural syntax. For example, the above + could be rewritten as:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" [] [xml|
+<head>
+    <title>
+        My #
+        <b>Title
+<body>
+    <p>foo bar baz
+|]
+
+

Let's make a few points:

+
  • The syntax is almost identical to normal Hamlet, except URL-interpolation (@{...}) has been + removed. As such:
    • No close tags.
    • +
    • Whitespace-sensitive.
    • +
    • If you want to have whitespace at the end of a line, use a # at the end. At the beginning, + use a backslash.
    • +
  • +
  • An xml interpolation will return a list of Nodes. So you still need to wrap up the output in all the normal + Document and root Element constructs.
  • +
  • There is no support for the special .class and + #id attribute forms.
  • +
+

And like normal Hamlet, you can use variable interpolation and control structures. So a + slightly more complex example would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+import Data.Text (Text, pack)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Michael" 26
+    , Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Gavriella" 1
+    ]
+
+main :: IO ()
+main =
+    writeFile def "people.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" [] [xml|
+<head>
+    <title>Some People
+<body>
+    <h1>Some People
+    $if null people
+        <p>There are no people.
+    $else
+        <dl>
+            $forall person <- people
+                ^{personNodes person}
+|]
+
+personNodes :: Person -> [Node]
+personNodes person = [xml|
+<dt>#{personName person}
+<dd>#{pack $ show $ personAge person}
+|]
+
+

A few more notes:

+
  • The caret-interpolation (^{...}) takes a list of nodes, and so can easily embed + other xml-quotations.
  • +
  • Unlike Hamlet, hash-interpolations (#{...}) are not polymorphic, and can only accept Text values.
  • +
+

xml2html

+

So far in this chapter, our examples have revolved around XHTML. I've done that so far + simply because it is likely to be the most familiar form of XML for most of our readers. But + there's an ugly side to all this that we must acknowledge: not all XHTML will be correct HTML. + The following discrepancies exist:

+
  • There are some void tags (e.g., img, br) in HTML + which do not need to have close tags, and in fact are not allowed to.
  • +
  • HTML does not understand self-closing tags, so <script></script> and + <script/> mean very different things.
  • +
  • Combining the previous two points: you are free to self-close void tags, though to a browser + it won't mean anything.
  • +
  • In order to avoid quirks mode, you should start your HTML documents with a + DOCTYPE statement.
  • +
  • We do not want the XML declaration <?xml ...?> at the top of an HTML + page
  • +
  • We do not want any namespaces used in HTML, while XHTML is fully namespaced.
  • +
  • The contents of <style> and <script> + tags should not be escaped.
  • +
+

That's where the xml2html package comes into play. It provides a + ToHtml instance for Nodes, + Documents and Elements. In order to use it, just import the + Text.XML.Xml2Html module.

+
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+import Text.Blaze (toHtml)
+import Text.Blaze.Renderer.String (renderHtml)
+import Text.XML
+import Text.Hamlet.XML
+import Text.XML.Xml2Html ()
+
+main :: IO ()
+main = putStr $ renderHtml $ toHtml $ Document (Prologue [] Nothing []) root []
+
+root :: Element
+root = Element "html" [] [xml|
+<head>
+    <title>Test
+    <script>if (5 < 6 || 8 > 9) alert("Hello World!");
+    <style>body > h1 { color: red }
+<body>
+    <h1>Hello World!
+|]
+
+

Outputs: (whitespace added)

+
<!DOCTYPE HTML>
+<html>
+    <head>
+        <title>Test</title>
+        <script>if (5 < 6 || 8 > 9) alert("Hello World!");</script>
+        <style>body > h1 { color: red }</style>
+    </head>
+    <body>
+        <h1>Hello World!</h1>
+    </body>
+</html>
+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/10/yesods-monads.html b/public/blog/2011/10/yesods-monads.html new file mode 100644 index 00000000..4cdeec13 --- /dev/null +++ b/public/blog/2011/10/yesods-monads.html @@ -0,0 +1,265 @@ + Yesod's Monads +

Yesod's Monads

October 17, 2011

GravatarBy Michael Snoyman

Yesod's Monads

+

As you've read through this book, there have been a number of monads which have + appeared: Handler, Widget and something to do with Persistent + (let's call it YesodDB for now). 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 let's you make database queries.

+

So far, we've presented some very straight-forward ways to use these monads: your main + handler will run in Handler, using runDB to execute a + YesodDB query, and defaultLayout to return a + Widget, which in turn was created by calls to toWidget.

+

However, if we have a deeper understanding of these types, we can achieve some fancier + results.

+

Monad Transformers

+
Monads are like onions. Monads are not like cakes.Shrek, more or less
+

Before we get into the heart of Yesod's monads, we need to understand a bit about + monad transformers. (If you already know all about monad transformers, you can likely skip this + section.) Different monads provide different functionality: Reader allows + read-only access to some piece of data throughout a computation, Error allows + you to short-circuit computations, and so on.

+

Often times, however, you would like to be able to combine a few of these features + together. After all, why not have a computation with read-only access to some settings variable, + that could error out at any time? One approach to this would be to write a new monad like + ReaderError, but this has the obvious downside of exponential complexity: + you'll need to write a new monad for every single possible combination.

+

Instead, we have monad transformers. In addition to Reader, we have + ReaderT, which adds reader functionality to any other monad. So we could + represent our ReaderError as (conceptually):

+
type ReaderError = ReaderT Error
+
+

In order to access our settings variable, we can use the ask function. But what about short-circuiting a computation? We'd like to use + throwError, but that won't exactly work. Instead, we need to lift our call into the next monad up. In other words:

+
throwError :: errValue -> Error
+lift . throwError :: errValue -> ReaderT Error
+
+

There are a few things you should pick up here:

+
  • A transformer can be used to add functionality to an existing monad.
  • +
  • A transformer must always wrap around an existing monad.
  • +
  • The functionality available in a wrapped monad will be dependent not only on the + monad transformer, but also on the inner monad that is being wrapped.
  • +
+

A great example of that last point is the IO monad. No matter how + many layers 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

+

We've already discussed two of our transformers previously: Handler and + Widget. Just to recap, there are two special things about these + transformers:

+
  1. In order to simplify error messages, they are not actual transformers. Instead, they are + newtypes that hard-code their inner monads.
  2. +
  3. In reality they have extra type parameters for the sub and master site. As a result, the + Yesod libraries provide GHandler sub master a and GWidget sub master + a, and each site gets a pair of type synonyms type Handler = GHandler MyApp + MyApp and type Widget = GWidget MyApp My App ().
  4. +
+

In persistent, we have a typeclass called + PersistStore. This typeclass defines all of the primitive operations you can + perform on a database, like get. This typeclass essentially looks like + class (Monad (b m)) => PersistStore b m. b is the backend itself, and is in fact a monad transformer, while m is the inner monad that b wraps around. Both SQL and + MongoDB have their own instances; in the case of SQL, it looks like:

+
instance MonadBaseControl IO m => PersistBackend SqlPersist m
+
+

This means that you can run a SQL database with any underlying monad, so long as that + underlying monad supports MonadBaseControl IO, which allows you to + properly deal with exceptions in a monad stack. That basically means any transformer stack built + around IO (besides exceptional cases like ContT). + Fortunately for us, that includes both Handler and Widget. The + takeaway here is that we can layer our Persistent transformer on top of Handler + or Widget.

+ +

In order to make it simpler to refer to the relevant Persistent transformer, the + yesod-persistent package defines the YesodPersistBackend + associated type. For example, if I have a site called MyApp and it uses SQL, I + would define something like type instance YesodPersistBackend MyApp = + SqlPersist.

+

When we want to run our database actions, we'll have a SqlPersist + wrapped around a Handler or Widget. We can then use the + standard Persistent unwrap functions (like runSqlPool) to run the action and get + back a normal Handler/Widget. To automate this, we provide the + runDB function. Putting it all together, we can now run database actions inside + our handlers and widgets.

+

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 + if you look at that last paragraph again, you'll realize that's not the way things have to be. + Since a widget is a transformer on top of a handler, anything you do in a handler can be done in + a widget, including database actions. All you have to do is lift.

+

Example: Database-driven navbar

+

Let's put some of this new knowledge into action. We want to create a + Widget that generates its output based on the contents of the + database. Previously, our approach would have been to load up the data in a + Handler, and then pass that data into a Widget. + Now, we'll do the loading of data in the Widget itself. This is a boon + for modularity, as this Widget can be used in any + Handler we want, without any need to pass in the database + contents.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell, FlexibleContexts,
+QuasiQuotes, TypeFamilies, MultiParamTypeClasses, GADTs #-}
+import Yesod
+import Database.Persist.Sqlite
+import Data.Text (Text)
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Link
+    title Text
+    url Text
+    added UTCTime
+|]
+
+data Links = Links ConnectionPool
+
+mkYesod "Links" [parseRoutes|
+/ RootR GET
+/add-link AddLinkR POST
+|]
+
+instance Yesod Links
+
+instance RenderMessage Links FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+instance YesodPersist Links where
+    type YesodPersistBackend Links = SqlPersist
+    runDB db = do
+        Links pool <- getYesod
+        runSqlPool db pool
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<form method=post action=@{AddLinkR}>
+    <p>
+        Add a new link to #
+        <input type=url name=url value=http://>
+        \ titled #
+        <input type=text name=title>
+        \ #
+        <input type=submit value="Add link">
+<h2>Existing links
+^{existingLinks}
+|]
+
+existingLinks :: Widget
+existingLinks = do
+    links <- lift $ runDB $ selectList [] [LimitTo 5, Desc LinkAdded]
+    [whamlet|
+<ul>
+    $forall Entity _ link <- links
+        <li>
+            <a href=#{linkUrl link}>#{linkTitle link}
+|]
+
+postAddLinkR :: Handler ()
+postAddLinkR = do
+    url <- runInputPost $ ireq urlField "url"
+    title <- runInputPost $ ireq textField "title"
+    now <- liftIO getCurrentTime
+    runDB $ insert $ Link title url now
+    setMessage "Link added"
+    redirect RootR
+
+main :: IO ()
+main = withSqlitePool "links.db3" 10 $ \pool -> do
+    runSqlPool (runMigration migrateAll) pool
+    warpDebug 3000 $ Links pool
+
+

Pay attention in particular to the existingLinks function. + Notice how all we needed to do was apply lift to a normal + database action. And from within getRootR, we treated existingLinks like any ordinary Widget, + no special parameters at all. See the figure for the output of this app.

+
Screenshot of the navbar
+ +
+

Example: Request information

+

Likewise, you can get request information inside a Widget. Here we + can determine the sort order of a list based on a GET parameter.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
+QuasiQuotes, TypeFamilies, MultiParamTypeClasses, GADTs #-}
+import Yesod
+import Data.Text (Text)
+import Data.List (sortBy)
+import Data.Ord (comparing)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Michael" 26
+    , Person "Gavriella" 1
+    ]
+
+data People = People
+
+mkYesod "People" [parseRoutes|
+/ RootR GET
+|]
+
+instance Yesod People
+
+instance RenderMessage People FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<p>
+    <a href="?sort=name">Sort by name
+    \ | #
+    <a href="?sort=age">Sort by age
+    \ | #
+    <a href="?">No sort
+^{showPeople}
+|]
+
+showPeople :: Widget
+showPeople = do
+    msort <- lift $ runInputGet $ iopt textField "sort"
+    let people' =
+            case msort of
+                Just "name" -> sortBy (comparing personName) people
+                Just "age"  -> sortBy (comparing personAge)  people
+                _           -> people
+    [whamlet|
+<dl>
+    $forall person <- people'
+        <dt>#{personName person}
+        <dd>#{show $ personAge person}
+|]
+
+main :: IO ()
+main = warpDebug 3000 People
+
+

Once again, all we need to do is lift our normal + Handler code (in this case, runInputGet) to have + it run in our Widget.

+

Summary

+

If you completely ignore this chapter, you'll still be able to use Yesod to great benefit. The + advantage of understanding how Yesod's monads interact is to be able to produce cleaner, more + modular code. Being able to perform arbitrary actions in a Widget can be a + powerful tool, and understanding how Persistent and your Handler code interact + can help you make more informed design decisions in your app.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/11/cabal-src.html b/public/blog/2011/11/cabal-src.html new file mode 100644 index 00000000..bff64a68 --- /dev/null +++ b/public/blog/2011/11/cabal-src.html @@ -0,0 +1,34 @@ + Announcing cabal-src +

Announcing cabal-src

November 28, 2011

GravatarBy Michael Snoyman

I'm happy to announce the first release of cabal-src. cabal-src is a package intended to help solve the Cabal diamond dependency problem. In the Yesod development process- due to the large number of packages involved- we often run into "dependency hell" during development. Hopefully, this tool will allow us to fix all that by simply replacing "cabal install" with "cabal-src-install".

+

The package is now available on Hackage, and is therefore a simple "cabal install cabal-src-install" away. You can also view the code on Github.

+

The problem

+

Let's say you have three packages. foo depends on the text package, and can use any version of it. bar depends on text as well, but requires version 0.10.*. foobar depends on both of those.

+

If you upload these three packages to Hackage and install foobar, cabal will build foo and bar against the same version of text, and then build foobar against them. However, if you install these packages locally, foo will be built against the most recent version of text (currently 0.11.something), and then foobar will be unbuildable, since its dependencies depend on different versions of text.

+

You can see sample packages demonstrating the issue in the example folder.

+

This is just one example of the diamond dependency issue. When dealing with complicated systems such as Yesod, with dozens of packages that are in development, the situation becomes much worse.

+

Our solution

+

The important thing to note is that, if the packages are on Hackage, Cabal can handle the situation. The reason is that Cabal has enough information to calculate the correct versions of all dependencies to be used. So our goal is to give Cabal access to information on all dependencies, even those not yet on Hackage.

+

Instead of installing a local package with "cabal install", you can now use "cabal-src-install". This program actually calls out to "cabal install", and if that build succeeds, will perform the following steps:

+
  1. Create a source tarball via "cabal sdist"

  2. +
  3. Copy this tarball into a special location in your .cabal folder.

  4. +
  5. Update a 00-index.tar file specifically kept for cabal-src.

  6. +
  7. Update your .cabal/config file to recognize the special cabal-src folder as necessary.

  8. +
+

If you now install your "foo" and "bar" packages via "cabal-src-install", Cabal has full access to their source code. When it comes time to install foobar, Cabal can determine that foo can be recompiled with text 0.10 and will do so automatically.

+

Project status

+

This software should be considered alpha. We'll likely be using it for all Yesod development going forward, so I expect that alpha to be upgraded to beta and finally production quality in short order. All feedback is welcome!

+

Usage

+

Simply replace a call to "cabal install" with a call to "cabal-src-install". If you would like to only install the source tarball without actually installing the binary package, run it with "cabal-src-install --src-only".

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/11/got-right.html b/public/blog/2011/11/got-right.html new file mode 100644 index 00000000..70990dab --- /dev/null +++ b/public/blog/2011/11/got-right.html @@ -0,0 +1,61 @@ + What Yesod got Right +

What Yesod got Right

November 19, 2011

GravatarBy Greg Weber

Web development in Haskell is still virgin territory. Haskell is always capable of trying to copy what other languages do, but Yesod has blazed many new trails in the pursuit of finding the right way for Haskell. Yesod is a web framework plus 3 other projects: WAI, Shakespeare/Hamlet, and Persistent. When we go over this list, we start to realize that the strenght of Yesod lies in these 3 other projects and how they are easily integrated into Yesod.

+

WAI

+
  • Right +
    • backend independence (cgi, Warp, whatever comes next).
    • +
    • Share huge amounts of code between applications and frameworks at the middleware and backend layers
    • +
    • Overall this is a no-brainer to anyone experienced with using a similar concept in other programming language, and almost every programming language is converging on a similar standard concept.
    • +
    • Enumerators have the right performance characteristics
    • +
  • +
  • Wrong? +
    • Maybe a better version of enumerators will appear in the future.
    • +
  • +
+

Shakespeare/Hamlet

+
  • Right +
    • compile-time safety
    • +
    • no extra variable mapping steps required
    • +
    • automatic variable conversion based on type
    • +
    • Hamlet's DRY HTML, specifically indentation to signify closing tags
    • +
  • +
  • Wrong? +
    • currently no dependency tracking, but I patched GHC for that, and our development server handles it
    • +
    • How to put code into a template? Hamlet supports a minimal set of Haskell (we could call it logic-less), but others want the full power of Haskell in their templates.
    • +
  • +
+

Persistent Database interface

+
  • Right +
    • easy data serialization
    • +
    • easy basic query language
    • +
  • +
  • Wrong? +
    • Haskell records have namespace collisions
    • +
    • Query language is not truly universal and cannot satisfy many use cases
    • +
  • +
+

Yesod

+
  • Right +
    • routing - Discussed in the last blog post
    • +
    • declarative, succinct, and easy to use
    • +
    • easy type-safe URLs
    • +
    • widgets - bind HTML, CSS, and Javascript together
    • +
  • +
  • Wrong? +
    • ?
    • +
  • +
+

The funny thing is we really have no idea if we actually got the web framework right. Routing is only a small part of a web framework. In Yesod using templates is safe and DRY, WAI gives you flexibility, and data serializes easily to the database. There is really only one (very broadly speaking) style we can think of for compile-time templates or a WAI. But there are a lot of ways to change how a web framework operates. Widgets are definitely great, but I wouldn't be suprised if there is even a more powerful way to achieve the same result. The nice thing is that anyone else can try to build a better web framework by re-using what Yesod got right. And we have already seen new frameworks appear that leverage WAI and re-use yesod packages.

+

We are always thinking about how to make things better, and we appreciate all the constructive feedback we get, and those that start extending Yesod. Thinking about what Persistent got right, and watching some developments in the community is going to lead to some changes to Persistent that should be announced soon. Hopefully we will then have all 3 ancillary projects right and can then focus more on the web framework itself.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/0-9-4-release.html b/public/blog/2011/12/0-9-4-release.html new file mode 100644 index 00000000..49e04490 --- /dev/null +++ b/public/blog/2011/12/0-9-4-release.html @@ -0,0 +1,69 @@ + Yesod 0.9.4 Released +

Yesod 0.9.4 Released

December 26, 2011

GravatarBy Greg Weber

Happy Holidays!

+

As a present for all Yesod users we are pleased to announce the 0.9.4 release! This release features a fast production logger, improved configuration, a lot of package upgrades and bug fixes, and paves the way for a 1.0 release.

+

Features

+

production quality logger - integration with Kazu Yamamoto's fast-logger

+

In the 0.9 release we built a logging interface for Yesod. The logger code it uses to write the logs out is very simple and easy to understand. Since that release, Kazu Yamamoto released a really fast logger he has been using that we are very happy to upgrade to, appropriately named fast-logger. Kazu has written about some details of the original design and shared some of his bench-marking. This should be the fastest logger known to Haskell, and with it we implemented a production request logger. Previously there was just a development request logger focused entirely on convenience, without a thought to performance. One current limitation we will eventually improve is that the production logger doesn't log POST parameters, but the development logger still has that covered.

+

Note that we output logs to stdout by default. We expect you to pipe to syslog or the logging tool of your choice (but you can supply a different handle if you like). This keeps Yesod out of the business of dealing with log files and lets you pick a much more robust solution.

+

We still need to add the needed documentation to the logger. One nice feature that users may be unaware of is the ability to easily add file & line number information to your logging statement by using the Template Haskell versions.

+
$(logDebug) "This is a debug log message with file location information"
+
+

configuration/scaffolding/devel changes

+

Yesod generates scaffolding code for a few purposes - to do some boilerplate customization of your site based on some details that you provide, allow you to change functionality based on your application compilation flags, and also because we think you will likely want to change the scaffolding code (and it that is easier than dealing with complex configuration options). We have moved as much scaffolding code into the yesod-default package where it exists as real code as opposed to code templates in the scaffolding system . The scaffolding code now just references these default functions. This helps us maintain the code better, and will keep your boilerplate to a minimum. There is still nothing to stop you from overriding the default functions as you would have with scaffolding code, but you will probably want to copy the default function source code first.

+

We now have several techniques for configuring Yesod * YAML configuration files * cabal flags (use -fdev for a development build) corresponding to CPP defines for compile-time settings - these work extremely well if you know exactly what you want * command line arguments (-p --port).

+

By default the executable now requires an environment argument to prevent users from accidently using the wrong settings in production. Application configuration is different depending on the environment.

+

The core per-environment application settings are now maintained in the following configuration structure.

+
data ConfigSettings environment extra = ConfigSettings 
{csEnv :: environment
,csLoadExtra :: TextObject -> IO extra
,csFile :: environment -> IO FilePath
}
+

This is a type signature change in the release. Where you used to have just AppConfig, it should be changed to: AppConfig DefaultEnv ∅

+

The extra parameter allows you to define a function that will load arbitrary data into your configuration.

+

As another point of flexibility, "yesod devel" now takes an optional parameter giving it a file to be used as the "dist/devel.hs" to allow people who need it to control how their devel environment is launched.

+

Upgrade notes

+

Please see the above on the mandatory change to AppConfig. Another change to types is that you may need to add a type signature of ::Wiget where you invoke widgetFile or otherwise create widgets.

+

The below changes are not all required. We recognize that upgrading scaffolding code is painful, and this release continues to addresses that point by moving more functionality into configuration/default code in a way that will make future upgrades less painful. However, coming with us for that change does require you to undertake another effort to change your scaffolding generated code.

+

We have changed the flag system: in the cabal file there are now -flibrary-only and -fdev flags instead of a -fdevel flag. These now set a DEVELOPMENT CPP define instead of PRODUCTION.

+

When you upgrade to the latest Yesod, please note that the logger was moved to the scaffolding so that it can be switched on the #ifdef DEVELOPMENT flag. Please make sure your code looks like this in Application.hs to take advantage of logging:

+
import Yesod.Logger (Logger, logBS, flushLogger)
import Network.Wai.Middleware.RequestLogger

-- inside your with<Site> function where you would now invoke the defaultRunner function
defaultRunner (f . logWare) h
where
#ifdef DEVELOPMENT
logWare = logHandleDev (\msg -> logBS logger msg » flushLogger logger)
#else
logWare = logStdout
#endif
+

For the rest of the upgrade, you may want to do a yesod init and take a look at how the current files are generated. One important change is we have defined an Import module that is a modified Prelude that you can add your frequently used functions to.

+

Here is a diff of a public site that just upgraded There are a things in the diff that won't apply to you, but it should be fairly obvious.

+

More Changelog

+
  • Yesod +
    • yesod version now correctly uses the Cabal generated version
    • +
    • RedirectTemporary now results in a 307 for HTTP 1.1 clients (still 302 for HTTP 1.0)
    • +
  • +
  • Shakespeare/Hamlet +
    • Hamlet supports Haskell's case pattern match with $case/$of
    • +
    • use the less obscure $doctype instead of !!! to declare your doctype
    • +
    • much of Yesod's i18n code is now moved into the shakespeare-i18n package so that non-Yesod hamlet users can benefit from i18n
    • +
    • clearer error reporting when incorrect $tokens are used
    • +
  • +
  • WAI +
  • +
  • authenticate - improved facebook integration
  • +
  • monad-control - 0.3 (much improved) is now supported, and there is still support for 0.2 (which MongoDB users will be stuck on for a little bit longer).
  • +
  • persistent - bug fixes for !=. against non-nullable SQL columns and empty FilterOr/FilterAnd filters
  • +
+

infrastructure improvements

+

A big thanks to Arash Rouhani, who got another build server up and running for us!

+

We have revised the source installation scripts and instructions. Please speak up if you encounter any problems building Yesod from source. We are expecting dependency hell to be a very limited occurrence, particularly if you use virthualenv when building Yesod from source.

+

And a Happy New Year!

+

2012 is going to be a great year for Haskell! The official GHC 7.4 release will be out soon, which features some amazing new features. Much of the Yesod code base already compiles under the first 7.4 release candidates, so we will likely have 7.4 support soon after the official release.

+

We think we are ready for our 1.0 milestone. Almost all of the 1.0 features we laid out have now been implemented. One of the biggest aspects we wanted for 1.0 was API stability, and there are few disruptive API changes we want to make for the 1.0 release.

+

The biggest change we are planning on making is switching to conduits from enumerators. Although we expect that most of our users will not notice a difference, for anyone that has integrated with enumerator functionality, this will mean changes, although most changes should not be difficult.

+

Given that conduits could use more time to stabilize, we are planning on a 0.10 release very soon that will have all of the other API changes we want to make, and possibly the switch to conduits. Then we will be ready for the 1.0.

+

Please upgrade and get your feedback in. We are now starting the API changes for the 0.10 branch. For 0.10 you can look forward to a significant upgrade to a more modular and featureful Persistent. However, the new Persistent should require few, if any changes to your Persistent code.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/conduits-sink.html b/public/blog/2011/12/conduits-sink.html new file mode 100644 index 00000000..1f2edf06 --- /dev/null +++ b/public/blog/2011/12/conduits-sink.html @@ -0,0 +1,336 @@ + Conduits, part 3: Sinks +

Conduits, part 3: Sinks

December 27, 2011

GravatarBy Michael Snoyman

Conduits released!

+

Since the last blog post, I've uploaded the conduit packages to Hackage so that everyone can + play along at home. Obviously, this code is very new, and user feedback is highly appreciated. In + the same vein, it's entirely possible that there will be changes to the API. That said, a huge + amount of code has already been migrated over to conduit (besides the packages below, all of WAI, + http-conduit, and a number of XML libraries have been ported), so I feel pretty confident that + things are mostly stable.

+

The code is split into a few different pacakges:

+
conduit
+
Base package, including the Resource transformer.
+ + +
attoparsec-conduit
+
Turn attoparsec parsers into Sinks.
+ + +
blaze-builder-conduit
+
Convert a stream of Builders into a stream of ByteStrings.
+ + +
filesystem-conduit
+
Traverse folders, and convenience adapters for the system-filepath + package.
+ + +
zlib-conduit
+
Compress and decompress streams of bytes.
+ +
+

Quick Review

+

This is part 3 in the conduits series. The first two parts were:

+
  1. The Resource monad transformers
  2. +
  3. Conduits- an overview, and in-depth coverage of sources
  4. +
+

To give a basic overview: you use runResourceT to run a Resource block, which + ensures all allocated resources are freed. The conduit package always lives inside a Resource + block, which ensures that resources are freed. Sources produce data, sinks consume data, and + conduits transform data. You can fuse conduits into sources, into sinks, or with other conduits. + And finally, you can connect a source to a sink to produce a result.

+

That should be enough information to cover this post, though reading the previous posts- and + especially the second one- is highly recommended.

+

Sinks

+

A sink consumes a stream of data, and produces a result. A sink must always produce a result, + and must always produce a single result. This is encoded in the types themselves.

+

There is a Monad instance for sink, making it simple to compose multiple sinks + together into a larger sink. You can also use the built-in sink functions to perform most of your + work. Like sources, you'll rarely need to dive into the inner workings. Let's start off with an + example: getting lines from a stream of Chars (we'll assume Unix line endings + for simplicity).

+
import Data.Conduit
+import qualified Data.Conduit.List as CL
+
+-- Get a single line from the stream.
+sinkLine :: Resource m => Sink Char m String
+sinkLine = sinkState
+    id -- initial state, nothing at the beginning of the line
+    push
+    close
+  where
+    -- On a new line, return the contents up until here
+    push front '\n' =
+        return $ StateDone Nothing $ front []
+
+    -- Just another character, add it to the front and keep going
+    push front char =
+        return $ StateProcessing $ front . (char:)
+
+    -- Got an EOF before hitting a newline, just give what we have so far
+    close front = return $ front []
+
+-- Get all the lines from the stream, until we hit a blank line or EOF.
+sinkLines :: Resource m => Sink Char m [String]
+sinkLines = do
+    line <- sinkLine
+    if null line
+        then return []
+        else do
+            lines <- sinkLines
+            return $ line : lines
+
+content :: String
+content = unlines
+    [ "This is the first line."
+    , "Here's the second."
+    , ""
+    , "After the blank."
+    ]
+
+main :: IO ()
+main = do
+    lines <- runResourceT $ CL.sourceList content $$ sinkLines
+    mapM_ putStrLn lines
+
+

Running this sample produces the expected output:

+
This is the first line.
+Here's the second.
+

sinkLine demonstrates usage of the sinkState function, which + is very similar to the sourceState function we just saw. It takes three + arguments: an initial state, a push function (takes the current state and next input, and returns + a new state and result) and a close function (takes the current state and returns an output). As + opposed to sourceState- which doesn't need a close function- a sink is required + to always return a result.

+

Our push function has two clauses. When it gets a newline character, it indicates that + processing is complete via StateDone. The Nothing indicates + that there is no leftover input (we'll discuss that later). It also gives an output of all the + characters it has received. The second clause simply appends the new character to the existing + state and indicates that we are still working via StateProcessing. The close + function returns all characters.

+

sinkLines shows how we can use the monadic interface to produce new sinks. If + you replace sinkLine with getLine, this would look like + standard code to pull lines from standard input. This familiar interface should make it easy to + get up and running quickly.

+

Types

+

The types for sinks are just a bit more involved than sources. Let's have a look:

+
type SinkPush input m output = input -> ResourceT m (SinkResult input m output)
+type SinkClose m output = ResourceT m output
+
+data SinkResult input m output =
+    Processing (SinkPush input m output) (SinkClose m output)
+  | Done (Maybe input) output
+
+data Sink input m output =
+    SinkNoData output
+  | SinkData
+        { sinkPush :: SinkPush input m output
+        , sinkClose :: SinkClose m output
+        }
+  | SinkLift (ResourceT m (Sink input m output))
+
+

Whenever a sink is pushed to, it can either say it needs more data + (Processing) or say it's all done. When still processing, it must provided + updated push and close function; when done, it returns any leftover inut and the output. Fairly + straight-forward.

+

The first real "gotcha" is the three constructors for Sink. Why do we need + SinkNoData: aren't sinks all about consuming data? The answer is that we need + it to efficiently implement our Monad instance. When we use + return, we're giving back a value that requires no data in order to compute it. + We could model this with the SinkData constructor, with something like:

+
myReturn a = SinkData (\input -> return (Done (Just input) a)) (return a)
+
+

But doing so would force reading in an extra bit of input that we don't need right now, and + possibly will never need. (Have a look again at the sinkLines example.) So + instead, we have an extra constructor to indicate that no input is required. Likewise, + SinkLift is provided in order to implement an efficient + MonadTrans instance.

+

Sinks: no helpers

+

Let's try to implement some sinks on the "bare metal", without any helper functions.

+
import Data.Conduit
+import System.IO
+import Control.Monad.Trans.Resource
+import Control.Monad.IO.Class (liftIO)
+
+-- Consume all input and discard it.
+sinkNull :: Resource m => Sink a m ()
+sinkNull =
+    SinkData push close
+  where
+    push _ignored = return $ Processing push close
+    close = return ()
+
+-- Let's stream characters to a file. Here we do need some kind of
+-- initialization. We do this by initializing in a push function,
+-- and then returning a different push function for subsequent
+-- calls. By using withIO, we know that the handle will be closed even
+-- if there's an exception.
+sinkFile :: ResourceIO m => FilePath -> Sink Char m ()
+sinkFile fp =
+    SinkData pushInit closeInit
+  where
+    pushInit char = do
+        (releaseKey, handle) <- withIO (openFile fp WriteMode) hClose
+        push releaseKey handle char
+    closeInit = do
+        -- Never opened a file, so nothing to do here
+        return ()
+
+    push releaseKey handle char = do
+        liftIO $ hPutChar handle char
+        return $ Processing (push releaseKey handle) (close releaseKey handle)
+
+    close releaseKey _ = do
+        -- Close the file handle as soon as possible.
+        return ()
+
+-- And we'll count how many values were in the stream.
+count :: Resource m => Sink a m Int
+count =
+    SinkData (push 0) (close 0)
+  where
+    push count _ignored =
+        return $ Processing (push count') (close count')
+      where
+        count' = count + 1
+
+    close count = return count
+
+

Nothing is particularly complicated to implement. You should notice a common pattern here: + declaring your push and close functions in a where clause, and then + using them twice: once for the initial SinkData, and once for the + Processing constructor. This can become a bit tedious; that's why + we have helper functions.

+

Sinks: with helpers

+

Let's rewrite sinkFile and count to take advantage of the + helper functions sinkIO and sinkState, respectively.

+
import Data.Conduit
+import System.IO
+import Control.Monad.IO.Class (liftIO)
+
+-- We never have to touch the release key directly, sinkIO automatically
+-- releases our resource as soon as we return IODone from our push function,
+-- or sinkClose is called.
+sinkFile :: ResourceIO m => FilePath -> Sink Char m ()
+sinkFile fp = sinkIO
+    (openFile fp WriteMode)
+    hClose
+    -- push: notice that we are given the handle and the input
+    (\handle char -> do
+        liftIO $ hPutChar handle char
+        return IOProcessing)
+    -- close: we're also given the handle, but we don't use it
+    (\_handle -> return ())
+
+-- And we'll count how many values were in the stream.
+count :: Resource m => Sink a m Int
+count = sinkState
+    0
+    -- The push function gets both the current state and the next input...
+    (\state _ignored ->
+        -- and it returns the new state
+        return $ StateProcessing $ state + 1)
+    -- The close function gets the final state and returns the output.
+    (\state -> return state)
+
+

Nothing dramatic, just slightly shorter, less error-prone code. Using these two helper + functions is highly recommended, as it ensures proper resource management and state updating.

+

List functions

+

As easy as it is to write your own sinks, you'll likely want to take advantage of the built-in + sinks available in the Data.Conduit.List module. These provide + analogues to common list functions, like folding. (The module also has some + Conduits, like map.)

+

If you're looking for some way to practice with conduits, reimplementing the functions in the + List module- both with and without the helper functions- would be a good + start.

+

Let's look at some simple things we can make out of the built-in sinks.

+
import Data.Conduit
+import qualified Data.Conduit.List as CL
+import Control.Monad.IO.Class (liftIO)
+
+-- A sum function.
+sum' :: Resource m => Sink Int m Int
+sum' = CL.fold (+) 0
+
+-- Print every input value to standard output.
+printer :: (Show a, ResourceIO m) => Sink a m ()
+printer = CL.mapM_ (liftIO . print)
+
+-- Sum up all the values in a stream after the first five.
+sumSkipFive :: Resource m => Sink Int m Int
+sumSkipFive = do
+    CL.drop 5
+    CL.fold (+) 0
+
+-- Print each input number and sum the total
+printSum :: ResourceIO m => Sink Int m Int
+printSum = do
+    total <- CL.foldM go 0
+    liftIO $ putStrLn $ "Sum: " ++ show total
+    return total
+  where
+    go accum int = do
+        liftIO $ putStrLn $ "New input: " ++ show int
+        return $ accum + int
+
+

Connecting

+

At the end of the day, we're actually going to want to use our sinks. While we could manually + call sinkPush and sinkClose, it's tedious. For example:

+
main :: IO ()
+main = runResourceT $ do
+    res <-
+        case printSum of
+            SinkData push close -> loop [1..10] push close
+            SinkNoData res -> return res
+    liftIO $ putStrLn $ "Got a result: " ++ show res
+  where
+    start (SinkData push close) = loop [1..10] push close
+    start (SinkNoData res) = return res
+    start (SinkLift msink) = msink >>= start
+
+    loop [] _push close = close
+    loop (x:xs) push close = do
+        mres <- push x
+        case mres of
+            Done _leftover res -> return res
+            Processing push' close' -> loop xs push' close'
+
+

Instead, the recommended approach is to connect your sink to a source. Not only is this + simpler, it's less error prone, and means you have a lot of flexibility in where your data is + coming from. To rewrite the example above:

+
main :: IO ()
+main = runResourceT $ do
+    res <- CL.sourceList [1..10] $$ printSum
+    liftIO $ putStrLn $ "Got a result: " ++ show res
+
+

Connecting takes care of testing for the sink constructor (SinkData versus + SinkNoData versus SinkLift), pulling from the source, and + pushing to/closing the sink.

+

However, there is one thing I wanted to point out from the long-winded example. On the second + to last line, we ignore the leftover value of Done. This brings up the issue of + data loss. This is an important topic that has had a lot of thought put into it. + Unfortunately, we can't fully cover it yet, as we haven't discussed the main culprit in the + drama: Conduits (the type, not the package).

+

But as a quick note here, the leftover value from the Done constructor is not + always ignored. The Monad instance, for example, uses it to pass data from one + sink to the next in a binding. And in fact, the real connect operator doesn't always throw + away the leftovers. When we cover resumable sources later, we'll see that the leftover value is + put back on the buffer to allow later sinks reusing an existing source to pull the value.

+

To be continued...

+

We still have a lot to cover in conduits, though at this point you likely have enough + information to get started using them. The next big topic is Conduits. We'll see + what they are, and how they combine together with sources and sinks. Finally, we'll try to cover + the larger design decisions behind conduits, and some more advanced usages.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/conduits.html b/public/blog/2011/12/conduits.html new file mode 100644 index 00000000..98ed6d3b --- /dev/null +++ b/public/blog/2011/12/conduits.html @@ -0,0 +1,375 @@ + Conduits +

Conduits

December 25, 2011

GravatarBy Michael Snoyman

Conduits

+

Conduits are a solution to the streaming data problem. They fit the same solution space as + enumerators, in that we want to have deterministic resource handling, constant memory usage, and + composability. However, conduits have been designed based on a huge amount of experience using + enumerators in real life projects. They intend to solve the same problems in a simpler, more + robust way.

+

This post is not intended to be a detailed comparison between enumerators and conduits, though + it will be impossible to avoid some comparison. Instead, the goal is to explain the design + decisions and usage of conduits, and give enough information that a reader familiar with both + concepts can draw his/her own conclusions. In addition, the README file of the conduit repo on + Github provides some of the motivation for conduits.

+

Note that this article will assume a basic understanding of the Resource monad transformer, + which was described in a previous article. Also, conduits have not yet been + released to Hackage, though the code in question is mostly stable, and is in fact in production + use already.

+

Goals

+

We often talk vaguely about having streaming, composable data. Let's give some concrete ideas + of what we want out of this library:

+
  • A unified interface for streaming data from and piping data into various places, whether they + be files, sockets, or memory-based.
  • +
  • Some technique for modifying this stream of data in some way, such as applying HTTP chunking + to a stream of bytes of decoding a stream of bytes into a stream of characters.
  • +
  • Deterministic resource management. If we read data from a file, we want the file to be closed + immediately after an EOF is reached. We should not need to wait for any finalizer to be called. + (This is a common complaint against lazy I/O.)
  • +
  • Exception safety. All resources should be freed, even in the presence of exceptions, and even + if those exceptions are asynchronous.
  • +
  • Any part of the conduit pipeline- including the data consumer- should be able to safely + acquire scarce resources and know that they will be released. (This is a downside of the + enumerator approach, and one of our main design goals.)
  • +
  • Conduits should interoperate well with monad transformer stacks. Almost all frameworks today + use a monad stack in some way, and we want easy interoperability.
  • +
  • The developer should be free to choose the control flow of the program. (This is another + complaint against enumerator, which creates a form of "inversion of control.")
  • +
  • Finally, conduits should be simple. The majority of this article should be + understandable by almost any Haskell developer. If not, I've failed in either my writing or my + coding.
  • +
+

Conduits in Five Minutes

+

While a good understanding of the lower-level mechanics of conduits is advisable, you can get + very far without it. Let's start off with some high-level examples. Don't worry if some of the + details seem a bit magical right now. We'll cover everything in the course of this series. Let's + start with the terminology, and then some sample code.

+
Source
+
A producer of data. The data could be in a file, coming from a socket, or in memory as a + list. To access this data, we pull from the source.
+ + +
Sink
+
A consumer of data. Basic examples would be a sum function (adding up a stream of numbers + fed in), a file sink (which writes all incoming bytes to a file), or a socket. We + push data into a sink. When the sink finishes processing (we'll explain that + later), it returns some value.
+ + +
Conduit
+
A transformer of data. The simplest example is a map function, though there are many others. + Like a sink, we push data into a conduit. But instead of returning a single value + at the end, a conduit can return multiple outputs every time it is pushed to.
+ + +
Fuse
+
(Thanks to David Mazieres for the term.) A conduit can be fused with a source + to produce a new, modified source (the $= operator). For example, you could + have a source that reads bytes from a file, and a conduit that decodes bytes into text. If you + fuse them together, you would now have a source that reads text from a file. Likewise, a + conduit and a sink can fuse into a new sink (=$), and two conduits can fuse + into a new conduit (=$=).
+ + +
Connect
+
You can connect a source to a sink using the $$ operator. Doing so will + pull data from the source and push it to the sink, until either the source or sink signals that + they are "done."
+ +
+

Let's see some examples of conduit code.

+
{-# LANGUAGE OverloadedStrings #-}
+import Data.Conduit -- the core library
+import qualified Data.Conduit.List as CL -- some list-like functions
+import qualified Data.Conduit.Binary as CB -- bytes
+import qualified Data.Conduit.Text as CT
+
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Control.Monad.ST (runST)
+
+-- Let's start with the basics: connecting a source to a sink. We'll use the
+-- built in file functions to implementing efficient, constant-memory,
+-- resource-friendly file copying.
+--
+-- Two things to note: we use $$ to connect our source to our sink, and then
+-- use runResourceT.
+copyFile :: FilePath -> FilePath -> IO ()
+copyFile src dest = runResourceT $ CB.sourceFile src $$ CB.sinkFile dest
+
+
+-- The Data.Conduit.List module provides a number of helper functions for
+-- creating sources, sinks, and conduits. Let's look at a typical fold: summing
+-- numbers.
+sumSink :: Resource m => Sink Int m Int
+sumSink = CL.fold (+) 0
+
+-- If we want to go a little more low-level, we can code our sink with the
+-- sinkState function. This function takes three parameters: an initial state,
+-- a push function (receive some more data), and a close function.
+sumSink2 :: Resource m => Sink Int m Int
+sumSink2 = sinkState
+    0 -- initial value
+
+    -- update the state with the new input and
+    -- indicate that we want more input
+    (\accum i -> return $ StateProcessing (accum + i))
+    (\accum -> return accum) -- return the current accum value on close
+
+-- Another common helper function is sourceList. Let's see how we can combine
+-- that function with our sumSink to reimplement the built-in sum function.
+sum' :: [Int] -> Int
+sum' input = runST $ runResourceT $ CL.sourceList input $$ sumSink
+
+-- Since this is Haskell, let's write a source to generate all of the
+-- Fibonacci numbers. We'll use sourceState. The state will contain the next
+-- two numbers in the sequence. We also need to provide a pull function, which
+-- will return the next number and update the state.
+fibs :: Resource m => Source m Int
+fibs = sourceState
+    (0, 1) -- initial state
+    (\(x, y) -> return $ StateOpen (y, x + y) x)
+
+-- Suppose we want to get the sum of the first 10 Fibonacci numbers. We can use
+-- the isolate conduit to make sure the sum sink only consumes 10 values.
+sumTenFibs :: Int
+sumTenFibs =
+        runST -- runs fine in pure code
+      $ runResourceT
+      $ fibs
+    $= CL.isolate 10 -- fuse the source and conduit into a source
+    $$ sumSink
+
+-- We can also fuse the conduit into the sink instead, we just swap a few
+-- operators.
+sumTenFibs2 :: Int
+sumTenFibs2 =
+        runST
+      $ runResourceT
+      $ fibs
+    $$ CL.isolate 10
+    =$ sumSink
+
+-- Alright, let's make some conduits. Let's turn our numbers into text. Sounds
+-- like a job for a map...
+
+intToText :: Int -> Text -- just a helper function
+intToText = T.pack . show
+
+textify :: Resource m => Conduit Int m Text
+textify = CL.map intToText
+
+-- Like previously, we can use a conduitState helper function. But here, we
+-- don't even need state, so we provide a dummy state value.
+textify2 :: Resource m => Conduit Int m Text
+textify2 = conduitState
+    ()
+    (\() input -> return $ StateProducing () [intToText input])
+    (\() -> return [])
+
+-- Let's make the unlines conduit, that puts a newline on the end of each piece
+-- of input. We'll just use CL.map; feel free to write it with conduitState as
+-- well for practice.
+unlines' :: Resource m => Conduit Text m Text
+unlines' = CL.map $ \t -> t `T.append` "\n"
+
+-- And let's write a function that prints the first N fibs to a file. We'll
+-- use UTF8 encoding.
+writeFibs :: Int -> FilePath -> IO ()
+writeFibs count dest =
+      runResourceT
+    $ fibs
+   $= CL.isolate count
+   $= textify
+   $= unlines'
+   $= CT.encode CT.utf8
+   $$ CB.sinkFile dest
+
+-- We used the $= operator to fuse the conduits into the sources, producing a
+-- single source. We can also do the opposite: fuse the conduits into the sink. We can even combine the two.
+writeFibs2 :: Int -> FilePath -> IO ()
+writeFibs2 count dest =
+      runResourceT
+    $ fibs
+   $= CL.isolate count
+   $= textify
+   $$ unlines'
+   =$ CT.encode CT.utf8
+   =$ CB.sinkFile dest
+
+-- Or we could fuse all those inner conduits into a single conduit...
+someIntLines :: ResourceThrow m -- encoding can throw an exception
+             => Int
+             -> Conduit Int m ByteString
+someIntLines count =
+      CL.isolate count
+  =$= textify
+  =$= unlines'
+  =$= CT.encode CT.utf8
+
+-- and then use that conduit
+writeFibs3 :: Int -> FilePath -> IO ()
+writeFibs3 count dest =
+      runResourceT
+    $ fibs
+   $= someIntLines count
+   $$ CB.sinkFile dest
+
+main :: IO ()
+main = do
+    putStrLn $ "First ten fibs: " ++ show sumTenFibs
+    writeFibs 20 "fibs.txt"
+    copyFile "fibs.txt" "fibs2.txt"
+
+

Source

+

I think it's simplest to understand sources by looking at the types:

+
data SourceResult m a = Open (Source m a) a | Closed
+data Source m a = Source
+    { sourcePull :: ResourceT m (SourceResult m a)
+    , sourceClose :: ResourceT m ()
+    }
+
+

A source has just two operations on it: you can pull data from it, and you can close it (think + of closing a file handle). When you pull, you either get some data and the a new + Source (the source is still open), or nothing (the source is closed). Let's + look at some of the simplest sources:

+
import Prelude hiding (repeat)
+import Data.Conduit
+
+-- | Never give any data
+eof :: Monad m => Source m a
+eof = Source
+    { sourcePull = return Closed
+    , sourceClose = return ()
+    }
+
+-- | Always give the same value
+repeat :: Monad m => a -> Source m a
+repeat a = Source
+    { sourcePull = return $ Open (repeat a) a
+    , sourceClose = return ()
+    }
+
+

These sources are very straight-forward, since they always return the same results. + Additionally, their close records don't do anything. You might think that this is a bug: + shouldn't a call to sourcePull return Closed after it's been + closed? This isn't required, since one of the rules of sources is that they can never be + reused. In other words:

+
  • If a Source returns Open, it has provided you with a new + Source which you should use in place of the original one.
  • +
  • If it returns Closed, then you cannot perform any more operations on + it.
  • +
+

Don't worry too much about the invariant. In practice, you will almost never call + sourcePull or sourceClose yourself. In fact, you hardly + even write them yourself either (that's what sourceState and + sourceIO are for). The point is that we can make some assumptions when we + implement our sources.

+

State

+

There is something similar about the two sources mentioned above: they never change. They + always return the same value. In other words, they have no state. For almost all serious + sources, we'll need some kind of state.

+ +

The way we store state in a source is by updating the returned Source value in + the Open constructor. This is best seen with an example.

+
import Data.Conduit
+import Control.Monad.Trans.Resource
+
+-- | Provide data from the list, one element at a time.
+sourceList :: Resource m => [a] -> Source m a
+sourceList list = Source
+    { sourcePull =
+        case list of
+            [] -> return Closed -- no more data
+
+            -- This is where we store our state: by return a new
+            -- Source with the rest of the list
+            x:xs -> return $ Open (sourceList xs) x
+        , sourceClose = return ()
+        }
+
+

Each time we pull from the source, it checks the input list. If the list is empty, pulling + returns Closed, which makes sense. If the list is not empty, pulling returns + Open with both the next value in the list, and a new Source + value containing the rest of the input list.

+

sourceState and sourceIO

+

In addition to being able to manually create Sources, we also have a few + convenience functions that allow us to create most sources in a more high-level fashion. + sourceState let's you write code similar to how you would use the + State monad. You provide an initial state, your pull function is provided with + the current state, and it returns a new state and a return value. Let's use this to reimplement + sourceList.

+
import Data.Conduit
+import Control.Monad.Trans.Resource
+
+-- | Provide data from the list, one element at a time.
+sourceList :: Resource m => [a] -> Source m a
+sourceList state0 = sourceState
+    state0
+    pull
+  where
+    pull [] = return StateClosed
+    pull (x:xs) = return $ StateOpen xs x
+
+

Notice the usage of the StateClosed and StateOpen + constructors. These are very similar to Closed and Open, except + that instead of specifying the next Source to be used, you provide the next + state (here, the remainder of the list).

+

The other common activity is to perform some I/O allocation (like opening a file), registering + some cleanup action (closing that file), and having a function for pulling data from that + resource. conduit comes built-in with a sourceFile function + that gives a stream of ByteStrings. Let's write a wildly inefficient alternative + that returns a stream of characters.

+
import Data.Conduit
+import Control.Monad.Trans.Resource
+import System.IO
+import Control.Monad.IO.Class (liftIO)
+
+sourceFile :: ResourceIO m => FilePath -> Source m Char
+sourceFile fp = sourceIO
+    (openFile fp ReadMode)
+    hClose
+    (\h -> liftIO $ do
+        eof <- hIsEOF h
+        if eof
+            then return IOClosed
+            else fmap IOOpen $ hGetChar h)
+
+

Like sourceState, it uses a variant on the Open and + Closed constructors. sourceIO does a number of things for + us:

+
  • It registers the cleanup function with the ResourceT transformer, ensuring + it gets called even in the presence of exceptions.
  • +
  • It sets up the sourceClose record to release the resource immediately.
  • +
  • As soon as you return IOClosed, it will release the resource.
  • +
+

Next time

+

Everyone loves cliffhangers, right? Well, there's too much information to stick into a single + blog post, so here are the topics I'm hoping to cover in the rest of this series:

+
  • Sinks
  • +
  • Connecting sources to sinks
  • +
  • Conduits
  • +
  • The fuse operators
  • +
  • The rules of data loss
  • +
  • Buffering/resumable sources
  • +
  • Using conduits in pure code
  • +
  • Building up conduits from sinks
  • +
  • Some real-life examples
  • +
+

Remember that conduit has not yet been officially released, and some of the information in this + article may change over time. However, the core concepts are mostly solidified now.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/future-of-http-enumerator.html b/public/blog/2011/12/future-of-http-enumerator.html new file mode 100644 index 00000000..c9f93ab9 --- /dev/null +++ b/public/blog/2011/12/future-of-http-enumerator.html @@ -0,0 +1,29 @@ + The future of http-enumerator +

The future of http-enumerator

December 29, 2011

GravatarBy Michael Snoyman

My coworker Yitz Gale told me that some people were getting worried about the future of http-enumerator now that conduits are on the scene. Let's try to clear the air on what's going to be happening with it.

+

Firstly, I've already released http-conduit, which I do intend to be a replacement for http-enumerator. In fact, http-enumerator itself was one of the main motivations for creating conduits.

+
  • Due to enumerator's inability to safely allocate resources in an Iteratee, it's possible to leak sockets in some circumstances. This is an insurmountable problem with drastically changing the API.
  • +
  • One of the goals of the API design in http-enumerator was to allow the creation of an HTTP proxy when paired up with WAI. Two people- separately- spent a lot of time trying to get this to work. It seems like at the end it might be possible, with some API changes, but conduits offer a much simpler approach.
  • +
+

In other words, http-enumerator would need a breaking change to get it to work properly anyway. So instead of considering http-conduit as a totally separate package, unrelated to http-enumerator, think of it as http-enumerator 1.0. You'll need to change some of your code to get it to work with the "new version," but that's always the case. The only reason it's getting a new name is because it would be a bit silly to call something using conduits http-enumerator. By contrast, WAI is switching to conduits as well, and is keeping the same name.

+

But is this really a fair argument? After all, switching from enumerator to conduit in all your code is going to be very hard, right? Well, probably not:

+
  • If you're not using the enumerator functions (http and httpRedirect), the changes are incredibly minor, just a few updates to fix the leaking socket issue.
  • +
  • If you are using the enumerator functions, but are not using enumerators pervasively throughout your code, you can likely just switch to an equivalent conduit function. conduit was designed to have a very similar API to enumerator, complete with matching operators and similar module names.
  • +
+

That leaves one group: the people writing code using enumerator everywhere. Two things to say for that crowd:

+
  • http-enumerator isn't disappearing. It will still be available on Hackage, I'll continue putting in bug fixes for the next few months, and if someone wants to take over as maintainer, it will survive even longer.
  • +
  • It may not be practical to just switch over to conduit immediately, but I urge everyone to at least consider it. Either you'll find that you like conduit more, or you'll have some valuable feedback on how to make conduit better. After all, the goal here is to advance the state of streaming I/O in Haskell.
  • +
+

If people have questions about changes in http-conduit, or want some help migrating code, feel free to ask.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/please-use-packdeps.html b/public/blog/2011/12/please-use-packdeps.html new file mode 100644 index 00000000..f710c85c --- /dev/null +++ b/public/blog/2011/12/please-use-packdeps.html @@ -0,0 +1,27 @@ + Please use Packdeps! +

Please use Packdeps!

December 1, 2011

GravatarBy Michael Snoyman

This blog post is an explanation of what the packdeps site is, why it's useful, and how to use it most effectively. The tl;dr is: if anyone is using your packages on Hackage, please start using this site.

+

I originally created packdeps after getting lots of emails about my packages having outdated dependencies. As a religious follower of the PVP (I've since reformed a bit), my packages always had upper bounds on every package. This means that, if a new version is released, my package will refuse to work with it until I update it.

+

This is great in theory, as it means your code will never accidently break when someone releases a new package. But there is also a downside: Cabal dependency hell. Due to various issues I won't rehash here, if you depend on a package that relies on older versions of another package, Cabal will sometimes not be able to help you.

+

We have tools to try and get around Cabal dependency hell (like our recently-released cabal-src), but better yet is nipping the problem in the bud, and keeping all packages up-to-date. That's where packdeps comes in.

+

The site is very simple. You simply give it a search string, and it scours all of Hackage to find packages containing the string in the author or name fields. Once it finds those packages, it determines if there are any "restrictive upper bounds" on them. For example, if my "foo" package depends on "bar == 1.1.*", and Hackage has bar-1.2, packdeps will flag it.

+

I find it most convenient to perform a search on my name, that way I get all of the packages I work on in a single screen. For example, here is a list of my outdated packages (hopefully blank right now).

+

Now for a few less-than-obvious features:

+
  • If you add the string "(deprecated)" to a synopsis field, packdeps will ignore it.
  • +
  • If you want to see if any of your ancestor dependencies (i.e., recursive dependencies) have out-of-date dependencies, click on the relevant link on that first page, e.g. http://packdeps.haskellers.com/feed?needle=snoy&deep=on.
  • +
  • You can get an RSS feed of this information. This is what I highly recommend everyone do, so you get an alert when a package is out-of-date.
  • +
+

In addition, packdeps provides a reverse dependency list, but that is really a separate feature, and fairly self-explanatory.

+

I'm beginning to think we need to re-examine the PVP and its cost/benefit ratio, but that's a discussion for another time. But if you're planning on releasing packages to Hackage that have upper bounds, please use packdeps to help us avoid Cabal dependency hell.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/resourcet.html b/public/blog/2011/12/resourcet.html new file mode 100644 index 00000000..dcaf04dd --- /dev/null +++ b/public/blog/2011/12/resourcet.html @@ -0,0 +1,301 @@ + The Resource monad transformer +

The Resource monad transformer

December 22, 2011

GravatarBy Michael Snoyman

The Resource monad transformer

+

The Resource transformer (ResourceT) plays a vital role in proper resource + management in the conduit project. It is included within the conduit package + itself. We'll explaining ResourceT as its own entity. While some of the design + decisions clearly are biased towards conduits, ResourceT should remain a usable + tool in its own right.

+

Goals

+

What's wrong with the following code?

+
import System.IO
+
+main = do
+    output <- openFile "output.txt" WriteMode
+    input  <- openFile "input.txt"  ReadMode
+    hGetContents input >>= hPutStr output
+    hClose input
+    hClose output
+
+

If the file input.txt does not exist, then an exception will be thrown + when trying to open it. As a result, hClose output will never be called, and + we'll have leaked a scarce resource (a file descriptor). In our tiny program, this isn't a big + deal, but clearly we can't afford such waste in a long running, highly active server process.

+

Fortunately, solving the problem is easy:

+
import System.IO
+
+main =
+    withFile "output.txt" WriteMode $ \output ->
+    withFile "input.txt" ReadMode $ \input ->
+    hGetContents input >>= hPutStr output
+
+

withFile makes sure that the Handle is always closed, even in + the presence of exceptions. It also handles asynchronous exceptions. Overall, it's a great + approach to use... when you can use it. While often withFile is easy to use, + sometimes it can require restructuring our programs. And this restructuring can range from mildly + tedious to wildly inefficient.

+

Let's take enumerators for example. If you look in the documentation, there is an + enumFile function (for reading contents from a file), but no + iterFile (for writing contents to a file). That's because the flow of control + in an iteratee doesn't allow proper allocation of the Handle. Instead, in order to write to a + file, you need to allocate the Handle before entering the Iteratee, e.g.:

+
import System.IO
+import Data.Enumerator
+import Data.Enumerator.Binary
+
+main =
+    withFile "output.txt" WriteMode $ \output ->
+    run_ $ enumFile "input.txt" $$ iterHandle output
+
+

This code works fine, but imagine that, instead of simply piping data directly to the file, + there was a huge amount of computation that occurred before we need to use the output handle. We + will have allocated a file descriptor long before we needed it, and thereby locked up a scarce + resource in our application. Besides this, there are times when we can't allocate the file + before hand, such as when we won't know which file to open until we've read from the input + file.

+

One of the stated goals of conduits is to solve this problem, and it does so via + ResourceT. As a result, the above program can be written in conduit as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Data.Conduit
+import Data.Conduit.Binary
+
+main = runResourceT $ sourceFile "input.txt" $$ sinkFile "output.txt"
+
+

How it Works

+

There are essentially three base functions on ResourceT, and then a bunch of + conveniences thrown on top. The first function is:

+
register :: IO () -> ResourceT IO ReleaseKey
+
+ +

This function registers a piece of code that it asserts must be run. It gives back a + ReleaseKey, which is used by the next function:

+
release :: ReleaseKey -> ResourceT IO ()
+
+

Calling release on a ReleaseKey immediately performs the + action you previously registered. You may call release on the same + ReleaseKey as many times as you like; the first time it is called, it + unregisters the action. This means you can safely register an action like a memory + free, and have no concerns that it will be called twice.

+

Eventually, we'll want to exit our special ResourceT. To do so, we use:

+
runResourceT :: ResourceT IO a -> IO a
+
+

This seemingly innocuous function is where all the magic happens. It runs through all of the + registered cleanup actions and performs them. It is fully exception safe, meaning the cleanups + will be performed in the presence of both synchronous and asynchronous exceptions. And as + mentioned before, calling release will unregister an action, so there is no + concern of double-freeing.

+

Finally, as a convenience, we provide one more function for the common case of allocating a + resource and registering a release action:

+
with :: IO a -- ^ allocate
+     -> (a -> IO ()) -- ^ free resource
+     -> ResourceT IO (ReleaseKey, a)
+
+

So, to rework our first buggy example to use ResourceT, we would write:

+
import System.IO
+import Control.Monad.Trans.Resource
+import Control.Monad.Trans.Class (lift)
+
+main = runResourceT $ do
+    (releaseO, output) <- with (openFile "output.txt" WriteMode) hClose
+    (releaseI, input)  <- with (openFile "input.txt"  ReadMode)  hClose
+    lift $ hGetContents input >>= hPutStr output
+    release releaseI
+    release releaseO
+
+

Now there is no concern of any exceptions preventing the releasing of resources. We could skip + the release calls if we want to, and in an example this small, it would not make + any difference. But for larger applications, where we want processing to continue, this ensures + that the Handles are freed as early as possible, keeping our scarce resource + usage to a minimum.

+

Some Type Magic

+

As alluded to, there's a bit more to ResourceT than simply running in + IO. Let's cover some of the things we need from this underlying + Monad.

+
  • Mutable references to keep track of the registered release actions. You might think we could + just use a StateT transformer, but then our state wouldn't survive + exceptions.
  • +
  • We only want to register actions in the base monad. For example, if we have a + ResourceT (WriterT [Int] IO) stack, we only want to register + IO actions. This makes it easy to lift our stacks around (i.e., add an extra + transformer to the middle of an existing stack), and avoids confusing issues about the threading + of other monadic side-effects.
  • +
  • Some way to guarantee an action is performed, even in the presence of exceptions. This boils + down to needing a bracket-like function.
  • +
+

For the first point, we define a new typeclass to represent monads that have mutable + references:

+
class Monad m => HasRef m where
+    type Ref m :: * -> *
+    newRef' :: a -> m (Ref m a)
+    readRef' :: Ref m a -> m a
+    writeRef' :: Ref m a -> a -> m ()
+    modifyRef' :: Ref m a -> (a -> (a, b)) -> m b
+    mask :: ((forall a. m a -> m a) -> m b) -> m b
+    mask_ :: m a -> m a
+    try :: m a -> m (Either SomeException a)
+
+

We have an associated type to signify what the reference type should be. (For fans of fundeps, + you'll see in the next section that this has to be an associated type.) Then we provide a + number of basic reference operations. Finally, there are some functions to help with exceptions, + which are needed to safely implement the functions described in the last section. The instance + for IO is very straight-forward:

+
instance HasRef IO where
+    type Ref IO = I.IORef
+    newRef' = I.newIORef
+    modifyRef' = I.atomicModifyIORef
+    readRef' = I.readIORef
+    writeRef' = I.writeIORef
+    mask = E.mask
+    mask_ = E.mask_
+    try = E.try
+
+

However, we have a problem when it comes to implementing the ST instance: + there is no way to deal with exceptions in the ST monad. As a result, + mask, mask_ and try are given default + implementations that do no exception checking. This gives rise to the first word of warning: + operations in the ST monad are not exception safe. You should not be allocating scarce + resources in ST when using ResourceT. You might be wondering why bother with + ResourceT at all then for ST. The answer is that there is a + lot you can do with conduits without allocating scarce resources, and ST is a + great way to do this in a pure way. But more on this later.

+

Now onto point 2: we need some way to deal with this base monad concept. Again, we use an + associated type (again explained in the next section). Our solution looks something like:

+
class (HasRef (Base m), Monad m) => Resource m where
+    type Base m :: * -> *
+
+    resourceLiftBase :: Base m a -> m a
+
+

But we forgot about point 3: some bracket-like function. So we need one more + method in this typeclass:

+
resourceBracket_ :: Base m a -> Base m b -> m c -> m c
+
+

The reason the first two arguments to resourceBracket_ (allocation and + cleanup) live in Base m instead of m is that, in + ResourceT, all allocation and cleanup lives in the base monad.

+

So on top of our HasRef instance for IO, we now need a + Resource instance as well. This is similarly straight-forward:

+
instance Resource IO where
+    type Base IO = IO
+    resourceLiftBase = id
+    resourceBracket_ = E.bracket_
+
+

We have similar ST instances, with resourceBracket_ having no + exception safety. The final step is dealing with monad transformers. We don't need to provide a + HasRef instance, but we do need a Resource instance. The + tricky part is providing a valid implementation of resourceBracket_. For this, + we use some functions from monad-control:

+
instance (MonadTransControl t, Resource m, Monad (t m))
+        => Resource (t m) where
+    type Base (t m) = Base m
+
+    resourceLiftBase = lift . resourceLiftBase
+    resourceBracket_ a b c =
+        control' $ \run -> resourceBracket_ a b (run c)
+      where
+        control' f = liftWith f >>= restoreT . return
+
+

For any transformer, its base is the base of its inner monad. Similarly, we lift to the base by + lifting to the inner monad and then lifting to the base from there. The tricky part is the + implemetnation of resourceBracket_. I will not go into a detailed explanation, + as I would simply make a fool of myself.

+

Definition of ResourceT

+

We now have enough information to understand the definition of ResourceT:

+
newtype ReleaseKey = ReleaseKey Int
+
+type RefCount = Int
+type NextKey = Int
+
+data ReleaseMap base =
+    ReleaseMap !NextKey !RefCount !(IntMap (base ()))
+
+newtype ResourceT m a =
+    ResourceT (Ref (Base m) (ReleaseMap (Base m)) -> m a)
+
+

We see that ReleaseKey is simply an Int. If you skip a few + lines down, this will make sense, since we're using an IntMap to keep track of + the registered actions. We also define two type synonyms: RefCount and + NextKey. NextKey keeps track of the most recently assigned + value for a key, and is incremented each time register is called. We'll touch on + RefCount later.

+

The ReleaseMap is three pieces of information: the next key and the reference + count, and then the map of all registered actions. Notice that ReleaseMap takes + a type parameter base, which states which monad release actions must live + in.

+

Finally, a ResourceT is essentially a ReaderT that keeps a + mutable reference to a ReleaseMap. The reference type is determined by the base + of the monad in question, as is the cleanup monad. This is why we need to use associated + types.

+

The majority of the rest of the code in the Control.Monad.Trans.Resource + module is just providing instances for the ResourceT type.

+

Other Typeclasses

+

There are three other typeclasses provided by the module:

+
ResourceUnsafeIO
+
Any monad which can lift IO actions into it, but that this may be + considered unsafe. The prime candidate here is ST. Care should be taken to + only lift actions which do not acquire scarce resources and which don't "fire the missiles." In + other words, all the normal warnings of unsafeIOToST apply.
+ + +
ResourceThrow
+
For actions that can throw exceptions. This automatically applies to all + IO-based monads. For ST-based monads, you can use the + supplied ExceptionT transformer to provide exception-throwing capabilities. + Some functions in conduit, for example, will require this (e.g., text decoding).
+ + +
ResourceIO
+
A convenience class tying together a bunch of other classes, included the two mentioned + above. This is purely for convenience; you could achieve the same effect without this type + class, you'd just have to do a lot more typing.
+ +
+

Forking

+

It would seem that forking a thread would be inherently unsafe with ResourceT, + since the parent thread may call runResourceT while the child thread is still + accessing some of the allocated resources. This is indeed true, if you use the normal + forkIO function.

+ +

In order to solve this, ResourceT includes reference counting. When you fork a + new thread via resourceForkIO, the RefCount value of the + ReleaseMap is incremented. Every time runResourceT is called, + the value is decremented. Only when the value hits 0 are all the release actions called.

+

Convenience Exports

+

In addition to what's been listed so far, there are a few extra functions exported (mostly) for + convenience.

+
  • newRef, writeRef, and readRef wrap up the + HasRef versions of the functions and allow them to run in any + ResourceT.
  • +
  • withIO is essentially a type-restricted version of with, + but working around some of the nastiness with types you would otherwise run into. In general: + you'll want to use withIO when writing IO code.
  • +
  • transResourceT let's you modify which monad your ResourceT is running in, + assuming it keeps the same + base.
    transResourceT :: (Base m ~ Base n)
    +               => (m a -> n a)
    +               -> ResourceT m a
    +               -> ResourceT n a
    +transResourceT f (ResourceT mx) = ResourceT (\r -> f (mx r))
    +
  • +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/variable-naming-context.html b/public/blog/2011/12/variable-naming-context.html new file mode 100644 index 00000000..99139cbe --- /dev/null +++ b/public/blog/2011/12/variable-naming-context.html @@ -0,0 +1,24 @@ + Good variable naming is context dependent +

Good variable naming is context dependent

December 21, 2011

GravatarBy Greg Weber

There were some relatively recent discussions on Reddit about variable naming conventions. In my observations, Haskellers tend to go much to far to the extreme of very brief and succinct names. One reason why is because there are very appropriate times for short and succinct names, such as:

+
  • generic code
  • +
  • standards
  • +
  • ease of writing
  • +
+

List code that is truly generic can operate on anything, so using 'x:xs' for variable and 'a' for a type variable is about as specific as we can get. However, if your variable does not represent a generic type, it is very likely 'a' or 'x' is not the best name for it.

+

There are certain standards, like 'm' for a Monad that would be overly-short names if they weren't standards. One universal programming standard is 'i' for an index. If we didn't follow these short-cut existing standards, we would end up with code that is harder to write, but also possibly code that is harder to read.

+

Ease of writing code is a subtle point, and has to do with the symbols a library exposes. We must write code keeping in mind 2 points of view - the library author, and the library consumer. The library author must think about what symbols they are asking the library consumer to use. The more frequently a symbol is written, the shorter it should be. The less frequently a symbol is written, the better a more descriptive and longer name is. These concerns should come first before the concern of the library author's own ease of writing their own library. Underneath what the library exports, the library author has a lot more freedom of how to structure their code.

+

The library author/consumer split manifests itself in many ways. Internally, a library can use whatever types it wants, but it needs to be very concerned about readable error messages for the consumer of the functions it exposes.

+

One big reason for poorly named (short or otherwise) symbols is because naming is not easy! Calling a variable x, tmp, or a is a way to get it over with quickly. But the only way to get better at naming is to put a little more effort into it - it is a skill I am always trying to improve.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2011/12/yesod-scaffolded-site.html b/public/blog/2011/12/yesod-scaffolded-site.html new file mode 100644 index 00000000..f1b7c12e --- /dev/null +++ b/public/blog/2011/12/yesod-scaffolded-site.html @@ -0,0 +1,245 @@ + Scaffolding and the Site Template +

Scaffolding and the Site Template

December 9, 2011

GravatarBy Michael Snoyman

Scaffolding and the Site Template

+

So you're tired of running small examples, and ready to write a real site? Then you're at + the right chapter. Even with the entire Yesod library at your fingertips, there are + still a lot of steps you need to go through to get a production-quality site setup:

+
  • Config file parsing
  • +
  • Signal handling (*nix)
  • +
  • More efficient static file serving
  • +
  • A good file layout
  • +
+

The scaffolded site is a combination of many Yesoders' best practices combined together + into a ready-to-use skeleton for your sites. It is highly recommended for all sites. + This chapter will explain the overall structure of the scaffolding, how to use it, and + some of its less-than-obvious features.

+

How to Scaffold

+

The yesod package installs both a library and an + executable (conveniently named yesod as well). This executable + provides a few commands (run yesod by itself to get a list). In order to + generate a scaffolding, the command is yesod init. This will start a + question-and-answer process where you get to provide basic details (your name, the project name, + etc). After answering the questions, you will have a site template in a subfolder with the name + of your project.

+

The most important of these questions is the database backend. You get four choices here: + SQLite, PostgreSQL, MongoDB, and tiny. tiny is not a database backend; instead, it is specifying + that you do not want to use any database. This option also turns off a few extra dependencies, + giving you a leaner overall site. The remainder of this chapter will focus on the scaffoldings + for one of the database backends. There will be minor differences for the tiny backend.

+

After creating your files, the scaffolder will print a message about getting started. + It gives two sets of options for commands: one using cabal, and the other + using cabal-dev. cabal-dev is basically a wrapper around + cabal which causes all dependencies to be built in a sandbox. Using it is a good way to ensure + that installing other packages will not break your site setup. It is strongly recommended. If you + don't have cabal-dev, you can install it by running cabal + install cabal-dev.

+

Note that you really do need to use the cabal install + (or cabal-dev install) command. Most likely, you do not yet have all + the dependencies in place needed by your site. For example, none of the database backends, nor + the Javascript minifier (hjsmin) are installed when installing the + yesod package.

+

Finally, to launch your development site, you would use yesod devel (or + yesod --dev devel). This site will automatically reload whenever you change + your code.

+

File Structure

+

The scaffolded site is built as a fully cabalized Haskell package. In addition to + source files, config files, templates, and static files are produced as well.

+

Cabal file

+

Whether directly using cabal, or indirectly using yesod devel, building your code will always go through the cabal file. If + you open the file, you'll see that there are both library and executable blocks. Only one of + these is built at a time, depending on the value of the library-only + flag. If library-only is turned on, then the library is built, which + is how yesod devel calls your app. Otherwise, the executable is + built.

+

The library-only flag should only be used by + yesod devel; you should never be explicitly passing it into + cabal. There is an additional flag, dev, that + allows cabal to build an executable, but turns on some of the same features as + the library-only flag, i.e., no optimizations and reload versions of the Shakespearean + template functions.

+

In general, you will build as follows:

+
  • When developing, use yesod devel exclusively.
  • +
  • When building a production build, perform cabal clean + && cabal configure && cabal build. This will produce an + optimized executable in your dist folder.
  • +
+

You'll also notice that we specify all language extensions in the cabal file. The + extensions are specified twice: once for the executable, and once for the + library. If you add any extensions to the list, add it to both places.

+

You might be surprised to see the NoImplicitPrelude extension. We turn this + on since the site includes its own module, Import, with a few changes to the + Prelude that make working with Yesod a little more convenient.

+

The last thing to note is the exported-modules list. If you add any modules to your + application, you must update this list to get yesod devel to work correctly. + Unfortunately, neither Cabal nor GHC will give you a warning if you forgot to make this + update, and instead you'll get a very scary-looking error message from yesod devel.

+

Routes and entities

+

Multiple times in this book, you've seen a comment like "We're declaring our routes/entities + with quasiquotes for convenience. In a production site, you should use an external file." The + scaffolding uses such an external file.

+

Routes are defined in config/routes, and entities in + config/models. They have the exact same syntax as the quasiquoting + you've seen throughout the book, and yesod devel knows to automatically + recompile the appropriate modules when these files change.

+

The models files is referenced by Model.hs. You are free to declare whatever you like in this file, but here are some + guidelines:

+
  • Any data types used in entities + must be imported/declared in Model.hs, above the persistFile call.
  • +
  • Helper utilities should either be declared in Import.hs + or, if very model-centric, in a file within the Model folder and + imported into Import.hs.
  • +
+

Foundation and Application modules

+

The mkYesod function which we have used throughout the book declares a few + things:

+
  • Route type
  • +
  • Route render function
  • +
  • Dispatch function
  • +
+

The dispatch function refers to all of the handler functions, and therefore all of those must + either be defined in the same file as the dispatch function, or be imported by the dispatch + function.

+

Meanwhile, the handler functions will almost certainly refer to the route type. Therefore, + they must be either in the same file where the route type is defined, or must import that + file. If you follow the logic here, your entire application must essentially live in a single + file!

+

Clearly this isn't what we want. So instead of using mkYesod, the scaffolding + site uses a decomposed version of the function. Foundation calls + mkYesodData, which declares the route type and render function. Since it does + not declare the dispatch function, the handler functions need not be in scope. + Import.hs imports Foundation.hs, and all the handler modules + import Import.hs.

+

In Application.hs, we call mkYesodDispatch, which creates our + dispatch function. For this to work, all handler functions must be in scope, so be sure to add an + import statement for any new handler modules you create.

+

Other than that, Application.hs is pretty simple. It provides + two functions: withDevelAppPort is used by yesod + devel to launch your app, and getApplication is used by the + executable to launch.

+

Foundation.hs is much more exciting. It:

+
  • Declares your foundation datatype
  • +
  • Declares a number of instances, such as Yesod, + YesodAuth, and YesodPersist
  • +
  • Imports the messages files. If you look for the line starting with mkMessage, you will see that it specifies the folder containing the messages + (messages) and the default language (en, for English).
  • +
+

This is the right file for adding extra instances for your foundation, such as + YesodAuthEmail or YesodBreadcrumbs.

+

We'll be referring back to this file later, as we discussed some of the special + implementations of Yesod typeclass methods.

+

Import

+

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'm always adding the same five import statements (Data.Text, + Control.Applicative, etc) to every handler module.
  • +
  • I want to make sure I never use some evil function (head, + readFile, ...) from Prelude.
  • +
+

The solution is to turn on the NoImplicitPrelude language extension, + re-export the parts of Prelude we want, add in all the other stuff we want, + define our own functions as well, and then import this file in all handlers.

+

Handler modules

+

Handler modules should go inside the Handler folder. The site + template includes one module: Handler/Root.hs. How you split up your handler + functions into individual modules is your decision, but a good rule of thumb is:

+
  • Different methods for the same route should go in the same file, e.g. + getBlogR and postBlogR.
  • +
  • Related routes can also usually go in the same file, e.g., + getPeopleR and getPersonR.
  • +
+

Of course, it's entirely up to you. When you add a new handler file, make sure you do the + following:

+
  • Add it to version control (you are using version control, right?).
  • +
  • Add it to the cabal file.
  • +
  • Add it to the Application.hs file.
  • +
  • Put a module statement at the top, and an import Import line below it.
  • +
+ +

widgetFile

+

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.

+

If you have a handler function:

+
getRootR = defaultLayout $(widgetFile "homepage")
+
+

, Yesod will look for the following files:

+
  • templates/homepage.hamlet
  • +
  • templates/homepage.lucius
  • +
  • templates/homepage.cassius
  • +
  • templates/homepage.julius
  • +
+

If any of those files are present, they will be automatically included in the output.

+ +

defaultLayout

+

One of the first things you're going to want to customize is the look of your site. The layout + is actually broken up into two files:

+
  • templates/default-layout-wrapper.hamlet contains just the basic shell of + a page. This file is interpreted as plain Hamlet, not as a Widget, and therefore cannot refer + to other widgets, embed i18n strings, or add extra CSS/JS.
  • +
  • templates/default-layout.hamlet is where you would put the bulk of your + page. You must remember to include the widget value in the page, as that + contains the per-page contents. This file is interpreted as a Widget.
  • +
+

Also, since default-layout is included via the widgetFile function, + any Lucius, Cassius, or Julius files named default-layout.* will + automatically be included as well.

+

Static files

+

The scaffolded site automatically includes the static file subsite, optimized for + serving files that will not change over the lifetime of the current build. What this means is + that:

+
  • When your static file identifiers are generated (e.g., + static/mylogo.png becomes mylogo_png), a query-string + parameter is added to it with a hash of the contents of the file. All of this happens at compile + time.
  • +
  • When yesod-static serves your static files, it sets expiration + headers far in the future, and incldues an etag based on a hash of your content.
  • +
  • Whenever you embed a link to mylogo_png, the rendering includes the + query-string parameter. If you change the logo, recompile, and launch your new app, the query + string will have changed, causing users to ignore the cached copy and download a new + version.
  • +
+

Additionally, you can set a specific static root in your + Settings.hs file to serve from a different domain name. This has the + advantage of not requiring 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 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:

+
  1. Caching works properly.
  2. +
  3. Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with + the same hash already exists.
  4. +
+

Finally, all of your Javascript is automatically minified via hjsmin.

+

Conclusion

+

The purpose of this chapter was not to explain every line that exists in the scaffolded site, + but instead to give a general overview to how it works. The best way to become more familiar with + it is to jump right in and start writing a Yesod site with it.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/aosa-chapter.html b/public/blog/2012/01/aosa-chapter.html new file mode 100644 index 00000000..ac1e715f --- /dev/null +++ b/public/blog/2012/01/aosa-chapter.html @@ -0,0 +1,626 @@ + AOSA Chapter on Yesod +

AOSA Chapter on Yesod

January 17, 2012

GravatarBy Michael Snoyman

AOSA Chapter on Yesod

+

The upcoming Architecture of Open Source Applications, volume 2 will include a chapter on Yesod. A big thanks to Amy Brown and Greg Wilson for including us. Here's a sneak peek for the impatient.

+

Introduction

+

Yesod is a web framework written in the Haskell programming language. While many + popular web frameworks exploit the dynamic nature of their host languages, Yesod exploits the static nature of Haskell to produce safer, faster code.

+

Development began about two years ago, and has been going strong ever since. Yesod cut its + teeth on real life projects, with all of its initial features borne out of an actual, real life + need. At first, development was almost entirely a one-man show. After about a year of development, the community efforts kicked + in, and Yesod has since blossomed into a thriving open-source project.

+

During the embryonic phase, when Yesod was incredibly ephemeral and ill-defined, it + would have been counter-productive to try and get a team to work on it. By the time it stabilized + enough to be useful to others, it was the right time to find out the downsides to some of the + decisions that had been made. Since then, we have made major changes to the user-facing API to + make it more useful, and are quickly solidifying a 1.0 release.

+

The question you may ask is: why another web framework? Let's instead redirect to a + different question: why use Haskell? It seems that most of the world is happy with one of two + styles of language:

+
  • Statically typed languages, like Java, C# and C++. These languages provide speed and type + safety, but are more cumbersome to program with.
  • +
  • Dynamically typed languages, like Ruby and Python. These languages greatly increase + productivity (at least in the short run), but run slowly and have very little support from the + compiler to ensure correctness. (The solution to this last point is unit testing. We'll get to + that later.)
  • +
+

This is a false dichotomy. There's no reason why statically typed languages need to be + so clumsy. Haskell is able to capture a huge amount of the expressivity of Ruby and Python, while + remaining a strongly typed language. In fact, Haskell's type system catches many more bugs than + Java and its ilk. Null pointer exceptions are completely eliminated; immutable data structures + simplify reasoning about your code and simplify parallel and concurrent programming.

+

So why Haskell? It is an efficient, developer-friendly language which provides many + compile-time checks of program correctness.

+

The goal of Yesod is to extend Haskell's strengths into web development. Yesod + strives to make your code as concise as possible. As much as possible, every line of your code is + checked for correctness at compile time. Instead of requiring large libraries of unit tests to + test basic properties, the compiler does it all for you. Under the surface, Yesod uses as many + advanced performance techniques as we can muster to make your high-level code fly.

+

Compared to other frameworks

+

In general terms, Yesod is more similar than different when compared to the leading frameworks, + such as Rails and Django. It generally follows the Model-View-Controller (MVC) paradigm, has a + templating system that separates view from logic, provides an Object Relational Mapping (ORM) + system, and has a front controller approach to routing.

+

The devil is in the details. Yesod strives to push as much error catching to the + compile phase instead of runtime, and to automatically catch both bugs and security flaws through + the type system. While Yesod tries to maintain a user-friendly, high-level API, it uses a number + of newer techniques from the functional programming world to achieve high performance, and is not + afraid to expose these internals to developers.

+

The main architectural challenge in Yesod is balancing these two seemingly conflicting + goals. For example, there is nothing revolutionary about Yesod's approach to routing (called + type-safe URLs). + Historically, implementing such a solution was a tedious, error-prone process. Yesod's innovation + is to use Template Haskell (a form of code generation) to automate the boilerplate required to + bootstrap the process. Similarly, type-safe HTML has been around for a long while; Yesod tries to + keep the developer-friendly aspect of common template languages while keeping the power of type + safety.

+

Web Application Interface

+

A web application needs some way to communicate with a server. One possible approach is to bake + the server directly into the framework, but doing so necessarily limits your options for + deployment and leads to poor interfaces. Many languages have created standard interfaces to + address this issue: Python has WSGI and Ruby has Rack. In Haskell, we have WAI.

+

WAI is not intended to be a high level interface. It has two specific goals: + generality and performance. By staying general WAI has been able to support backends for + everything from standalone servers to old school CGI and even works directly with Webkit to + produce faux desktop applications. The performance side will introduce us to a number of the cool + features of Haskell.

+
Overall structure of a Yesod application
+ +
+

Datatypes

+

One of the biggest advantages of Haskell - and one of the things we make the most use of in + Yesod - is strong static typing. Before we begin to write the code for how to solve something, we + need to think about what the data will look like. WAI is a perfect example of this paradigm. The + core concept we want to express is that of an application. An application's most basic expression + is a function that takes a request and returns a response. In Haskell lingo: +

type Application = Request -> Response

+

This just raises the question: what do Request and Response look like? A Request has a + number of pieces of information, but the most basic are the requested path, query string, request + headers, and request body. And a Response has just three components: a status code, response + headers and response body.

+

How do we represent something like a query string? Haskell keeps a strict separation between + binary and textual data. The former is represented by ByteString, the latter by Text. Both are + highly optimized datatypes that provide a high level, safe API. In the case of query string we + store the raw bytes transferred over the wire as a ByteString and the parsed, decoded values as + Text.

+

Streaming

+

A ByteString represents a single memory buffer. If we were to naively use a plain ByteString + for holding the entire request or response bodies, our applications could never scale to large + requests or responses. Instead, we use a technique called enumerators, very similar in concept to + generators in Python. Our application becomes a consumer of a stream of ByteStrings representing + the incoming request body, and a producer of a separate stream for the response.

+

We now need to slightly revise our definition of an application. An application will + take a request value, containing headers, query string, etc, and will consume a stream of + ByteStrings, producing a Response. So the revised definition of an Application + is:

type Application = Request -> Iteratee ByteString IO Response
The + IO simply explains what types of side effects an application can perform. In the case of IO, it + can perform any kind of interaction with the outside world, an obvious necessity for the vast + majority of web applications.

+

Builder

+

The trick in our arsenal is how we produce our response buffers. We have a two + competing desires here: minimizing system calls, and minimizing buffer copies. On the one hand, + we want to minimize system calls for sending data over the socket. To do this we need to store + outgoing data in a buffer. However, if we make this buffer too large, we will exhaust our memory + and slow down the application's response time. On the other hand, we want to minimize the number + of times data is copied between buffers, preferably copying just once from the source to + destination buffer.

+

Haskell's solution is the builder. A builder is an instruction for how to fill a + memory buffer, such as place the five bytes "hello" in the next open position. Instead of passing + a stream of memory buffers to the server, a WAI application passes a stream of these + instructions. The server takes the stream and uses it to fill up optimally sized memory buffers. + As each buffer is filled, the server makes a system call to send the data over over the wire and + then starts filling up the next buffer.

+

In theory, this kind of optimization could be performed in the application itself. + However, by encoding this approach in the interface itself, we are able to simply prepend the + response headers to the response body. The result is that, for small to medium sized responses, + the entire response can be sent with a single system call and memory is copied only once.

+

Handlers

+

Now that we have an application, we need some way to run it. In WAI parlance, this is a + handler. WAI has some basic, standard handlers, such as a standalone server (Warp, + discussed below), FastCGI, SCGI and CGI. This spectrum allows WAI applications to be run on + anything from dedicated servers down to shared hosting. But in addition to these, WAI has some + more interesting backends:

Webkit
+
This backend embeds a Warp server and calls out to QtWebkit. By launching a server, + followed by launching a new standalone browser window, we have faux desktop applications.
+ + +
Launch
+
This is a slight variant on Webkit. Having to deploy the Qt and Webkit libraries can be a + bit burdensome, so instead we just launch the user's default browser.
+ + +
Test
+
Even testing counts as a handler. After all, testing is simply the act of running + an application and inspecting the responses.
+ +

+

Most developers will likely use Warp. It is lightweight enough to be used for + testing. It requires no config files, no folder hierarchy and no long-running, administrator + owned process. It's a simple library that gets compiled into your application or run via the + Haskell interpreter. On the flip side, Warp is an incredibly fast server, with protection from + all kinds of attack vectors, such as slow loris and infinite headers. Warp can be the only web + server you need, though it is also quite happy to sit behind a reverse HTTP proxy.

+

The PONG benchmark measures the requests per second of various servers for the 4-byte response + body "PONG". In this graph, Yesod is measured as a framework on top of Warp. As can be seen, the + Haskell servers (Warp, Happstack and Snap) lead the pack.

+
Warp PONG Benchmark
+ +
+

Most of the reasons for Warp's speed have already been spelled out in the overall + description of WAI: enumerators, builders and packed datatypes. The last piece in the puzzle is + from the Glasgow Haskell Compiler's (GHC's) multithreaded runtime. GHC, Haskell's flagship + compiler, has light-weight green threads. Unlike system threads, it is possible to spin up + thousands of these without serious performance hits. Therefore, in Warp, each connection is + handled by its own green thread.

+

The next trick is asynchronous I/O. Any web server hoping to scale to tens of thousands of + requests per second will need some type of asynchronous communication. In most languages, this + involves complicated programming involving callbacks. GHC lets us cheat: we program as if we're + using a synchronous API, and GHC automatically switches between different green threads waiting + for activity.

+

Under the surface, GHC uses whatever system is provided by the host operating system, + such as kqueue, epoll and select. This gives us all the performance of an + event-based IO system, without worrying about cross-platform issues or writing in a + callback-oriented way.

+

Middleware

+

In between handlers and applications, we have middlewares. Technically, a middleware + is an application transformer: it takes one application, and returns a new + one. This is defined as type Middleware = Application -> + Application. The best way to understand the purpose of a middleware is to look at some + common ones:

  • gzip automatically compresses the response from an application.
  • +
  • jsonp automatically converts JSON responses to JSON-P + responses when the client provided a callback parameter.
  • +
  • autohead will generate appropriate HEAD responses based on the GET response + of an application.
  • +
  • debug will print debug information to the console or a log on each + request.
  • +

+

The theme here is to factor out common code from applications and let it be shared easily. Note + that, based on the definition of a middleware, we can easily stack these things up. The general + workflow of a middleware is:

  1. Take the request value and apply some modifications.
  2. +
  3. Pass the modified request to the application and receive a response.
  4. +
  5. Modify the response and return it to the handler.
  6. +
In the case of stacked middlewares, instead of passing to the application or handler, the + middleware will actually be passing to the inner and outer middlewares, respectively.

+

wai-test

+

No amount of static typing will obviate the need for testing. We all know that + automated testing is a necessity for any serious applications. wai-test is the recommended + approach to testing a WAI application. Since requests and responses are simple datatypes, it is + easy to mock up a fake request, pass it to an application, and test properties about the + response. wai-test simply provides some convenience functions for testing common properties like + the presence of a header or a status code.

+

Templates

+

In the typical Model-View-Controller (MVC) paradigm, one of the goals is to separate logic from + the view. Part of this separation is achieved through the use of a template language. However, + there are many different ways to approach this issue. At one end of the spectrum, for example, + PHP/ASP/JSP will allow you to embed any arbitrary code within your template. At the other end, + you have systems like StringTemplate and QuickSilver, which are passed some arguments and have no + other way of interacting with the rest of the program.

+

Each system has its pros and cons. Having a more powerful template system can be a + huge convenience. Need to show the contents of a database table? No problem, pull it in with the + template. However, such an approach can quickly lead to convoluted code, interspersing database + cursor updates with HTML generation. This can be commonly seen in a poorly written ASP + project.

+

While weak template systems make for simple code, they also tend towards a lot of + redundant work. You will often need to not only keep your original values in datatypes, but also + create dictionaries of values to pass to the template. Maintaining such code is not easy, and + usually there is no way for a compiler to help you out.

+

Yesod's family of template languages, the Shakespearean languages, strive for a middle ground. + By leveraging Haskell's standard referential transparency, we can be assured that our templates + produce no side effects. However, they still have full access to all the variables and functions + available in your Haskell code. Also, since they are fully checked for both well-formedness, + variable resolution and type safety at compile time, typos are much less likely to have you + searching through your code trying to pin down a bug.

+ +

Types

+

One of the overarching themes in Yesod is proper use of types to make developers' + lives easier. In Yesod templates, we have two main examples:

  1. All content embedded into a Hamlet template must have a type of Html. As we'll see later, this forces us to properly escape dangerous HTML when + necessary, while avoiding accidental double-escaping as well.
  2. +
  3. Instead of concatenating URLs directly in our template, we have datatypes- known as + type-safe URLs- which represent the routes in our application.
  4. +

+

As a real-life example, suppose that a user submits his/her name to an application via + a form. This data would be represented with the Text datatype. Now we would like to display this + variable, called name, in a page. The type system- at compile time- prevents it from being + simply stuck into a Hamlet template, since it's not of type Html. Instead we must convert it + somehow. For this, there are two conversion functions:

  1. toHtml will automatically escape any entities. So if a user submits the string + <script src="http://example.com/evil.js"></script>, the + less than signs will automatically be converted to &lt;.
  2. +
  3. preEscapedText, on the other hand, will leave the content precisely as it is + now.
  4. +
So in the case of untrusted input from a possibly nefarious user, toHtml would be our + recommended approach. On the other hand, let us say we have some static HTML stored on our server + that we would like to insert into some pages verbatim. In that case, we could load it into a Text + value and then apply preEscapedText, thereby avoiding any double-escaping.

+

By default, Hamlet will use the toHtml function on any content you try to interpolate. + Therefore, you only need to explicitly perform a conversion if you want to avoid escaping. This + follows the dictum of erring on the side of caution.

+
Proper HTML Handling
+ +
name <- runInputPost $ ireq textField "name"
+snippet <- readFile "mysnippet.html"
+return [hamlet|
+    <p>Welcome #{name}, you are on my site!
+    <div .copyright>#{preEscapedText snippet}
+|]
+
+

The first step in type-safe URLs is creating a datatype that represents all the + routes in your site. Let us say you have a site for displaying Fibonacci numbers. The site will + have a separate page for each number in the sequence, plus the homepage. This could be modeled + with the Haskell datatype

data FibRoute = Home | Fib Int
We could + then create a page like + so:
<p>You are currently viewing number #{show index} in the sequence. Its value is #{fib index}.
+<p>
+    <a href=@{Fib (index + 1)}>Next number
+<p>
+    <a href=@{Home}>Homepage
Then + all we need is some function to convert a type-safe URL into a string representation. In our + case, that could look something like + this:
render :: FibRoute -> Text
+render Home = "/home"
+render (Fib i) = "/fib/" ++ show i

+

Fortunately, all of the boilerplate of defining and rendering type-safe URL datatypes is + handled for the developer automatically by Yesod. We will cover that in more depth later.

+

The Other Languages

+

In addition to Hamlet, there are three other languages. Julius is used for Javascript. + However, it's a simple pass-through language, just allowing for interpolation. In other words, + barring accidental use of the interpolation syntax, any piece of Javascript could be dropped into + Julius and be valid. For example, to test the performance of Julius, jQuery was run through the + language without an issue.

+

The other two languages are alternate CSS syntaxes. Those familiar with the difference between + Sass and Less will recognize this immediately: Cassius is whitespace delimited, while Lucius uses + braces. Lucius is in fact a superset of CSS, meaning all valid CSS files are valid Lucius files. + In addition to allowing text interpolation, there are some helper datatypes provided to model + unit sizes and colors. Also, type-safe URLs work in these languages, making it convenient for + specifying background images.

+

Aside from the type safety and compile-time checks mentioned above, having specialized + languages for CSS and Javascript give us a few other advantages:

+
  • For production, all the CSS and Javascript is compiled into the final executable, increasing + performance (by avoiding file I/O) and simplifying deployment.
  • +
  • By being based around the efficient builder construct described earlier, the + templates can be rendered very quickly.
  • +
  • There is built-in support for automatically including these in final webpages. We + will get into this in more detail when describing widgets below.
  • +
+

Persistent

+

Most web applications will want to store information in a database. Traditionally, this has + meant some kind of SQL database. In that regard, Yesod continues a long tradition, with + PostgreSQL as our most commonly used backend. But as we have been seeing in recent years, SQL + isn't always the answer to the persistence question. Therefore, Yesod was designed to work well + with NoSQL databases as well, and ships with a MongoDB backend as a first-class citizen.

+

The result of this design decision is Persistent, Yesod's preferred storage option. There are + really two guiding lights for Persistent: make it as backend agnostic as possible, and let user + code be completely type-checked.

+

At the same time, we fully recognize that it is impossible to completely shield the + user away from all details of the backend. Therefore, we provide two types of escape routes:

+
  • Provide backend-specific functionality as necessary. For example, Persistent provides + features for SQL joins and MongoDB lists and hashes. Proper portability warnings will apply, but + if you want this functionality, it's there.
  • +
  • Easy access to performing raw queries. We don't believe it's possible for any + abstraction to cover every use case of the underlying library. If you just have to write a + 5-table, correlated subquery in SQL, go right ahead.
  • +
+

Terminology

+

The most primitive datatype in Persistent is the PersistValue. This represents + any raw data that can appear within the database, such as a number, a date, or a string. Of + course, sometimes you'll have some more user-friendly datatypes you want to store, like HTML. For + that, we have the PersistField class. Internally, a + PersistField expresses itself to the database in terms of a + PersistValue.

+

All of this is very nice, but we will want to combine different fields together into a + larger picture. For this, we have a PersistEntity, which is basically a + collection of PersistFields. And finally, we have a PersistBackend that describes how to create, read, update and delete these + entities.

+

As a practical example, consider storing a person in a database. We want to store the + person's name, birthday, and a profile image (a PNG file). We create a new entity Person with three fields: a Text, a Day and a PNG. Each of those get stored in the + database using a different PersistValue constructor: PersistText, PersistDay and PersistByteString, respectively.

+

There is nothing surprising about the first two mappings, but the last one is + interesting. There is no specific constructor for storing PNG content in a database, so instead + we use a more generic type (a ByteString, which is just a sequence of bytes). We could use the + same mechanism to store other types of arbitrary data.

+ +

How is all this represented in the database? Consider SQL as an example: the Person entity becomes a table with three columns (name, birthday, and + picture). Each field is stored as a different SQL type: Text becomes a + VARCHAR, Day becomes a Date and PNG becomes a BLOB (or + BYTEA).

+

The story for MongoDB is very similar. Person becomes its own + document, and its three fields each become a MongoDB field. There is + no need for data types or creation of a schema in MongoDB.

+ + + + + + + + + + + + + + + + +
PersistentSQLMongoDB
PersistEntityTableDocument
PersistFieldColumnField
PersistValueColumn type N/A
+

Type Safety

+

Persistent handles all of the data marshaling concerns behind the scenes. As a user of + Persistent, you get to completely ignore the fact that a Text becomes a + VARCHAR. You are able to simply declare your datatypes and use + them.

+

Every interaction with Persistent is strongly typed. This prevents you from + accidentally putting a number in the date fields; the compiler will not accept it. Entire classes + of subtle bugs simply disappear at this point.

+

Nowhere is the power of strong typing more pronounced than in refactoring. Let's say + you have been storing users' ages in the database, and you realize that you really wanted to + store birthdays instead. You are able to make a single line change to your entities declaration + file, hit compile, and automatically find every single line of code that needs to be updated.

+

In most dynamically-typed languages, and their web frameworks, the recommended + approach to solving this issue is writing unit tests. If you have full test coverage, then + running your tests will immediately reveal what code needs to be updated. This is all well and + good, but it is a weaker solution than true types:

+
  • It is all predicated on having full test coverage. This takes extra time, and worse, + is boilerplate code that the compiler should be able to do for you.
  • +
  • You might be a perfect developer who never forgets to write a test, but can you say the same + for every person who will touch your codebase?
  • +
  • Even 100% test coverage doesn't guarantee that you really have tested every case. All it's + done is proven you've tested every line of code.
  • +
+

Cross-database Syntax

+

Creating an SQL schema that works for multiple SQL engines can be tricky enough. How + do you create a schema that will also work with a non-SQL database like MongoDB?

+

Persistent allows you to define your entities in a high-level syntax, and will + automatically create the SQL schema for you. In the case of MongoDB, we currently use a + schema-less approach. This also allows Persistent to ensure that your Haskell datatypes match + perfectly with the database's definitions.

+

Additionally, having all this information gives Persistent the ability to perform more advanced + functions for you automatically, such as migrations.

+

Migrations

+

Persistent not only creates schema files as necessary, but will also automatically + apply database migrations if possible. Database modification is one of the less-developed pieces + of the SQL standard, and thus each engine has a different take on the process. As such, each + Persistent backend defines its own set of migration rules. In PostgreSQL, which has a rich set of + ALTER TABLE rules, we use those extensively. Since SQLite lacks much + of that functionality, we are reduced to creating temporary tables and copying rows. MongoDB's + schema-less approach means no migration support is required.

+

This feature is purposely limited to prevent any kind of data loss. It will not remove any + columns automatically; instead, it will give you an error message, telling you the unsafe + operations that are necessary in order to continue. You will then have the option to either + manually run the SQL it provides you, or to change your data model to avoid the dangerous + behavior.

+

Relations

+

Persistent is non-relational in nature, meaning it has no requirement for backends to + support relations. However, in many use cases, we may want to use relations. In those cases, + developers will have full access to them.

+

Assume we want to now store a list of skills with each user. If we were writing a + MongoDB-specific app, we could go ahead and just store that list as a new field in the original + Person entity. But that approach would not work in SQL. In SQL, we call this kind of relationship + a one-to-many relationship.

+

The idea is to store a reference to the "one" entity (person) with each "many" entity (skill). + Then if we want to find all the skills a person has, we simply find all skills that reference + that person. For this reference, every entity has an ID. And as you might expect by now, these + IDs are completely type-safe. The datatype for a Person ID is PersonId. So to + add our new skill, we would just add the following to our entity definition:

+
Skill
+    person PersonId
+    name Text
+    description Text
+    UniqueSkill person name
+

This ID datatype concept comes up throughout Persistent and Yesod. You can dispatch based on an + ID. In such a case, Yesod will automatically marshal the textual representation of the ID to the + internal one, catching any parse errors along the way. These IDs are used for lookup and deletion + with the get and delete functions, and are returned by the + insertion and query functions insert and selectList.

+

Yesod

+

If we are looking through the typical Model-View-Controller (MVC) paradigm, Persistent + is the model and Sheakespeare is the view. This would leave Yesod as the controller.

+

The most basic feature of Yesod is routing. It features a declarative syntax and + type-safe dispatch. Layered on top of this, Yesod provides many other features: streaming content + generation, widgets, i18n, static files, forms and authentication. But the core feature added by + Yesod is really routing.

+

This layered approach makes it simpler for users to swap different components of the system. + Some people are not interested in using Persistent. For them, nothing in the core system even + mentions Persistent. Likewise, while commonly used features, not everyone needs authentication or + static file serving.

+

On the other hand, many users will want to integrate all of these features. And doing + so- while enabling all the optimizations available in Yesod- is not always straightforward. To + simplify the process, Yesod provides a scaffolding tool as well that sets up a basic site with + the most commonly used features.

+

Routes

+

Given that routing is really the main function of Yesod, let's start there. The routing syntax + is very simple: a resource pattern, a name, and request methods. For example, a + simple blog site might look like:

+
/ HomepageR GET
+/add-entry AddEntryR GET POST
+/entry/#EntryId EntryR GET
+

The first line defines the homepage. This says "I respond to the root path of the domain, I'm + called HomepageR, and I answer GET requests."

+

The second line defines the add entry page. This time, we answer both GET and POST + requests. You might be wondering why Yesod, as opposed to most frameworks, requires you to + explicitly state your request methods. The reason is that Yesod tries to adhere to RESTful + principles as much as possible, and a GET and POST request really have very different meanings. + Not only do you state these two methods separately, but later you will define their handler + functions separately.

+ +

The third line is a bit more interesting. After the second slash we have #EntryId. This defines a parameter of type EntryId. In the Persistent section, we already alluded to this feature: Yesod will now + automatically marshal the path component into the relevant ID value. Assuming an SQL backend + (Mongo is addressed later), if a user requests /entry/5, the handler + function will get called with an argument EntryId 5. But if the user + requests /entry/some-blog-post, Yesod will return a 404.

+

This is obviously possible in most other web frameworks as well. The approach taken + by Django, for instance, would use a regular expression for matching the routes, e.g. r"/entry/(\d+)". The Yesod approach, however, provides some advantages:

+
  • Typing "EntryId" is much more semantic/developer-friendly than a regular + expression.
  • +
  • Regular expressions cannot express everything (or at least, can't do so + succinctly). We can use /calendar/#Day in Yesod; do you want to type + a regex to match dates in your routes?
  • +
  • Yesod also automatically marshals the data for us. In our calendar case, our handler function + would receive a Day value. In the Django equivalent, the function would receive + a piece of text which it would then have to marshal itself. This is tedious, repetitive and + inefficient.
  • +
  • So far we've assumed that a database ID is just a string of digits. But what if it's more + complicated? MongoDB uses GUIDs, for example. In Yesod, your #EntryId will still work, and the + type system will instruct Yesod how to parse the route. In a regex system, you would have to go + through all of your routes and change the (\d+) to whatever monstrosity of regex is needed to + match.
  • +
+

Type-safe URLs

+

This approach to routing gives birth to one of Yesod's most powerful features: + type-safe URLs. Instead of just splicing together pieces of text to refer to a route, every route + in your application can be represented by a Haskell value. This immediately eliminates a large + number of 404 not found errors: it is simply not possible to produce an invalid URL.

+ +

So how does this magic work? Each site has a route datatype, and each resource pattern gets its + own constructor. In our previous example, we would get something that looks like:

+
data MySiteRoute = HomepageR
+                 | AddEntryR
+                 | EntryR EntryId
+
+

If you want to link to the homepage, you use HomepageR. To link to a specific + entry, you would use the EntryR constructor with an EntryId + parameter. For example, to create a new entry and redirect to it, you could write:

+
entryId <- insert (Entry "My Entry" "Some content")
+redirect RedirectTemporary (EntryR entryId)
+
+

Hamlet, Lucius and Julius all include built-in support for these type-safe URLs. + Inside a Hamlet template, you can easily create a link to the add entry page:

+
<a href=@{AddEntryR}>Create a new entry.
+
+

The best part? Just like Persistent entities, the compiler will keep you honest. If you change + any of your routes (e.g., you want to include the year and month in your entry routes), Yesod + will force you to update every single reference throughout your codebase.

+

Handlers

+

Once you define your routes, you need to tell Yesod how you want to respond to + requests. This is where handler functions come into play. The setup is + simple; for each resource (e.g., HomepageR) and request method, create a function named + methodResourceR. For our previous example, we would need four functions: getHomepageR, getAddEntryR, postAddEntryR, and getEntryR.

+

All of the parameters collected from the route are passed in as arguments to the handler + function. getEntryR will take a first arugment of type EntryId, while all the + other functions will take no arguments.

+

The handler functions live in a Handler monad, which + provides a great deal of functionality, such as redirecting, accessing sessions, and running + database queries. For the last one, a typical way to start of the getEntryR function would be:

+
getEntryR entryId = do
+    entry <- runDB $ get404 entryId
+

This will run a database action that will get the entry associated with the given ID from the + database. If there is no such entry, it will return a 404 response.

+

Each handler function will return some value, which must be an instance of + HasReps. This is another RESTful feature at play: instead of just returning + some HTML or some JSON, you can return a value that will return either one, depending on the HTTP + Accept request header. In other words, in Yesod, a resource is a specific piece of data, and it + can be returned in one of many representations.

+

Widgets

+

Assume you want to include a navbar on a few different pages of your site. This navbar + will load up the five most recent blog posts (stored in your database), generate some HTML, and + then need some CSS and Javascript to style and enhance.

+

Without a higher-level interface to tie these components together, this could be a + pain to implement. You could add the CSS to the sitewide CSS file, but that's adding extra + declarations you don't always need. Likewise with the Javascript, though a bit worse: having that + extra Javascript might cause problems on a page it was not intended to live on. You will also be + breaking modularity by having to generate the database results from multiple handler + functions.

+

In Yesod, we have a very simple solution: widgets. A widget is a piece of code that + ties together HTML, CSS and Javascript, allowing you to add content to both the <head> and + <body>, and can run any arbitrary code that belongs in a handler. For example, to implement + our navbar:

+
-- Get last five blog posts. The "lift" says to run this code like we're in the handler.
+entries <- lift $ runDB $ selectList [] [LimitTo 5, Desc EntryPosted]
+toWidget [hamlet|
+<ul .navbar>
+    $forall entry <- entries
+        <li>#{entryTitle entry}
+|]
+toWidget [lucius| .navbar { color: red } |]
+toWidget [julius|alert("Some special Javascript to play with my navbar");|]
+
+

But there is even more power at work here. When you produce a page in Yesod, the + standard approach is to combine a number of widgets together into a single widget containing all + your page content, and then apply defaultLayout. This function is defined per site, and applies + the standard site layout.

+

There are two out-of-the-box approaches to handle where the CSS and Javascript + goes:

+
  1. Concatenate them and place them into <style> and <script> tags, respectively, within + your HTML.
  2. +
  3. Place them in external files and refer to them with <link> and <script> tags, + respectively.
  4. +
+

In addition, your Javascript can be automatically minified. Option 2 is the preferred + approach, since it allows a few extra optimizations:

+
  1. The files are created with names based on a hash of the contents. This means you can place + cache values far in the future without worries of users receiving stale content.
  2. +
  3. Your Javascript can be asynchronously loaded.
  4. +
+

The second point requires a bit of elaboration. Widgets not only contain raw + Javascript, they also contain a list of Javascript dependencies. For example, many sites will + refer to the jQuery library and then add some Javascript that uses it. Yesod is able to + automatically turn all of that into an asynchronous load via yepnope.js.

+

In other words, widgets allow you to create modular, composable code that will result in + incredibly efficient serving of your static resources.

+

Subsites

+

Many websites share common pieces of functionality. Perhaps the two most common examples of + this are serving static files and authentication. In Yesod, you can easily drop in this code + using a subsite. All you need to do is add an extra line to your routes. For + example, to add the static subsite, you would write:

+
/static StaticR Static getStatic
+

The first argument tells where in the site the subsite starts. The static subsite is + usually used at /static, but you could use whatever you want. StaticR is the name of the route; + this is also entirely up to you, but convention is to use StaticR. Static is the name of the + static subsite; this is one you do not have control over. getStatic is a + function that returns the settings for the static site, such as where the static files are + located.

+

Like all of your handlers, the subsite handlers also have access to the + defaultLayout function. This means that a well designed subsite will + automatically use your site skin without any extra intervention on your part.

+

Lessons Learned

+

Yesod has been a very rewarding project to work on. It has given me an opportunity to work on a + large system with a diverse group of developers. One of the things that has truly shocked me is + how different the end product has become versus what I had originally intended. I started off + Yesod by creating a list of goals. Very few of the main features we currently tout in Yesod are + in that list, and a good portion of that list is no longer something I plan to implement. The + first lesson is:

+
You will have a better idea of the system you need after you start working on it. Do not tie + yourself down to your initial ideas.
+

As this was my first major piece of Haskell code, I've learnt a lot about the language during + Yesod's development. I'm sure others can relate to the feeling of "How did I ever write code like + this?" Even though that initial code was not of the same caliber as the code we have in Yesod at + this point, it was solid enough to kick-start the project. The second lesson is:

+
Don't be deterred due to supposed lack of mastery of the tools at hand. Write the best code + you can, and keep improving it.
+

One of the most difficult steps in Yesod's development was moving from a single-person team- + me- to collaborating with others. It started off simply with merging pull requests on Github, and + eventually moved to having a number of core maintainers. I had established some of my own + development patterns, which were nowhere explained or documented. As a result, contributors found + it difficult to pull my latest unreleased changes and play around with them. This hindered others + both from contributing and testing.

+

When Greg Weber came aboard as another lead on Yesod, he put in place a lot of the coding + standards that were sorely lacking. To compound the problems, there were some inherent + difficulties playing with the Haskell development toolchain, specifically dealing with Yesod's + large number of packages. One of the goals of the entire Yesod team has since been to create + standard scripts and tools to automate building. Many of these tools are making their way back + into the general Haskell community. The final lesson is:

+
Consider early on how to make your project approachable for others.
+ +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/blog-example.html b/public/blog/2012/01/blog-example.html new file mode 100644 index 00000000..cc5b3816 --- /dev/null +++ b/public/blog/2012/01/blog-example.html @@ -0,0 +1,516 @@ + Blog: i18n, authentication, authorization, and database +

Blog: i18n, authentication, authorization, and database

January 13, 2012

GravatarBy Michael Snoyman

+

This is a simple blog app. It allows an admin to add blog posts via a rich text editor (nicedit), allows logged-in users to comment, and has full i18n support. It is also a good example of using a Persistent database, leveraging Yesod's authorization system, and templates.

+

While in general we recommend placing templates, Persist entity definitions, and routing in separate files, we'll keep it all in one file here for convenience. The one exception you'll see below will be i18n messages.

+

We'll start off with our language extensions. In scaffolded code, the language extensions are specified in the cabal file, so you won't need to put this in your individual Haskell files.

1
+2
+3
+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+TemplateHaskell, GADTs, FlexibleContexts,
+MultiParamTypeClasses #-}
+

Now our imports.

4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+
import Yesod
+import Yesod.Auth
+import Yesod.Form.Nic (YesodNic, nicHtmlField)
+import Yesod.Auth.BrowserId (authBrowserId)
+import Data.Text (Text)
+import Network.HTTP.Conduit (Manager, newManager, def)
+import Database.Persist.Sqlite
+    ( ConnectionPool, SqlPersist, runSqlPool, runMigration
+    , createSqlitePool
+    )
+import Data.Time (UTCTime, getCurrentTime)
+import Control.Applicative ((<$>), (<*>), pure)
+

First we'll set up our Persistent entities. We're going to both create our data types (via mkPersist) and create a migration function, which will automatically create and update our SQL schema. If you were using the MongoDB backend, migration would not be needed.

16
+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+

Keeps track of users. In a more robust application, we would also keep account creation date, display name, etc.

17
+18
+19
+
User
+   email Text
+   UniqueUser email
+

An individual blog entry (I've avoided using the word "post" due to the confusion with the request method POST).

20
+21
+22
+23
+
Entry
+   title Text
+   posted UTCTime
+   content Html
+

We need to tack on this "deriving" line since Html doesn't specify instances for Read, Show or Eq. If you get an error message about "cannot derive" in your own code, try adding the deriving statement.

24
+
deriving
+

And a comment on the blog post.

25
+26
+27
+28
+29
+30
+31
+
Comment
+   entry EntryId
+   posted UTCTime
+   user UserId
+   name Text
+   text Textarea
+|]
+

Every site has a foundation datatype. This value is initialized before launching your application, and is available throughout. We'll store a database connection pool and HTTP connection manager in ours. See the very end of this file for how those are initialized.

32
+33
+34
+35
+
data Blog = Blog
+   { connPool :: ConnectionPool
+   , httpManager :: Manager
+   }
+

To make i18n easy and translator friendly, we have a special file format for translated messages. There is a single file for each language, and each file is named based on the language code (e.g., en, es, de-DE) and placed in that folder. We also specify the main language file (here, "en") as a default language.

36
+
mkMessage "Blog" "../messages-blog" "en"
+

Our en.msg message file contains the following content:

+
NotAnAdmin: You must be an administrator to access this page.
+
+WelcomeHomepage: Welcome to the homepage
+SeeArchive: See the archive
+
+NoEntries: There are no entries in the blog
+LoginToPost: Admins can login to post
+NewEntry: Post to blog
+NewEntryTitle: Title
+NewEntryContent: Content
+
+PleaseCorrectEntry: Your submitted entry had some errors, please correct and try again.
+EntryCreated title@Text: Your new blog post, #{title}, has been created
+
+EntryTitle title@Text: Blog post: #{title}
+CommentsHeading: Comments
+NoComments: There are no comments
+AddCommentHeading: Add a Comment
+LoginToComment: You must be logged in to comment
+AddCommentButton: Add comment
+
+CommentName: Your display name
+CommentText: Comment
+CommentAdded: Your comment has been added
+PleaseCorrectComment: Your submitted comment had some errors, please correct and try again.
+
+HomepageTitle: Yesod Blog Demo
+BlogArchiveTitle: Blog Archive
+
+

Now we're going to set up our routing table. We have four entries: a homepage, an entry list page (BlogR), an individual entry page (EntryR) and our authentication subsite. Note that BlogR and EntryR both accept GET and POST methods. The POST methods are for adding a new blog post and adding a new comment, respectively.

37
+38
+39
+40
+41
+42
+
mkYesod "Blog" [parseRoutes|
+/ RootR GET
+/blog BlogR GET POST
+/blog/#EntryId EntryR GET POST
+/auth AuthR Auth getAuth
+|]
+

Every foundation needs to be an instance of the Yesod typeclass. This is where we configure various settings.

43
+
instance Yesod Blog where
+

The base of our application. Note that in order to make BrowserID work properly, this must be a valid URL.

44
+
approot = ApprootStatic "http://localhost:3000"
+

Our authorization scheme. We want to have the following rules:

+
  • Only admins can add a new entry.
  • +
  • Only logged in users can add a new comment.
  • +
  • All other pages can be accessed by anyone.
  • +
+

We set up our routes in a RESTful way, where the actions that could make changes are always using a POST method. As a result, we can simply check for whether or not a request is a write request, given by the True in the second field.

+

First, we'll authorize requests to add a new entry.

45
+46
+47
+48
+49
+50
+51
+
isAuthorized BlogR True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just (Entity _ user)
+                | isAdmin user -> return Authorized
+                | otherwise    -> unauthorizedI MsgNotAnAdmin
+

Now we'll authorize requests to add a new comment.

52
+53
+54
+55
+56
+
isAuthorized (EntryR _) True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just _  -> return Authorized
+

And for all other requests, the result is always authorized.

57
+
isAuthorized _ _ = return Authorized
+

Where a user should be redirected to if they get an AuthenticationRequired.

58
+
authRoute _ = Just (AuthR LoginR)
+

This is where we define our site look-and-feel. The function is given the content for the individual page, and wraps it up with a standard template.

59
+
defaultLayout inside = do
+

Yesod encourages the get-following-post pattern, where after a POST, the user is redirected to another page. In order to allow the POST page to give the user some kind of feedback, we have the getMessage and setMessage functions. It's a good idea to always check for pending messages in your defaultLayout function.

60
+
mmsg <- getMessage
+

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 CSS for all pages. We'll use the Lucius template language to create the latter.

61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+
pc <- widgetToPageContent $ do
+            toWidget [lucius|
+body {
+    width: 760px;
+    margin: 1em auto;
+    font-family: sans-serif;
+}
+textarea {
+    width: 400px;
+    height: 200px;
+}
+#message {
+  color: #900;
+}
+|]
+            inside
+

And finally we'll use a new Hamlet template to wrap up the individual components (title, head data and body data) into the final output.

77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+
hamletToRepHtml [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <div #message>#{msg}
+        ^{pageBody pc}
+|]
+

This is a simple function to check if a user is the admin. In a real application, we would likely store the admin bit in the database itself, or check with some external system. For now, I've just hard-coded my own email address.

88
+89
+
isAdmin :: User -> Bool
+isAdmin user = userEmail user == "michael@snoyman.com"
+

In order to access the database, we need to create a YesodPersist instance, which says which backend we're using and how to run an action.

90
+91
+92
+93
+94
+95
+
instance YesodPersist Blog where
+   type YesodPersistBackend Blog = SqlPersist
+   runDB f = do 
+       master <- getYesod
+       let pool = connPool master
+       runSqlPool f pool
+

This is a convenience synonym. It is defined automatically for you in the scaffolding.

96
+
type Form x = Html -> MForm Blog Blog (FormResult x, Widget)
+

In order to use yesod-form and yesod-auth, we need an instance of RenderMessage for FormMessage. This allows us to control the i18n of individual form messages.

97
+98
+
instance RenderMessage Blog FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

In order to use the built-in nic HTML editor, we need this instance. We just take the default values, which use a CDN-hosted version of Nic.

99
+
instance YesodNic Blog
+

In order to use yesod-auth, we need a YesodAuth instance.

100
+101
+102
+103
+104
+
instance YesodAuth Blog where
+    type AuthId Blog = UserId
+    loginDest _ = RootR
+    logoutDest _ = RootR
+    authHttpManager = httpManager
+

We'll use BrowserID, which is a third-party system using email addresses as your identifier. This makes it easy to switch to other systems in the future, locally authenticated email addresses (also included with yesod-auth).

105
+
authPlugins _ = [authBrowserId]
+

This function takes someone's login credentials (i.e., his/her email address) and gives back a UserId.

106
+107
+108
+109
+110
+
getAuthId creds = do
+        let email = credsIdent creds
+            user = User email
+        res <- runDB $ insertBy user
+        return $ Just $ either entityKey id res
+

Homepage handler. The one important detail here is our usage of setTitleI, which allows us to use i18n messages for the title. We also use this message with a _{Msg...} interpolation in Hamlet.

111
+112
+113
+114
+115
+116
+117
+118
+
getRootR :: Handler RepHtml
+getRootR = defaultLayout $ do
+    setTitleI MsgHomepageTitle
+    [whamlet|
+<p>_{MsgWelcomeHomepage}
+<p>
+   <a href=@{BlogR}>_{MsgSeeArchive}
+|]
+

Define a form for adding new entries. We want the user to provide the title and content, and then fill in the post date automatically via getCurrentTime.

119
+120
+121
+122
+123
+
entryForm :: Form Entry
+entryForm = renderDivs $ Entry
+    <$> areq textField (fieldSettingsLabel MsgNewEntryTitle) Nothing
+    <*> aformM (liftIO getCurrentTime)
+    <*> areq nicHtmlField (fieldSettingsLabel MsgNewEntryContent) Nothing
+

Get the list of all blog entries, and present an admin with a form to create a new entry.

124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+
getBlogR :: Handler RepHtml
+getBlogR = do
+    muser <- maybeAuth
+    entries <- runDB $ selectList [] [Desc EntryPosted]
+    ((_, entryWidget), enctype) <- generateFormPost entryForm
+    defaultLayout $ do
+        setTitleI MsgBlogArchiveTitle
+        [whamlet|
+$if null entries
+    <p>_{MsgNoEntries}
+$else
+    <ul>
+        $forall Entity entryId entry <- entries
+            <li>
+                <a href=@{EntryR entryId}>#{entryTitle entry}
+

We have three possibilities: the user is logged in as an admin, the user is logged in and is not an admin, and the user is not logged in. In the first case, we should display the entry form. In the second, we'll do nothing. In the third, we'll provide a login link.

139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+
$maybe Entity _ user <- muser
+    $if isAdmin user
+        <form method=post enctype=#{enctype}>
+            ^{entryWidget}
+            <div>
+                <input type=submit value=_{MsgNewEntry}>
+$nothing
+    <p>
+        <a href=@{AuthR LoginR}>_{MsgLoginToPost}
+|]
+

Process an incoming entry addition. We don't do any permissions checking, since isAuthorized handles it for us. If the form submission was valid, we add the entry to the database and redirect to the new entry. Otherwise, we ask the user to try again.

149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+
postBlogR :: Handler RepHtml
+postBlogR = do
+    ((res, entryWidget), enctype) <- runFormPost entryForm
+    case res of
+        FormSuccess entry -> do
+            entryId <- runDB $ insert entry
+            setMessageI $ MsgEntryCreated $ entryTitle entry
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectEntry
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{entryWidget}
+    <div>
+        <input type=submit value=_{MsgNewEntry}>
+|]
+

A form for comments, very similar to our entryForm above.

165
+166
+167
+168
+169
+170
+171
+
commentForm :: EntryId -> Form Comment
+commentForm entryId = renderDivs $ Comment
+    <$> pure entryId
+    <*> aformM (liftIO getCurrentTime)
+    <*> aformM requireAuthId
+    <*> areq textField (fieldSettingsLabel MsgCommentName) Nothing
+    <*> areq textareaField (fieldSettingsLabel MsgCommentText) Nothing
+

Show an individual entry, comments, and an add comment form if the user is logged in.

172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+
getEntryR :: EntryId -> Handler RepHtml
+getEntryR entryId = do
+    (entry, comments) <- runDB $ do
+        entry <- get404 entryId
+        comments <- selectList [CommentEntry ==. entryId] [Asc CommentPosted]
+        return (entry, map entityVal comments)
+    muser <- maybeAuth
+    ((_, commentWidget), enctype) <-
+        generateFormPost (commentForm entryId)
+    defaultLayout $ do
+        setTitleI $ MsgEntryTitle $ entryTitle entry
+        [whamlet|
+<h1>#{entryTitle entry}
+<article>#{entryContent entry}
+    <section .comments>
+        <h1>_{MsgCommentsHeading}
+        $if null comments
+            <p>_{MsgNoComments}
+        $else
+            $forall Comment _entry posted _user name text <- comments
+                <div .comment>
+                    <span .by>#{name}
+                    <span .at>#{show posted}
+                    <div .content>#{text}
+        <section>
+            <h1>_{MsgAddCommentHeading}
+            $maybe _ <- muser
+                <form method=post enctype=#{enctype}>
+                    ^{commentWidget}
+                    <div>
+                        <input type=submit value=_{MsgAddCommentButton}>
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>_{MsgLoginToComment}
+|]
+

Receive an incoming comment submission.

207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+
postEntryR :: EntryId -> Handler RepHtml
+postEntryR entryId = do
+    ((res, commentWidget), enctype) <-
+        runFormPost (commentForm entryId)
+    case res of
+        FormSuccess comment -> do
+            _ <- runDB $ insert comment
+            setMessageI MsgCommentAdded
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectComment
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{commentWidget}
+    <div>
+        <input type=submit value=_{MsgAddCommentButton}>
+|]
+

Finally our main function.

224
+225
+226
+227
+228
+229
+230
+
main :: IO ()
+main = do
+    pool <- createSqlitePool "blog.db3" 10 -- create a new pool
+    -- perform any necessary migration
+    runSqlPool (runMigration migrateAll) pool
+    manager <- newManager def -- create a new HTTP manager
+    warpDebug 3000 $ Blog pool manager -- start our server
+
+ +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/coming-soon.html b/public/blog/2012/01/coming-soon.html new file mode 100644 index 00000000..fb1d0052 --- /dev/null +++ b/public/blog/2012/01/coming-soon.html @@ -0,0 +1,17 @@ + Coming soon, to a bookstore near you +

Coming soon, to a bookstore near you

January 31, 2012

GravatarBy Michael Snoyman

Yesod Web Development book cover
+ +

That is all :)

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/conduit-changes-0-2.html b/public/blog/2012/01/conduit-changes-0-2.html new file mode 100644 index 00000000..02088b84 --- /dev/null +++ b/public/blog/2012/01/conduit-changes-0-2.html @@ -0,0 +1,112 @@ + Exciting changes coming to conduit 0.2 +

Exciting changes coming to conduit 0.2

January 29, 2012

GravatarBy Michael Snoyman

+

tl;dr: The new Haddocks are available at: http://www.snoyman.com/haddocks/conduit-0.2.0/index.html

+

Even though it's relatively young, conduit has gotten a lot of real-world + usage, and a fair bit of scrutiny. I think we achieved all of our main objectives with the first + release, but that doesn't mean we're going to avoid improvements. I asked the community to give + their feedback, and here were the main criticisms I've heard:

+
  1. BufferedSource doesn't feel quite right. One complaint was the name + bsourceUnpull, but overall people thought it didn't fit in well with the rest + of the package.
  2. +
  3. Usage of mutable variables for storing state is suboptimal.
  4. +
  5. The split between Source and PreparedSource isn't very + nice.
  6. +
+

While I won't call the first issue fully resolved, I would say that conduit 0.1 was a big step + in the right direction. Instead of exposing all the internals of BufferedSource, + it's now an abstract type. (This does solve the bsourceUnpull name + dislike, though that's obviously a minor point.) Overall, we had a move in dependent packages + away from using BufferedSource in any external APIs. In other words, + BufferedSource is intended purely as an internal tool. For example, in Warp, we + use BufferedSource to parse the request headers, but then convert it back to a + Source to pass to the application for request body reading.

+

I've been opposed to making any changes for the second issue (mutable variables). My belief was + that one of the sources of conduits' simplicity relative to enumerators was its usage of mutable + state. And in general, I don't believe in changing something until there's hard evidence that + it's actually causing problems.

+

Last week, however, Felipe Lessa found one such concrete problem: using + SequencedSink was very slow. Upon investigation, I determined that the problem + came from Sink's monadic bind implementation. The issue is that for each bind, a + new mutable variable was being allocated, and it needed to be checked to determine its state. + Unfortunately, having a long chain of binds resulted in exponential complexity, having to check + N variables for each action. This clearly needed to be fixed, but there was no + way to do so (that I could see) with the previous types.

+

So I was presented with a dilemna: either continue in the mutable variable path and try to + solve the problem, or go in the pure/CPS direction, where I knew a simpler solution existed. The + choice was actually pretty easy: go for the pure approach. I had the following reasons:

+
  • The main motivation to avoid the change to CPS was to keep the simplicity of the current + approach. However, I was about to lose that simplicity anyway.
  • +
  • Like most Haskellers, I do have an innate dislike for mutable variables.
  • +
  • After more work comparing conduits to enumerators, I've come to believe that the main source + of confusion in enumerators is that the data producer (Enumerator) is just a + consumer-transformer. Since the essence of Source would stay the same in CPS, I + think that this change does not hinder our simplicity.
  • +
  • There was strong reason to believe that GHC would be able to optimize CPS code better than + mutable variable code.
  • +
+

So I took the plunge and tried out CPS... and I really like the result! The first change is to + SourceResult's Open constructor: instead of just returning a + new value, it returns a new value and a new Source. This allows us to + pass our state in that new Source. There are similar changes to + SinkResult and ConduitResult. After this, I benchmarked the + old and new version, comparing both a monadic-bind-intensive Sink and a + Sink without any binds. The former had a ten-fold speedup (not surprising due + to the decrease in algorithmic complexity), and the latter had a 20% speedup.

+

But that wasn't the end of it. This new approach allows us to get rid of the + Prepared family of types. Let's take the sourceFile function + as an example, which opens a Handle and reads data from a file. In the old + approach, we needed to provide the PreparedSource with the + Handle in order for the PreparedSource to read from it. + Therefore, we had a Source which opened the Handle and passed + it to the PreparedSource. In the new approach, we have a Source + that opens a handle, reads some data, and returns a new Source that reads from + the Handle.

+

So contrary to my original belief, I think this CPS move actually simplifies conduit + greatly.

+

Another, orthogonal change that I put in was better data types in a few places. Previously, if + you wanted to use the sourceState function, and had a pull function that + returned Closed, you needed to provide a dummy state value. (If you look through + current conduit code bases, you'll see a lot of error calls.) + Instead, we now have a specialized data type (ConduitStateResult, name + suggestions welcome) that avoids this need. Internally, I also cleaned up a number of the types + to enforce invariants at the type level.

+

Speaking of invariants, the final simplification is that we now have just one invariant ruling + over the whole package: never reuse a Source, Sink, or + Conduit. After you pull from a Source, it will give you a new + Source. Do not reuse the original Source. If you get a + Closed result, there is no new Source, and therefore you + cannot pull again or close the Source.

+

I encourage everyone to have a look at the Haddocks and give me your + feedback.

+

When will this be released?

Likely some time this week. I don't have any + specific changes in mind right now, outside of name adjustments that are suggested by the + community.
+

How this affects users

+

Anyone programming against the high-level conduit API exclusively will have no breakage. If + you're using functions like sourceIO or sinkState, you'll have + minimal changes to use the modified datatypes (essentially changing a few constructors and + reordering your arguments). If you're coding directly against the low-level types, you'll need + to restructure things a bit to pass around continuations.

+

Please email me (or preferably the Haskell cafe) if you want some help on converting old + conduit code to this new set of types. For the most part, it's a mechanical process, and I can + give lots of examples from the code I've already migrated.

+
+

How this affects Yesod

+

Yesod 0.10 will be built off of this new-and-improved conduit. In fact, the code is already + updated for it. This likely means that the Yesod release will be about a week later than + originally anticipated, maybe in the second week of February.

+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/conduit-versus-enumerator.html b/public/blog/2012/01/conduit-versus-enumerator.html new file mode 100644 index 00000000..f662dcd9 --- /dev/null +++ b/public/blog/2012/01/conduit-versus-enumerator.html @@ -0,0 +1,140 @@ + Conduits versus enumerators +

Conduits versus enumerators

January 16, 2012

GravatarBy Michael Snoyman

+

I've been tempted to write this blog post for a few weeks now. While my blog series on + conduits purposely avoided enumerator comparisons, I'm sure the first question people + have is, "Why did we need a new solution to the problem?" Now that I've gotten feedback from a + different + people, and since multiple + people + are looking at novel solutions to the streaming data problem, I think + it's time to write this post.

+

Before we can really address the question, we need to understand the goals we are trying to + achieve. We want a solution that will allow us to:

+
  1. Produce, consume and transform streams of data.
  2. +
  3. Abstract over the underlying data source and destination.
  4. +
  5. Handle resources determinstically. This includes exception safety.
  6. +
  7. Composability: it should be simple to take a few transformers and a consumer and produce a + new consumer, for example.
  8. +
  9. Approachable: it shouldn't take months to master the approach.
  10. +
  11. Robust: we should be making it difficult for people to shoot themselves in the foot.
  12. +
  13. Convenient: we don't want a solution that requires radically modifying our code.
  14. +
+

The enumerator approach (encompassing the iteratee, + enumerator, and iterIO packages, as well as Oleg's + original papers) was a breakthrough for solving these issues. It fully addresses issues (1) and + (2) above. I would argue it also solves (6) very well. However, it only gives a half solution to + (3): while we can deterministically allocate resources in the producer (i.e. Enumerator), you + cannot do so in the consumer (i.e. Iteratee). As a result, it's impossible to write an + exception-safe iterFile function (this has been confirmed in all four variations + listed above, I can provide the code if anyone's curious).

+

(4) is also only a partial solution in my mind, at least if you consider (7). The use case that + finally opened my eyes to the limitations here was producing an HTTP proxy. I won't get into all + the gory details, but the basic issue is that enumerator forces a kind of Inversion of Control on + its users, making it awkward to interleave different streams. Again, I can provide the code for + this for the curious, but it's too involved to be discussed here.

+

(5) is really a complete failure. I didn't understand enumerators until I wrote a three part + blog series on how to use them, and even to this day, after having written a huge amount of + enumerator code, I still get stuck trying to write an Enumeratee. The naming is + confusing, and the technique is non-intutitive. Want to produce a stream of data? Then write a + function that transforms a Step into an Iteratee. There's a + logic to it, but it's not immediate. All of these type synonyms based on a single base type + (Iteratee) leads to very difficult-to-parse error messages. (7) also presents + problems: it's impossible to catch exceptions in an Iteratee, it's awkward to + deal with monad transformer stacks, and so on.

+

So after much discussion with others and a lot of thought, I started working on + conduits. In my mind, here are the major changes:

+
  • The names are much more intuitive. I think most people understand Source and + Sink immediately, with Conduit only taking a moment + longer.
  • +
  • Source, Conduit, and Sink are all distinct + types, meaning error messages are much clearer. This also means no awkward, unused type + variables like we have for Enumerator and Enumeratee.
  • +
  • Instead of being a sink transformer, a Source is a type that allows you to + pull data from it. One advantage is simplicity. But a more powerful advantage is that there's + no inversion of control. If you want, you can just pull data from a source and never write + a sink. Or if you use the standard connect operator ($$, as I'm guessing + everyone does), there is no complicated control flow involved. A side effect of this is you can + actually deal with exceptions fully. (Another minor advantage is we get nice typeclass instances + like Monoid.)
  • +
  • There is full control for allocating resources anywhere, via the ResourceT + transformer. Not only does this allow us to write code we couldn't write before, it also means + we can write all of our code more easily. For example, in the past getting a database + connection from a resource pool required using a withPool function, which meant + that creating streaming responses from WAI was a huge inversion-of-control exercise. With + ResourceT, we can safely "check out" a resource and know it will be returned + appropriately.
  • +
  • Since we've avoided continuation passing style throughout the types, we can easily modify the + monad stacks that our code is living in. One example usage for this would be parsing some data, + introducing a new monad transformer (e.g., a ReaderT holding the parsed data) + for some internal computation, unwrapping the transformer, and continuing computation. I tried, and failed, + to implement a general purpose solution to this problem in enumerator.
  • +
  • To deal with the remaining inversion of control cases, we've introduced buffered sources. + This is a direct outcome of the third point above, and means massively simplified APIs (compare + http from + http-enumerator and + http from + http-conduit).
  • +
+

Vague criticisms of the conduit package notwithstanding, the only real downside I know of to + the approach is its reliance on mutable state. I've considered reworking parts of the codebase to + get rid of the mutable state, but have decided against it, because:

+
  • Besides a vague "I don't like it," no one has shown me a concrete problem with our usage of + mutable variables.
  • +
  • Since we allow ST, you can still use conduits from pure code. Besides, the + vast majority of conduit code will live in IO anyway.
  • +
  • I think the mutable state makes the internals of conduit much more approachable. It's very + easy to see what's going on.
  • +
  • We'd have to replace the mutable state with some form of CPS, which would likely destroy some + of our other advantages (e.g., easy monad stack modification).
  • +
+

So that's my overall conduit vs enumerator breakdown. I would like to point out that not only + have a huge percentage of the enumerator-based packages out there been successfully ported to + conduit (and simplified in the process!), but we have even added new packages that never existed + in enumerator. Conduits may be young, but they are production ready (I'm shipping conduit code to + clients already), and far beyond any proof-of-concept stage. They are a viable alternative to + enumerators.

+

Other alternatives

+

In the first paragraph of this post, I linked to three other alternate approaches. Firstly, + none of these address the resource allocation issue. While a few authors have pointed out that + they could reuse the ResourceT approach to do so, that hasn't actually been + done, and therefore- as they stand- they do not solve the problem.

+

One of the approaches is actually very close in spirit to conduits. The main difference is + its avoidance of mutable state. You can actually get a very good idea about the changes + necessary to get rid of mutable state in conduits by reading through that post. I still believe + that such a change is not only unnecessary, but would be detrimental.

+

The other two approaches use coroutines as a basis. I'll focus on pipes, as + it got much more attention on Reddit. While I find the approach beautiful, I don't find it + practical. It's actually a step backwards from enumerators, as it doesn't solve any of + the resource allocation issue. It's very nice that it fits snugly into a + Category, but I don't think people are going to rush to restructure their code + to fit the Category approach. Additionally, while the author makes claims that + all error handling is orthogonal to the package, I don't think it's at all possible to catch + exceptions within a Pipe, which would make it a deal breaker for me.

+

But probably my biggest concern with this package is its distinction between lazy and strict. + The tutorial makes it clear that if you use the wrong one, your code might not terminate, or + perhaps never free an allocated file. (I don't think exceptions were ever considered here.) This + screams out to me that the paradigm is brittle, and will not compose for any large project. I + think this is something that would become immediately apparent as soon as someone starts writing + real code with pipes. (Going to the original seven points in the post, I'm saying pipes fails at + (6).)

+

My point here is not to pick on pipes: I think it's an interesting concept, and I really do + like the code. My point is more fundamental: the problems we are solving are not simple, elegant + problems. We're dealing with the real world, where ugliness like asynchronous exceptions is the + norm. Elegant solutions are wonderful, but we can't have that elegance at the cost of + correctness. Any solution to the problems at hand needs to take into account all the bad stuff + that can happen.

+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/conduits-buffering.html b/public/blog/2012/01/conduits-buffering.html new file mode 100644 index 00000000..c35a2f81 --- /dev/null +++ b/public/blog/2012/01/conduits-buffering.html @@ -0,0 +1,276 @@ + Conduits, part 5: Buffering +

Conduits, part 5: Buffering

January 10, 2012

GravatarBy Michael Snoyman

Overview

+

This is the fifth (and probably final) part in a series on conduits, an + approach to handling streams of data. You can see the previous posts at:

+
  1. The Resource monad transformer
  2. +
  3. Sources
  4. +
  5. Sinks
  6. +
  7. Conduits
  8. +
+

This post is mostly about the concept of buffering. We'll also cover a few other miscealaneous + topics as well.

+ +

Inversion of Control

+

Buffering was actually one of the main motivations in the creation of the + conduit package. To see its importance, we need to consider the approach we've + seen so far, which we'll call inversion of control, or IoC.

+ +

Suppose you want to count how many newline characters there are in a file. In the standard + imperative approach, you would do someting like:

+
  1. Open the file
  2. +
  3. Pull some data into a buffer
  4. +
  5. Loop over the values in the buffer, incrementing a counter on each newline character
  6. +
  7. Return to 2
  8. +
  9. Close the file
  10. +
+

Notice that your code is explicitly calling out to other code and that code is returning + control back to your code. You have retained full control of the flow of execution of your + program. The conduit approach we've seen so far does not work this way. Instead, you + would:

+
  1. Write a sink that counts newlines and adds the result to an accumulator.
  2. +
  3. Connect the sink to a source
  4. +
+

There's no doubt in my mind that this is an easier approach. You don't have to worry about + opening and closing files or pulling data from the file. Instead, the data you need to process is + simply presented to you. This is the advantage of IoC: you can focus on specifically your piece + of the code.

+

We use this IoC approach all over Haskell: for example, instead of readMVar + and putMVar, you can use withMVar. Don't bother with + openFile and closeFile, just use withFile and + pass in a function that uses the Handle. Even C has a version of this: why + malloc and free when you could just + alloca?

+

Actually, that last one is a huge red herring. Of course you can't just use + alloca for everything. alloca only allocates memory locally on + the stack, not dynamically on the heap. There's no way to return your allocated memory outside + the current function.

+

But actually, the same restriction applies to the whole family of with + functions: you can never return an allocated resource outside of the "block". Usually this works + out just fine, but we need to recognize that this is a change in how we structure our + programs. Often times, with simple examples, this is a minor change. However, in larger settings + this can become very difficult to manage, bordering on impossible at times.

+

A web server

+

Let's say we're going to write a web server. We're going to use the following low-level + operations:

+
data Socket
+recv    :: Socket -> Int -> IO ByteString -- returns empty when the socket is closed
+sendAll :: Socket -> ByteString -> IO ()
+
+

We're up to the part where we need to implement the function handleConn that + handles an individual connection. It will look something like this:

+
data Request  -- request headers, HTTP version, etc
+data Response -- status code, response headers, resposne body
+type Application = Request -> IO Response
+handleConn :: Application -> Socket -> IO ()
+
+

What does our handleConn need to do? In broad strokes:

+
  1. Parse the request line
  2. +
  3. Parse the request headers
  4. +
  5. Construct the Request value
  6. +
  7. Pass Request to the Application and get back a + Response
  8. +
  9. Send the Response over the Socket
  10. +
+

We start off by writing steps 1 and 2 manually, without using conduits. We'll do this very + simply and just assume three space-separated strings. We end up with something that looks + like:

+
data RequestLine = RequestLine ByteString ByteString ByteString
+
+parseRequestLine :: Socket -> IO RequestLine
+parseRequestLine socket = do
+    bs <- recv socket 4096
+    let (method:path:version:ignored) = S8.words bs
+    return $ RequestLine method path version
+
+

There are two issues here: it doesn't handle the case where there are less than three words in + the chunk of data, and it throws away any extra data. We can definitely solve both of these + issues manually, but it's very tedious. It's much easier to implement this in terms of + conduits.

+
import Data.ByteString (ByteString)
+import qualified Data.ByteString as S
+import Data.Conduit
+import qualified Data.Conduit.Binary as CB
+import qualified Data.Conduit.List as CL
+
+data RequestLine = RequestLine ByteString ByteString ByteString
+
+parseRequestLine :: Sink ByteString IO RequestLine
+parseRequestLine = do
+    let space = toEnum $ fromEnum ' '
+    let getWord = do
+            CB.dropWhile (== space)
+            bss <- CB.takeWhile (/= space) =$ CL.consume
+            return $ S.concat bss
+
+    method <- getWord
+    path <- getWord
+    version <- getWord
+    return $ RequestLine method path version
+
+

This means that our code will automatically be supplied with more data as it comes in, and any + extra data will automatically be buffered in the Source, ready for the next time + it's used. Now we can easily structure our program together, demonstrating the power of the + conduits approach:

+
import Data.ByteString (ByteString)
+import Data.Conduit
+import Data.Conduit.Network (sourceSocket)
+import Control.Monad.IO.Class (liftIO)
+import Network.Socket (Socket)
+
+data RequestLine = RequestLine ByteString ByteString ByteString
+type Headers = [(ByteString, ByteString)]
+data Request = Request RequestLine Headers
+data Response = Response
+type Application = Request -> IO Response
+
+parseRequestHeaders :: Sink ByteString IO Headers
+parseRequestHeaders = undefined
+
+parseRequestLine :: Sink ByteString IO RequestLine
+parseRequestLine = undefined
+
+sendResponse :: Socket -> Response -> IO ()
+sendResponse = undefined
+
+handleConn :: Application -> Socket -> IO ()
+handleConn app socket = do
+    req <- runResourceT $ sourceSocket socket $$ do
+        requestLine <- parseRequestLine
+        headers <- parseRequestHeaders
+        return $ Request requestLine headers
+    res <- liftIO $ app req
+    liftIO $ sendResponse socket res
+
+

Whither the request body?

+

This is all great, until we realize we can't read the request body. The + Application is simply given the Request, and lives in the + IO monad. It has no access whatsoever to the incoming stream of data.

+

There's an easy fix for this actually: have the Application live in the + Sink monad. This is the very approach we took with + enumerator-based WAI 0.4. However, there are two problems:

+
  • People find it confusing. What people expect is that the Request + value would have a requestBody value of type Source.
  • +
  • This makes certain kinds of usage incredibly difficult. For example, trying to write an HTTP + proxy combining WAI and http-enumerator proved to be almost impossible.
  • +
+

This is the downside of inversion of control. Our code wants to be in control. It wants to be + given something to pull from, something to push to, and run with it. We need some solution to the + problem.

+ +

The simplest solution would be to just create a new Source and pass that to + the Application. Unfortunately, this will cause problems with our buffering. You + see, when we connect our source to the parseRequestLine and + parseRequestHeaders sinks, it made a call to recv. If the data + it received was not enough to cover all of the headers, it would issue another call. When it had + enough data, it would stop. However, odds are that it didn't stop exactly at the end of + the headers. It likely consumed a bit of the request body as well.

+

If we just create a new source and pass that to the request, it will be missing the beginning + of the request body. We need some way to pass that buffered data along.

+

BufferedSource

+

And so we finally get to introduce the last data type in conduits: + BufferedSource. This is an abstract data type, but all it really does is keep a + mutable reference to a buffer and an underlying Source. In order to create one + of these, you use the bufferSource function.

+
bufferSource ::Resource m => Source m a -> ResourceT m (BufferedSource m a)
+
+

This one little change is what allows us to easily solve our web server dilemna. Instead of + connecting a Source to our parsing Sinks, we use a + BufferedSource. At the end of each connection, any leftover data is put back on + the buffer. For our web server case, we can now create a BufferedSource, use + that to read the request line and headers, and then pass that same + BufferedSource to the application for reading the request body.

+

Typeclass

+

We want to be able to connect a buffered source to a sink, just like we would a regular source. + We would also like to be able to fuse it to a conduit. In order to make this convenient, conduit + has a typeclass, IsSource. There are instances provided for both + Source and BufferedSource. Both the connect + ($$) and left-fuse ($=) operators use this typeclass.

+

There's one "gotcha" in the BufferedSource instance of this typeclass, so + let's explain it. Suppose we want to write a file copy function, without any buffering. This is a + fairly standard usage of conduits:

+
sourceFile input $$ sinkFile output
+
+

When this line is run, both the input and output files are opened, the data is copied, and then + both files are closed. Let's change this example slightly to use buffering:

+
bsrc <- bufferSource $ sourceFile input
+bsrc $$ isolate 50 =$ sinkFile output1
+bsrc $$ sinkFile output2
+
+

When is the input file opened and closed? The opening occurs on the first line, when buffering + the source. And if we follow the normal rules from sources, the file should be closed after the + second line. However, if we did that, we couldn't reuse bsrc for line 3!

+

So instead, $$ does not close the file. As a result, you can pass a + buffered source to as many actions as you want, without concerns that the file handle has been + closed out from under you.

+ +

This presents one caveat: when you're finished with a buffered source, you should manually call + bsourceClose on it. However, as usual, this is merely an optimization, as the + source will automatically be closed when runResourceT is called.

+

Recapping the web server

+

So what exactly does our web server look like now?

+
import Data.ByteString (ByteString)
+import Data.Conduit
+import Data.Conduit.Network (sourceSocket)
+import Control.Monad.IO.Class (liftIO)
+import Network.Socket (Socket)
+
+data RequestLine = RequestLine ByteString ByteString ByteString
+type Headers = [(ByteString, ByteString)]
+data Request = Request RequestLine Headers (BufferedSource IO ByteString)
+data Response = Response
+type Application = Request -> ResourceT IO Response
+
+parseRequestHeaders :: Sink ByteString IO Headers
+parseRequestHeaders = undefined
+
+parseRequestLine :: Sink ByteString IO RequestLine
+parseRequestLine = undefined
+
+sendResponse :: Socket -> Response -> IO ()
+sendResponse = undefined
+
+handleConn :: Application -> Socket -> IO ()
+handleConn app socket = runResourceT $ do
+    bsrc <- bufferSource $ sourceSocket socket
+    requestLine <- bsrc $$ parseRequestLine
+    headers <- bsrc $$ parseRequestHeaders
+    let req = Request requestLine headers bsrc
+    res <- app req
+    liftIO $ sendResponse socket res
+
+

We've made a few minor changes. Firstly, the Application now lives in the + ResourceT IO monad. This isn't strictly necessary, but it's very convenient: + the application can now register cleanup actions that will only take place after the response has + been fully sent to the client.

+

But the major changes are in the handleConn function. We now start off by + buffering our source. This buffered source is then used twice in our function, and then passed + off to the application.

+

That's all folks!

+

Thanks for making it through this very long series of posts, I hope it's been informative. The + next step is to dive into the conduit packages on Hackage. Also, stay tuned in the next few weeks for + an all new, all conduit release of Yesod.

+

My hope is that the simplicity afforded by conduits will allow people not alone to become more + involved in playing around with code, but will let people make even more interesting combinations + of the existing packages. I'm looking forward to seeing the results.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/conduits-conduits.html b/public/blog/2012/01/conduits-conduits.html new file mode 100644 index 00000000..531d4bdb --- /dev/null +++ b/public/blog/2012/01/conduits-conduits.html @@ -0,0 +1,326 @@ + Conduits, part 4: Conduits +

Conduits, part 4: Conduits

January 4, 2012

GravatarBy Michael Snoyman

Review and Status

+

This is part 4 in the conduits series. You can see the previous posts at:

+
  1. The Resource monad transformer
  2. +
  3. Sources
  4. +
  5. Sinks
  6. +
+

This part covers the final major datatype in our package, conduits. While sources produce a + stream of data and sinks consume a stream, conduits transform a stream.

+

Also, just wanted to give an update on conduits activity. A few of the major enumerator + libraries have been converted over to conduits (http-conduit and + xml-conduit) and released to Hackage. Also, the Github versions of WAI + (including Warp), Persistent and Yesod have been converted over as well.

+

Not only does the code all work, it's already allowing enhancements we hadn't even thought of. All in all, the change to + conduits has been a very pleasant one.

+

Types

+

As we did previously, let's start off by looking at the types involved.

+
data ConduitResult input m output =
+    Producing (Conduit input m output) [output]
+  | Finished (Maybe input) [output]
+
+data Conduit input m output = Conduit
+    { conduitPush :: input -> ResourceT m (ConduitResult input m output)
+    , conduitClose :: ResourceT m [output]
+    }
+
+

This should look very similar to what we've seen with sinks. A conduit can be pushed to, in + which case it returns a result. A result either indicates that it is still producing data, or + that it is finished. When a conduit is closed, it returns some more output.

+

But let's examine the idiosyncracies a bit. Like sinks, we can only push one piece of input at + a time, and leftover data may be 0 or 1 pieces. However, there are a few changes:

+
  • When producing (the equivalent of processing for a sink), we can return output. This is + because a conduit will product a new stream of output instead of producing a single output value + at the end of processing.
  • +
  • A sink always returns a single output value, while a conduit returns 0 or more outputs (a + list). To understand why, consider conduits such as concatMap (produces + multiple outputs for one input) and filter (returns 0 or 1 output for each + input).
  • +
  • We have no special constructor like SinkNoData. That's because we provide no + Monad instance for conduits. We'll see later how you can still use a familiar + Monadic approach to creating conduits.
  • +
+

Overall conduits should seem very similar to what we've covered so far.

+

Simple conduits

+

We'll start off by defining some simple conduits that don't have any state.

+
import Prelude hiding (map, concatMap)
+import Data.Conduit
+
+-- A simple conduit that just passes on the data as-is.
+passThrough :: Monad m => Conduit input m input
+passThrough = Conduit
+    { conduitPush = \input -> return $ Producing passThrough [input]
+    , conduitClose = return []
+    }
+
+-- map values in a stream
+map :: Monad m => (input -> output) -> Conduit input m output
+map f = Conduit
+    { conduitPush = \input -> return $ Producing (map f) [f input]
+    , conduitClose = return []
+    }
+
+-- map and concatenate
+concatMap :: Monad m => (input -> [output]) -> Conduit input m output
+concatMap f = Conduit
+    { conduitPush = \input -> return $ Producing (concatMap f) $ f input
+    , conduitClose = return []
+    }
+
+

Stateful conduits

+

Of course, not all conduits can be declared without state. Doing so on the bare metal is not + too difficult.

+
import Prelude hiding (reverse)
+import qualified Data.List
+import Data.Conduit
+import Control.Monad.Trans.Resource
+
+-- Reverse the elements in the stream. Note that this has the same downside as
+-- the standard reverse function: you have to read the entire stream into
+-- memory before producing any output.
+reverse :: Resource m => Conduit input m input
+reverse =
+    mkConduit []
+  where
+    mkConduit state = Conduit (push state) (close state)
+    push state input = return $ Producing (mkConduit $ input : state) []
+    close state = return state
+
+-- Same thing with sort: it will pull everything into memory
+sort :: (Ord input, Resource m) => Conduit input m input
+sort =
+    mkConduit []
+  where
+    mkConduit state = Conduit (push state) (close state)
+    push state input = return $ Producing (mkConduit $ input : state) []
+    close state = return $ Data.List.sort state
+
+

But we can do better. Just like sourceState and sinkState, we + have conduitState to simplify things.

+
import Prelude hiding (reverse)
+import qualified Data.List
+import Data.Conduit
+
+-- Reverse the elements in the stream. Note that this has the same downside as
+-- the standard reverse function: you have to read the entire stream into
+-- memory before producing any output.
+reverse :: Resource m => Conduit input m input
+reverse =
+    conduitState [] push close
+  where
+    push state input = return $ StateProducing (input : state) []
+    close state = return state
+
+-- Same thing with sort: it will pull everything into memory
+sort :: (Ord input, Resource m) => Conduit input m input
+sort =
+    conduitState [] push close
+  where
+    push state input = return $ StateProducing (input : state) []
+    close state = return $ Data.List.sort state
+
+

Using conduits

+

The way Conduits interact with the rest of the package is via + fusing. A conduit can be fused into a source, producing a new source, fused into a + sink to produce a new sink, or fused with another conduit to produce a new conduit. It's best to + just look at the fusion operators.

+
-- Left fusion: source + conduit = source
+($=) :: (Resource m, IsSource src) => src m a -> Conduit a m b -> Source m b
+
+-- Right fusion: conduit + sink = sink
+(=$) :: Resource m => Conduit a m b -> Sink b m c -> Sink a m c
+
+-- Middle fusion: conduit + conduit = conduit
+(=$=) :: Resource m => Conduit a m b -> Conduit b m c -> Conduit a m c
+
+

Using these operators is straightforward.

+
useConduits = do
+    runResourceT
+          $  CL.sourceList [1..10]
+          $= reverse
+          $= CL.map show
+          $$ CL.consume
+
+    -- equivalent to
+    runResourceT
+          $  CL.sourceList [1..10]
+          $$ reverse
+          =$ CL.map show
+          =$ CL.consume
+
+    -- and equivalent to
+    runResourceT
+          $  CL.sourceList [1..10]
+          $$ (reverse =$= CL.map show)
+          =$ CL.consume
+
+

There is in fact one last way of expressing the same idea. I'll leave it as an exercise to the + reader to discover it.

+

It may seem like all these different approaches are redundant. While occasionally you can in + fact choose whichever approach you feel like using, in many cases you will need a specific + approach. For example:

+
  • If you have a stream of numbers, and you want to apply a conduit (e.g., map + show) to only some of the stream that will be passed to a specific sink, you'll want + to use the right fusion operator.
  • +
  • If you're reading a file, and want to parse the entire file as textual data, you'll want to + use left fusion to convert the entire stream.
  • +
  • If you want to create reusable conduits that combine together individual, smaller conduits, + you'll use middle fusion.
  • +
+

Data loss

+

Let's forget about conduits for a moment. Instead, suppose we want to write a program- using + plain old lists- that will take a list of numbers, apply some kind of transformation to them, + take the first five transformed values and do something with them, and then do something else + with the remaining non-transformed values. For example, we want something like:

+
main = do
+    let list = [1..10]
+        transformed = map show list
+        (begin, end) = splitAt 5 transformed
+        untransformed = map read end
+    mapM_ putStrLn begin
+    print $ sum untransformed
+
+

But clearly this isn't a good general solution, since we don't want to have to transform and + then untransform every element in the list. For one thing, we may not always have an inverse + function. Another issue is efficiency. In this case, we can write something more efficient:

+
main = do
+    let list = [1..10]
+        (begin, end) = splitAt 5 list
+        transformed = map show begin
+    mapM_ putStrLn transformed
+    print $ sum end
+
+

Note the change: we perform our split before transforming any elements. This works because, + with map, we have a 1-to-1 correspondence between the input and output elements. + So splitting at 5 before or after mapping show is the same thing. But what + happens if we replace map show with something more devious.

+
deviousTransform =
+    concatMap go
+  where
+    go 1 = [show 1]
+    go 2 = [show 2, "two"]
+    go 3 = replicate 5 "three"
+    go x = [show x]
+
+

We no longer have the 1-to-1 correspondence. As a result, we can't use the second method. But + it's even worse: we can't use the first method either, since there's no inverse of our + deviousTransform.

+

There's only one solution to the problem that I'm aware of: transform elements one at a time. + The final program looks like this:

+
deviousTransform 1 = [show 1]
+deviousTransform 2 = [show 2, "two"]
+deviousTransform 3 = replicate 5 "three"
+deviousTransform x = [show x]
+
+transform5 :: [Int] -> ([String], [Int])
+transform5 list =
+    go [] list
+  where
+    go output (x:xs)
+        | newLen >= 5 = (take 5 output', xs)
+        | otherwise = go output' xs
+      where
+        output' = output ++ deviousTransform x
+        newLen = length output'
+
+    -- Degenerate case: not enough input to make 5 outputs
+    go output [] = (output, [])
+
+main = do
+    let list = [1..10]
+        (begin, end) = transform5 list
+    mapM_ putStrLn begin
+    print $ sum end
+
+

The final output of this program is

1
+2
+two
+three
+three
+49
What's important + to note is that the number 3 is converted into five copies of the word "three", yet only two of + them show up in the output. The rest are discarded in the take 5 call.

+

This whole exercise is just to demonstrate the issue of data loss in conduits. By forcing + conduits to accept only one input at a time, we avoid the issue of transforming too many elements + at once. That doesn't mean we don't lose any data: if a conduit produces too much output + for the receiving sink to handle, some of it may be lost.

+

To put all this another way: conduits avoid chunking to get away from data loss. This is not an + issue unique to conduits. If you look in the implementation of concatMapM for + enumerator, you'll see that it forces elements to be handled one at a time. + In conduits, we opted to force the issue at the type level.

+

SequencedSink

+

Suppose we want to be able to combine up existing conduits and sinks to produce a new, more + powerful conduit. For example, we want to write a conduit that takes a stream of numbers and sums + up every five. In other words, for the input [1..50], it should result in the + sequence [15,40,65,90,115,140,165,190,215,240]. We can definitely do this with + the low-level conduit interface.

+
sum5Raw :: Resource m => Conduit Int m Int
+sum5Raw =
+    conduitState (0, 0) push close
+  where
+    push (total, count) input
+        | newCount == 5 = return $ StateProducing (0, 0) [newTotal]
+        | otherwise     = return $ StateProducing (newTotal, newCount) []
+      where
+        newTotal = total + input
+        newCount = count + 1
+    close (total, count)
+        | count == 0 = return []
+        | otherwise  = return [total]
+
+

But this is frustrating, since we already have all the tools we need to do this at a high + level! There's the fold sink for adding up the numbers, and the + isolate conduit which will only allow up to a certain number of elements to be + passed to a sink. Can't we combine these somehow?

+

The answer is a SequencedSink. The idea is to create a normal + Sink, except it returns a special output called a + SequencedSinkResponse. This value can emit new output, stop processing data, or + transfer control to a new conduit. (See the Haddocks for more information.) Then we can turn this + into a Conduit using the sequenceSink function. This function + also takes some state value that gets passed through to the sink.

+

So we can rewrite sum5Raw in a much more high-level manner.

+
sum5 :: Resource m => Conduit Int m Int
+sum5 = sequenceSink () $ \() -> do
+    nextSum <- CL.isolate 5 =$ CL.fold (+) 0
+    return $ Emit () [nextSum]
+
+

All of the () in there are simply the unused state variable being passed + around, they can be ignored. Otherwise, we're doing exactly what we want. We fuse + isolate to fold to get the sum of the next five elements from + the stream. We then emit that value, and start all over again.

+

Let's say we want to modify this slightly. We want to get the first 8 sums, and then pass + through the remaining values, multiplied by 2. We can keep track of how many values we've + returned in our state, and then use the StartConduit constructor to pass control + to the multiply-by-2 conduit next.

+
sum5Pass :: Resource m => Conduit Int m Int
+sum5Pass = sequenceSink 0 $ \count -> do
+    if count == 8
+        then return $ StartConduit $ CL.map (* 2)
+        else do
+            nextSum <- CL.isolate 5 =$ CL.fold (+) 0
+            return $ Emit (count + 1) [nextSum]
+
+

These are obviously very contrived examples, but I hope it makes clear the power and simplicity + available from this approach.

+

Summary

+

We're nearing the end of our conduits series. The last remaining major point is buffering and + resumable sources. (I would have included it here, but (a) it doesn't exactly fit with the rest + of the material and (b) it's 10:30 at night and I want to go to sleep.) In addition, we'll try to + cover some real-life use cases for conduits, and give examples of where libraries like + http-conduit and the upcoming conduit-based wai can be + used together.

+ +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/http-conduit.html b/public/blog/2012/01/http-conduit.html new file mode 100644 index 00000000..1157dc6b --- /dev/null +++ b/public/blog/2012/01/http-conduit.html @@ -0,0 +1,207 @@ + http-conduit +

http-conduit

January 6, 2012

GravatarBy Michael Snoyman

http-conduit

+

Most of Yesod is about serving content over HTTP. But that's only half the story: someone has + to receive it. And even when you're writing a web app, sometimes that someone will be you. If you + want to consume content from other services or interact with RESTful APIs, you'll need to write + client code. And the recommended approach for that is http-conduit.

+

This chapter is not directly connected to Yesod, and will be generally useful for anyone + wanting to make HTTP requests.

+

Synopsis

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Conduit -- the main module
+
+-- The streaming interface uses conduits
+import Data.Conduit
+import Data.Conduit.Binary (sinkFile)
+
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    -- Simplest query: just download the information from the given URL as a
+    -- lazy ByteString.
+    simpleHttp "http://www.example.com/foo.txt" >>= L.writeFile "foo.txt"
+
+    -- Use the streaming interface instead. We need to run all of this inside a
+    -- ResourceT, to ensure that all our connections get properly cleaned up in
+    -- the case of an exception.
+    runResourceT $ do
+        -- We need a Manager, which keeps track of open connections. simpleHttp
+        -- creates a new manager on each run (i.e., it never reuses
+        -- connections).
+        manager <- liftIO $ newManager def
+
+        -- A more efficient version of the simpleHttp query above. First we
+        -- parse the URL to a request.
+        req <- liftIO $ parseUrl "http://www.example.com/foo.txt"
+
+        -- Now get the response
+        res <- http req manager
+
+        -- And finally stream the value to a file
+        responseBody res $$ sinkFile "foo.txt"
+
+        -- Make it a POST request, don't follow redirects, and accept any
+        -- status code.
+        let req2 = req
+                { method = "POST"
+                , redirectCount = 0
+                , checkStatus = \_ _ -> Nothing
+                }
+        res2 <- http req2 manager
+        responseBody res2 $$ sinkFile "post-foo.txt"
+
+

Concepts

+

The simplest way to make a request in http-conduit is with the + simpleHttp function. This function takes a String giving a URL + and returns a ByteString with the contents of that URL. But under the surface, + there are a few more steps:

+
  • A new connection Manager is allocated.
  • +
  • The URL is parsed to a Request. If the URL is invalid, then an exception is + thrown.
  • +
  • The HTTP request is made, following any redirects from the server.
  • +
  • If the response has a status code outside the 200-range, an exception is thrown.
  • +
  • The response body is read into memory and returned.
  • +
  • runResourceT is called, which will free up any resources (e.g., the open + socket to the server).
  • +
+

If you want more control of what's going on, then you can configure any of the steps above + (plus a few more) by explicitly creating a Request value, allocating your + Manager manually, and using the http and + httpLbs functions.

+

Request

+

The easiest way to creating a Request is with the parseUrl + function. This function will return a value in any Failure monad, such as + Maybe or IO. The last of those is the most commonly used, and + results in a runtime exception whenever an invalid URL is provided. However, you can use a + different monad if, for example, you want to validate user input.

+
import Network.HTTP.Conduit
+import System.Environment (getArgs)
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    args <- getArgs
+    case args of
+        [urlString] ->
+            case parseUrl urlString of
+                Nothing -> putStrLn "Sorry, invalid URL"
+                Just req -> withManager $ \manager -> do
+                    Response _ _ lbs <- httpLbs req manager
+                    liftIO $ L.putStr lbs
+        _ -> putStrLn "Sorry, please provide example one URL"
+
+

The Request type is abstract so that http-conduit can add new + settings in the future without breaking the API (see the Settings Type + chapter for more information). In order to make changes to individual records, you use record + notation. For example, a modification to our program that issues HEAD requests + and prints the response headers would be:

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Conduit
+import System.Environment (getArgs)
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    args <- getArgs
+    case args of
+        [urlString] ->
+            case parseUrl urlString of
+                Nothing -> putStrLn "Sorry, invalid URL"
+                Just req -> withManager $ \manager -> do
+                    let reqHead = req { method = "HEAD" }
+                    Response status headers _ <- http reqHead manager
+                    liftIO $ do
+                        print status
+                        mapM_ print headers
+        _ -> putStrLn "Sorry, please provide example one URL"
+
+

There are a number of different configuration settings in the API, some noteworthy ones + are:

+
proxy
+
Allows you to pass the request through the given proxy server.
+ + +
redirectCount
+
Indicate how many redirects to follow. Default is 10.
+ + +
checkStatus
+
Check the status code of the return value. By default, gives an exception for any non-2XX + response.
+ + +
requestBody
+
The request body to be sent. Be sure to also update the method. For the + common case of url-encoded data, you can use the urlEncodedBody function.
+ +
+

Manager

+

The connection manager allows you to reuse connections. When making multiple queries to a + single server (e.g., accessing Amazon S3), this can be critical for creating efficient code. A + manager will keep track of multiple connections to a given server (taking into account port and + SSL as well), automatically reaping unused connections as needed. When you make a request, + http-conduit first tries to check out an existing connection. When you're + finished with the connection (if the server allows keep-alive), the connection is returned to the + manager. If anything goes wrong, the connection is closed.

+

To keep our code exception-safe, we use the ResourceT monad transformer. All + this means for you is that your code needs to be wrapped inside a call to + runResourceT, either implicitly or explicitly, and that code inside that block + will need to liftIO to perform normal IO actions.

+

There are two ways you can get ahold of a manager. newManager will return a + manager that will not be automatically closed (you can use closeManager to do so + manually), while withManager will start a new ResourceT block, + allow you to use the manager, and then automatically close the ResourceT when + you're done. If you want to use a ResourceT for an entire application, and have + no need to close it, you should probably use newManager.

+

One other thing to point out: you obviously don't want to create a new manager for each and + every request; that would defeat the whole purpose. You should create your + Manager early and then share it.

+

Response

+

The Response datatype has three pieces of information: the status code, the + response headers, and the response body. The first two are straight-forward; let's discuss the + body.

+

The Response type has a type variable to allow the response body to be of + multiple types. If you want to use http-conduit's streaming interface, you want + this to be a Source. For the simple interface, it will be a lazy + ByteString. One thing to note is that, even though we use a lazy + ByteString, the entire response is held in memory. In other words, we + perform no lazy I/O in this package.

+ +

http and httpLbs

+

So let's tie it together. The http function gives you access to the streaming + interface (i.e., it returns a Response using a BufferedSource) + while httpLbs returns a lazy ByteString. Both of these return + values in the ResourceT transformer so that they can access the + Manager and have connections handled properly in the case of exceptions.

+

Here are a bunch of random comments about the library. Consider it the FAQ. Likewise, if you + have a question, please ask it so that this section can be updated.

+
How do I ignore the remainder of a response body without reading it?
+
Connect it to the sinkNull sink.
+ + +
I want to share a single Manager across my entire application. How can I do + this?
+
Use the newManager function at the beginning of your application, and pass + it to all functions that need it.
+ +
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/new-pet-peeve.html b/public/blog/2012/01/new-pet-peeve.html new file mode 100644 index 00000000..4f3154bf --- /dev/null +++ b/public/blog/2012/01/new-pet-peeve.html @@ -0,0 +1,29 @@ + My New Pet Peeve +

My New Pet Peeve

January 24, 2012

GravatarBy Michael Snoyman

Apologies to the targets of this post.

+

Just wanted to share a new pet peeve I've developed. Suppose someone comes up with a new approach to achieving some goal. The previous approaches have issues that have been clearly demonstrated. The new approach shows that those issues exist, actually break things in real life, and then offers a new implementation that solves the issues.

+

Are the new approaches perfect? Nothing ever is. But they achieve the goals we have today, and are workable tools.

+

Along comes someone, doesn't like something about the new approach (has unexpected semantics, or perhaps a name they dislike). They write a blog post, or an email to Haskell cafe, or a Google+ post, or a Reddit comment explaining how this new approach is broken/unsound/yucky because of X. Full stop.

+

This critique irks me, because:

+
  • It's often times based in someone's lack of understanding of the problem at hand.
  • +
  • It ignores the fact that in the real world, the complaints haven't actually caused serious problems.
  • +
  • There's no explanation of how to fix it, besides some vague hand-waving.
  • +
+

I'm all in favor of healthy discussion, but throwing around these words without hard evidence is meaningless. If you must say something vague, tell everyone that you're being vague!

+
  • "I can't say exactly why, but this approach feels wrong."
  • +
  • "This doesn't fit with the semantics I would expect."
  • +
+

And your comments will have far more of a holding with me if you can actually show that your goals are achievable.

+

</rant>

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/persisent-0-7-yesod-0-10-beta.html b/public/blog/2012/01/persisent-0-7-yesod-0-10-beta.html new file mode 100644 index 00000000..01b8503b --- /dev/null +++ b/public/blog/2012/01/persisent-0-7-yesod-0-10-beta.html @@ -0,0 +1,33 @@ + Persistent 0.7, Yesod 0.10 beta +

Persistent 0.7, Yesod 0.10 beta

January 25, 2012

GravatarBy Michael Snoyman

I'm very happy to announce the newest release of Persistent. Version 0.7 is now on Hackage, and supports three major changes:

+
  • The typeclass hierarchy has been split up into PersistStore and PersistQuery. This means that it should be much easier to add backends for data stores like Redis. (Thanks to Greg Weber for this.)
  • +
  • There's a much more convenient raw SQL interface with automated data marshaling. (Thanks to Felipe Lessa for this.)
  • +
  • The EntityDef and Template Haskell code has been drastically cleaned up and simplified. This makes it easier to add new features as separate TH components, and fixes a number of bugs related to automated migrations with field renames. (I'll take credit here.)
  • +
+

Persistent 0.7 will be used in the upcoming Yesod 0.10... Speaking of which, Yesod 0.10 is now officially in beta! I strongly encourage all users to give this a test drive. Greg has put together a 0.10 upgrade guide, and we'll likely have a blog post giving the typical "here's how I upgraded Haskellers.com."

+

We'll go into the exciting new features of Yesod 0.10 in the actual release announcement, but for testing purposes, there hasn't been a significant number of changes. This release was mostly focused on API cleanup. Remember: the goal is to turn Yesod 0.10 into 1.0!

+

The only feature we still plan to add before the official release is merging in Nubis's test branch. This is a really nice set of features, not only adding easy, high-level testing to the scaffolding, but in general making the scaffolding a more user-friendly site. Thank you Nubis!

+

In order to install the beta, you can either install from Github or from the Yesod Yackage site. To install from Github:

+
  1. cabal update
  2. +
  3. git clone https://github.com/yesodweb/yesod
  4. +
  5. cd yesod
  6. +
  7. git submodule update --init
  8. +
  9. ./scripts/install
  10. +
+

To install from Yackage, add the following line to your ~/.cabal/config file:

+
remote-repo: yesodweb-yackage:http://yackage.yesodweb.com/
+
+

And then run cabal update && cabal install yesod.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/warp-conduits.html b/public/blog/2012/01/warp-conduits.html new file mode 100644 index 00000000..ab0f2060 --- /dev/null +++ b/public/blog/2012/01/warp-conduits.html @@ -0,0 +1,22 @@ + Warp Conduits Released! +

Warp Conduits Released!

January 19, 2012

GravatarBy Michael Snoyman

The Yesod team is very happy to announce the first conduit-based release of WAI and Warp. We've received a lot of positive feedback on the new API, our tests show it working correctly, and benchmarks put it at the same (fast) speed as previous releases. All in all, we're very happy with this new version.

+

We've bumped the version number on all WAI packages to 1.0. We've done this for three reasons:

+
  • Indicate that this is a significant change.
  • +
  • Make it clear that after years of active development and use, we feel that the WAI ecosystem is now fully stable.
  • +
  • Allow anyone interested in continuing the enumerator-based WAI to use the 0.* version numbers to do so. Please contact me if you're interested.
  • +
+

In addition to upgrading all WAI packages, we're also releasing a new package: warp-tls. This package allows you to serve your WAI applications over a secure connection, using Vincent Hanquez's wonderful tls package. The architecture has been set up in a modular enough fashion that creating other SSL backends (e.g., openssl, gnutls) is entirely possible. If anyone is interested in either using or developing such a backend, please let me know.

+

The upcoming 0.10 release of Yesod is built fully on WAI/Warp 1.0. This release is our first step towards a Yesod 0.10 beta release (probably early next week).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/01/wiki-chat-subsite.html b/public/blog/2012/01/wiki-chat-subsite.html new file mode 100644 index 00000000..eb9c2e3f --- /dev/null +++ b/public/blog/2012/01/wiki-chat-subsite.html @@ -0,0 +1,343 @@ + Wiki: markdown, chat subsite, event source +

Wiki: markdown, chat subsite, event source

January 24, 2012

GravatarBy Michael Snoyman

+

This example will tie together a few different ideas. We'll start with a chat subsite, which + allows us to embed a chat widget on any page. We'll use the HTML 5 event source API to handle + sending events from the server to the client.

+
-- @Chat.hs
+{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+TemplateHaskell, FlexibleInstances, MultiParamTypeClasses,
+FlexibleContexts
+#-}
+-- | This modules defines a subsite that allows you to insert a chat box on
+-- any page of your site. It uses eventsource for sending the messages from
+-- the server to the browser.
+module Chat where
+
+import Yesod
+import Control.Concurrent.Chan (Chan, dupChan, writeChan)
+import Data.Text (Text)
+import Network.Wai.EventSource (ServerEvent (..), eventSourceApp)
+import Language.Haskell.TH.Syntax (Type (VarT), Pred (ClassP), mkName)
+import Blaze.ByteString.Builder.Char.Utf8 (fromText)
+import Data.Monoid (mappend)
+
+-- | Our subsite foundation. We keep a channel of events that all connections
+-- will share.
+data Chat = Chat (Chan ServerEvent)
+
+-- | We need to know how to check if a user is logged in and how to get
+-- his/her username (for printing messages).
+class (Yesod master, RenderMessage master FormMessage)
+        => YesodChat master where
+    getUserName :: GHandler sub master Text
+    isLoggedIn :: GHandler sub master Bool
+
+-- Now we set up our subsite. The first argument is the subsite, very similar
+-- to how we've used mkYesod in the past. The second argument is specific to
+-- subsites. What it means here is "the master site must be an instance of
+-- YesodChat".
+--
+-- We define two routes: a route for sending messages from the client to the
+-- server, and one for opening up the event stream to receive messages from
+-- the server.
+mkYesodSub "Chat"
+    [ ClassP ''YesodChat [VarT $ mkName "master"]
+    ] [parseRoutes|
+/send SendR POST
+/recv ReceiveR GET
+|]
+
+-- | Get a message from the user and send it to all listeners.
+postSendR :: YesodChat master => GHandler Chat master ()
+postSendR = do
+    from <- getUserName
+
+    -- Note that we're using GET parameters for simplicity of the Ajax code.
+    -- This could easily be switched to POST. Nonetheless, our overall
+    -- approach is still RESTful since this route can only be accessed via a
+    -- POST request.
+    body <- runInputGet $ ireq textField "message"
+
+    -- Get the channel
+    Chat chan <- getYesodSub
+
+    -- Send an event to all listeners with the user's name and message.
+    liftIO $ writeChan chan $ ServerEvent Nothing Nothing $ return $
+        fromText from `mappend` fromText ": " `mappend` fromText body
+
+-- | Send an eventstream response with all messages streamed in.
+getReceiveR :: GHandler Chat master ()
+getReceiveR = do
+    -- First we get the main channel
+    Chat chan0 <- getYesodSub
+
+    -- We duplicated the channel, which allows us to create broadcast
+    -- channels.
+    chan <- liftIO $ dupChan chan0
+
+    -- Now we use the event source API. eventSourceApp takes two parameters:
+    -- the channel of events to read from, and the WAI request. It returns a
+    -- WAI response, which we can return with sendWaiResponse.
+    req <- waiRequest
+    res <- lift $ eventSourceApp chan req
+    sendWaiResponse res
+
+-- | Provide a widget that the master site can embed on any page.
+chatWidget :: YesodChat master
+           => (Route Chat -> Route master)
+           -> GWidget sub master ()
+-- This toMaster argument tells us how to convert a Route Chat into a master
+-- route. You might think this is redundant information, but taking this
+-- approach means we can have multiple chat subsites in a single site.
+chatWidget toMaster = do
+    -- Get some unique identifiers to help in creating our HTML/CSS. Remember,
+    -- we have no idea what the master site's HTML will look like, so we
+    -- should not assume we can make up identifiers that won't be reused.
+    -- Also, it's possible that multiple chatWidgets could be embedded in the
+    -- same page.
+    chat <- lift newIdent   -- the containing div
+    output <- lift newIdent -- the box containing the messages
+    input <- lift newIdent  -- input field from the user
+
+    ili <- lift isLoggedIn  -- check if we're already logged in
+    if ili
+        then do
+            -- Logged in: show the widget
+            [whamlet|
+<div ##{chat}>
+    <h2>Chat
+    <div ##{output}>
+    <input ##{input} type=text placeholder="Enter Message">
+|]
+            -- Just some CSS
+            toWidget [lucius|
+##{chat} {
+    position: absolute;
+    top: 2em;
+    right: 2em;
+}
+##{output} {
+    width: 200px;
+    height: 300px;
+    border: 1px solid #999;
+    overflow: auto;
+}
+|]
+            -- And now that Javascript
+            toWidgetBody [julius|
+// Set up the receiving end
+var output = document.getElementById("#{output}");
+var src = new EventSource("@{toMaster ReceiveR}");
+src.onmessage = function(msg) {
+    // This function will be called for each new message.
+    var p = document.createElement("p");
+    p.appendChild(document.createTextNode(msg.data));
+    output.appendChild(p);
+
+    // And now scroll down within the output div so the most recent message
+    // is displayed.
+    output.scrollTop = output.scrollHeight;
+};
+
+// Set up the sending end: send a message via Ajax whenever the user hits
+// enter.
+var input = document.getElementById("#{input}");
+input.onkeyup = function(event) {
+    var keycode = (event.keyCode ? event.keyCode : event.which);
+    if (keycode == '13') {
+        var xhr = new XMLHttpRequest();
+        var val = input.value;
+        input.value = "";
+        var params = "?message=" + encodeURI(val);
+        xhr.open("POST", "@{toMaster SendR}" + params);
+        xhr.send(null);
+    }
+}
+|]
+        else do
+            -- User isn't logged in, give a not-logged-in message.
+            master <- lift getYesod
+            [whamlet|
+<p>
+    You must be #
+    $maybe ar <- authRoute master
+        <a href=@{ar}>logged in
+    $nothing
+        logged in
+    \ to chat.
+|]
+
+

This module stands on its own, and can be used in any application. Next we'll provide such a + driver application: a wiki. Our wiki will have a hard-coded homepage, and then a wiki section of + the site. We'll be using multiple dynamic pieces to allow an arbitrary hierarchy of + pages within the Wiki.

+

For storage, we'll just use a mutable reference to a Map. For a production + application, this should be replaced with a proper database. The content will be stored and + served as Markdown. yesod-auth's dummy plugin will provide us with (fake) + authentication.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+TemplateHaskell, FlexibleInstances, MultiParamTypeClasses,
+FlexibleContexts
+#-}
+import Yesod
+import Yesod.Auth
+import Yesod.Auth.Dummy (authDummy)
+import Chat
+import Control.Concurrent.Chan (Chan, newChan)
+import Network.Wai.Handler.Warp (run)
+import Data.Text (Text)
+import qualified Data.Text.Lazy as TL
+import qualified Data.IORef as I
+import qualified Data.Map as Map
+import Text.Markdown (markdown, def)
+
+-- | Our foundation type has both the chat subsite and a mutable reference to
+-- a map of all our wiki contents. Note that the key is a list of Texts, since
+-- a wiki can have an arbitrary hierarchy.
+--
+-- In a real application, we would want to store this information in a
+-- database of some sort.
+data Wiki = Wiki
+    { getChat :: Chat
+    , wikiContent :: I.IORef (Map.Map [Text] Text)
+    }
+
+-- Set up our routes as usual.
+mkYesod "Wiki" [parseRoutes|
+/ RootR GET                 -- the homepage
+/wiki/*Texts WikiR GET POST -- note the multipiece for the wiki hierarchy
+/chat ChatR Chat getChat    -- the chat subsite
+/auth AuthR Auth getAuth    -- the auth subsite
+|]
+
+instance Yesod Wiki where
+    authRoute _ = Just $ AuthR LoginR -- get a working login link
+
+    -- Our custom defaultLayout will add the chat widget to every page.
+    -- We'll also add login and logout links to the top.
+    defaultLayout widget = do
+        pc <- widgetToPageContent $ widget >> chatWidget ChatR
+        mmsg <- getMessage
+        hamletToRepHtml [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <div .message>#{msg}
+        <nav>
+            <a href=@{AuthR LoginR}>Login
+            \ | #
+            <a href=@{AuthR LogoutR}>Logout
+        ^{pageBody pc}
+|]
+
+-- Fairly standard YesodAuth instance. We'll use the dummy plugin so that you
+-- can create any name you want, and store the login name as the AuthId.
+instance YesodAuth Wiki where
+    type AuthId Wiki = Text
+    authPlugins _ = [authDummy]
+    loginDest _ = RootR
+    logoutDest _ = RootR
+    getAuthId = return . Just . credsIdent
+    authHttpManager = error "authHttpManager" -- not used by authDummy
+
+-- Just implement authentication based on our yesod-auth usage.
+instance YesodChat Wiki where
+    getUserName = requireAuthId
+    isLoggedIn = do
+        ma <- maybeAuthId
+        return $ maybe False (const True) ma
+
+instance RenderMessage Wiki FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Nothing special here, just giving a link to the root of the wiki.
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<p>Welcome to the Wiki!
+<p>
+    <a href=@{wikiRoot}>Wiki root
+|]
+  where
+    wikiRoot = WikiR []
+
+-- A form for getting wiki content
+wikiForm mtext = renderDivs $ areq textareaField "Page body" mtext
+
+-- Show a wiki page and an edit form
+getWikiR :: [Text] -> Handler RepHtml
+getWikiR page = do
+    -- Get the reference to the contents map
+    icontent <- fmap wikiContent getYesod
+
+    -- And read the map from inside the reference
+    content <- liftIO $ I.readIORef icontent
+
+    -- Lookup the contents of the current page, if available
+    let mtext = Map.lookup page content
+
+    -- Generate a form with the current contents as the default value.
+    -- Note that we use the Textarea wrapper to get a <textarea>.
+    ((_, form), _) <- generateFormPost $ wikiForm $ fmap Textarea mtext
+    defaultLayout $ do
+        case mtext of
+            -- We're treating the input as markdown. The markdown package
+            -- automatically handles XSS protection for us.
+            Just text -> toWidget $ markdown def $ TL.fromStrict text
+            Nothing -> [whamlet|<p>Page does not yet exist|]
+        [whamlet|
+<h2>Edit page
+<form method=post>
+    ^{form}
+    <div>
+        <input type=submit>
+|]
+
+-- Get a submitted wiki page and updated the contents.
+postWikiR :: [Text] -> Handler RepHtml
+postWikiR page = do
+    icontent <- fmap wikiContent getYesod
+    content <- liftIO $ I.readIORef icontent
+    let mtext = Map.lookup page content
+    ((res, form), _) <- runFormPost $ wikiForm $ fmap Textarea mtext
+    case res of
+        FormSuccess (Textarea t) -> do
+            liftIO $ I.atomicModifyIORef icontent $
+                \m -> (Map.insert page t m, ())
+            setMessage "Page updated"
+            redirect $ WikiR page
+        _ -> defaultLayout [whamlet|
+<form method=post>
+    ^{form}
+    <div>
+        <input type=submit>
+|]
+
+main :: IO ()
+main = do
+    -- Create our server event channel
+    chan <- newChan
+
+    -- Initially have a blank database of wiki pages
+    icontent <- I.newIORef Map.empty
+
+    -- Run our app
+    warpDebug 3000 $ Wiki (Chat chan) icontent
+
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/02/answers-about-the-book.html b/public/blog/2012/02/answers-about-the-book.html new file mode 100644 index 00000000..5c34b36e --- /dev/null +++ b/public/blog/2012/02/answers-about-the-book.html @@ -0,0 +1,24 @@ + Answers about the Yesod book +

Answers about the Yesod book

February 2, 2012

GravatarBy Michael Snoyman

Thanks to everyone for the very positive response about the upcoming Yesod book! There were various questions floating around the comments, Google+, Reddit, and email, so I just wanted to give as thorough a set of answers as I could.

+
  • The book will be available in paperback and ebook format, supporting Mobi and ePub.
  • +
  • For the ebook, you can get it directly from O'Reilly, which means you'll get updates. It will also be available from various other sources: Amazon, Nook store, iBookstore, Kobo, and more.
  • +
  • I know Amazon says the book is 100 pages; that was just an initial guess from who knows when. The book is close to 300 pages.
  • +
  • The book is being updated as we're making changes to Yesod. The goal is to release the book not long after Yesod 1.0. The book should cover that release. +
    • The current book on this site covers 0.9. There's a beta version that covers 0.10.
    • +
  • +
  • Some people believe the book is imbued with magical powers, and can cure people of all ailments and turn lead into gold. That's absolutely ridiculous; lead can't be turned into gold.
  • +
+

If you have any other questions, let me know, I'll try to keep the post up-to-date with answers.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/02/call-for-gsoc-2012.html b/public/blog/2012/02/call-for-gsoc-2012.html new file mode 100644 index 00000000..19ba967e --- /dev/null +++ b/public/blog/2012/02/call-for-gsoc-2012.html @@ -0,0 +1,27 @@ + Call for GSoC: bring Haskell to the masses +

Call for GSoC: bring Haskell to the masses

February 15, 2012

GravatarBy Greg Weber

Yesod shows people that Haskell can excel in any paradigm. The limiting issue is simply a lack of quality libraries. Hackage may have thousands of libraries available, but I still see gaping holes preventing Haskell from being a viable option in many settings. Below are 2 GSoC proposals to address that.

+

Make creating interactive web sites simple

+

Create the building block for modern interactive ("real-time") websites: a library that can use websockets but automatically fallback to other available communication channels. node.js, Erlang, and Scala seem to be the goto solution for interactive websites. But Haskell is a high performance language with the best asynchrounous IO implementation available. Adding another thread costs almost nothing. And yet, node.js is getting all the spotlight! Node.js has several interfaces that use websockets and fall back to other technologies that are available. The Lift framework for Scala received a lot of attention and helped draw users to Scala. Perhaps Lift's biggest selling point has always been interactivity.

+

Yesod is a great web framework that uses Haskell's type safety to reduce your bugs to as few as possible. And yet, for all of our advantages we still have trouble maintaining a level of productivity of users of dynamic languages because they have more libraries at their disposal. The compelling use case for Haskell then needs to be not just that it can do things better, but that it can also do things which are impossible in a slow language with weak concurrency (i.e. Ruby and Python).

+

One solution that Rubyists currently use is to do their messaging/interactivity layer with a different toolset. They might drop in a websockets chat servers that uses node.js. This is a great opportunity for Haskell to sneak in the backdoor on these projects.

+

A Better Persistence layer

+

What does modern Haskell development have in common with common development practices 30 years ago? Many users still think the best approach to data storage is to write raw SQL queries that are a typo away from failure and require boilerplate serialization.

+

We have been trying to solve these issues with the Persistent library, but frankly this is still the weak link in our tool-chain. The latest release of Persistent greatly improved the situation, but we just aren't there yet.

+

We think we have struck on a good design for serialization that can be re-used in different database adapters. Our high-level query interface makes sure your queries are valid, but it can't express every possible query. We have some nice support for writing raw SQL queries and automatically getting back real Haskell records. However, your raw SQL may have errors, and you can't automatically get back a projection of your data. There is a similar situation for the MongoDB backend.

+

At the end of the summer, we would have great ways to write raw queries in SQL and MongoDB, but have them compile-time verified. The project would also involve investigating ways to accomplish projections.

+

Apply for GSoC

+

If you are interested in these Google Summer of Code proposals, please contact myself or Michael Snoyman who would mentor on these projects. Jasper Van der Jeugt, an experienced GSoC student that wrote a websockets library, will also mentor on the interactive websites proposal.

+

Students certainly aren't limited to the proposals outlined here, and they should ask about the GSoC withing the Haskell community. Alternatively, if you know students that would make a good fit (have existing Haskell experience), please pass the word along.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/02/persistent-0-8.html b/public/blog/2012/02/persistent-0-8.html new file mode 100644 index 00000000..36a1223b --- /dev/null +++ b/public/blog/2012/02/persistent-0-8.html @@ -0,0 +1,58 @@ + The New Persistent +

The New Persistent

February 8, 2012

GravatarBy Greg Weber

We are excited to announce the release of Persistent 0.8. This will be used by the upcoming 0.10 Yesod release.

+

Persistent is a data store interface library that is backend agnostic - it currently works with SQL and MongoDB databases.

+

More backends!

+

Persistent has solid support for Sqlite, PostgreSQL, and MongoDB. Felipe Lessa just contributed a MySQL backend! There is also a couchDB backend in the works - if you are a couchDB fan let us know - the main thing holding this one back are some willing maintainers to keep it up to date with changes in Persistent.

+

Dependency upgrades

+
  • The new MySQL backend uses the mysql-simple library.
  • +
  • The PostgreSQL backend now uses postgresql-simple instead of HDBC.
  • +
  • The persistent streaming API now uses conduits instead of enumerators.
  • +
  • Move from pool to resource-pool (thanks Bryan O'Sullivan!)
  • +
+

A new raw SQL interface

+

Felipe Lessa also added an amazing rawSql interface!

+

The Persistent query interface cannot capture every operation needed in a backend. For more advanced SQL queries without support from Persistent, rawSql is now a great tool. Persistent already had a very low-level raw SQl interface. However, you had to parse the result yourself. The rawSql interface maintains Persistent's automatic serialization.

+
SELECT ??, ??
+FROM "Person", "Likes", "Object"
+WHERE "Person".id = "Likes"."personId"
+AND "Object".id = "Likes"."objectId"
+AND "Person".name LIKE ?
+
+

If you name that query likesStmt, you then execute it:

+
do results <- rawSql likesStmt ["%Luke%"]
forM_ results $
\\( Entity personKey person
, Entity objectKey object
) -> do ...
+

This new interface adds greatly needed flexibility to Persistent. However, it does away with Persistent's guarantee that the query is correct: you could easily have a typo in your query. Now is a good time to mention another exciting project that attempts to solve this issue by validating the query at compile time: persistent-hssqlppp.

+

Complex Data Structures Support!

+

Persistent's serialization is now much more powerful. Previously, Persistent only supported flat Haskell records. However, MongoDB directly supports complex data structures, such as a record that contains a list of records. This functionality is essential to proper data modeling with MongoDB, and is one of the aspects of MongoDB that I have enjoyed the most. I think SQL users will enjoy this also.

+

SQL does not explicitly support complex data structures as column values. However it is a common strategy to serialize data structures to something like a JSON string and then save them to a SQL column - Persistent now automates this process. This is very useful functionality as long as SQL users keep in mind that JSON-serialized columns are meant just to be updated, deleted, and selected, not to be compared in a query (with a where clause, etc). Also, please keep in mind that consistent partial updates of an embedded data structure impossible. If you need to query your data structure or update portions of it, you should instead continue to spread your data structures over multiple tables rather than use this new feature.

+

Persistent now comes out of the box with support for Haskell bread-and-butter data structures: Records, Maps, Lists, and Sets. This means we automatically generate To/From JSON instances for your Persistent entities now. So when you need to send them as JSON, most of your work will already be done. If you don't like our JSON instances, use a no-json annotation in your schema.

+

The Great Divide

+

In a recent blog post I talked about how the strength of Yesod is the ease of integration with the compelling Hamlet, WAI, and Persistent libraries. We think Hamlet and WAI can still be improved, but that we have struck on the right design for Haskell. For all of its Persistent's strengths, it has always been something we are much less certain about. In re-thinking Persistent, we realized that Persistent is dealing with two separate issues.

+
  1. Serializing Haskell data to the database and de-serializing from the database to Haskell
  2. +
  3. Defining a query interface that all backends can share for type-safe querying of the database.
  4. +
+

We believe we have struck upon a great design for Haskell for data serialization to the database. On the other hand, the query interface could certainly be improved, and there are alternative approaches that might be better.

+

The basic serialization functions Persistent exposes (get, insert, replace) seem fairly universal to all databases. However, it seems impossible to make a universal query interface to satisfy every backend. For example, when Michael experimented with a Redis backend, he quickly realized many of the query operations did not map well to a simple key value store.

+

So rather than have a monolithic typeclass interface, Persistent now exposes 3 typeclasses

+
  1. PersistStore - univeral, basic serialization (get, insert, etc). Required by all other Persistent type-classes
  2. +
  3. PersistUnique - secondary indexes (getBy, etc). PersistStore is based on operating on a primary key. PersistUnique extends some of these operations to secondary indexes, or other columns where the values are unique
  4. +
  5. PersistQuery - Persistent's out of the box advanced query interface
  6. +
+

Our hope is that a PersistStore backend can now be made for any data store. If the PersistQuery interface does not align well with the data store, than an entirely different query interface can be used instead.

+

Other improvements

+

We cleaned up EntityDef, the data structure that maintains meta-data about your database entities - this fixed several bugs including making it more resilient to renamings. Database parameters can now be configured via environment variables

+

Future directions for Persistent

+

We are getting pretty happy with the SQL/MongoDB querying capabilities. Eventually we think there will be a way to have compile-time validation of raw queries for both SQL and MongoDB.

+

The new Persistent architecture makes it easier to have more contributions to Persistent from the community. It has never been easier to create a Persistent backend - just start by implementing the PersistStore serialization API. We look forward to seeing more Persistent backends and more powerful query interfaces.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/02/qcon-video.html b/public/blog/2012/02/qcon-video.html new file mode 100644 index 00000000..822ddb88 --- /dev/null +++ b/public/blog/2012/02/qcon-video.html @@ -0,0 +1,16 @@ + QCon video and book updates +

QCon video and book updates

February 10, 2012

GravatarBy Michael Snoyman

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/02/release-0-10.html b/public/blog/2012/02/release-0-10.html new file mode 100644 index 00000000..c167b246 --- /dev/null +++ b/public/blog/2012/02/release-0-10.html @@ -0,0 +1,45 @@ + Announcing Yesod 0.10 +

Announcing Yesod 0.10

February 9, 2012

GravatarBy Greg Weber

We are proud to announce the relase of Yesod 0.10 Please note that 0.10 < 1.0

+

The main purpose of this release was to switch to conduits. But while we were waiting for the conduit integration to mature, we added a log of great stuff.

+

Please see the upgrade guide for 0.10. This is a wiki page: please add your notes and improvements.

+

If you haven't upgraded to 0.9.4, please see the previous release announcement.

+

Persistent

+

There were so many improvements to Persistent, that it needed its own release announcement: Persistent 0.8

+

Some quick highlights:

+
  • New rawSql interface
  • +
  • New MySQL backend, couchDB in the works
  • +
  • Support for embedded records, maps, lists, and sets
  • +
+

For most users, only the initialization of Persistent in their site will be effected: persistent queries are still the same.

+

Shakespeare

+

We now use the new addDependentFile for GHC 7.4 users. This means ghc will automatically recompile your shakespeare templates when they change. This takes a burden of dependency tracking away from yesod devel: for 7.4 its main utitlity is to automatically restart compilation.

+

shakespeare-js

+

Coffeescript compilation is now performed at compilation time. This pattern is now supported in the shakespeare library: if a template needs to execute an external program, that should be done once at compilation time. This finally makes Coffeescript a great, effortless option.

+

I am a big fan of using Coffeescript rather than plain Javascript. Coffeescript puts a nicer face on Javascript that gets rid of its inconsistencies and adds conveniences. However, even a well-done Javascript is still a very lacking programming language. I am looking forward to the day that I can write Haskell or at least some language with a great type system and have that easily translated to Javascript. There are some interesting developments going on in this area independent of Yesod, and we look forward to the prospect of integrating these solutions when they mature.

+

WAI

+

The big change is the move to conduits. There were also a lot of small bug fixes and performance improvements. The RequestLogger Interface was improved, and a bug in fast-logger that it uses was squashed. We are now experimenting with ways to make WAI a more powerful way to share code. One limiting factor was not being able to pass along arbitrary data. WAI now has a vault parameter for that purpose, which uses the vault package.

+

Yesod

+

Again, the focus of this release was the switch to conduits. For a Yesod user this is mostly an implementation detail. This leaked in two main places: the scaffolding configuration (see upgrade guide), and exception handling. With the move to conduits we have completely removed the liftIOHandler exception handling hack.

+

Approot, the setting which generates urls is maturing, see the upgrade guide. You now have the option of changing the url based on request information.

+

The redirect system has been reworked to use the correct status and to remove redundancies. The need for RedirectTemporary has been removed, and instead we just default to the correct HTTP status codes. If you want to specify an HTTP status, use redirectWith.

+

Improved routing. While there were some bug fixes, the main improvement is a .scalable dispatcher. Most web framework routers dumbly check routes one-by-one until they find a match. Yesod now features an efficent dispatch. The literate haskell source explains the two main optimizations:

+
  • Break up routes based on how many components they can match (slashes in a route). This lookup runs in constant time.
  • +
  • Use a Map to reduce string comparisons for each route to logarithmic complexity.
  • +
+

1.0

+

We consider this to be a 1.0 release candidate. We want 1.0 to be very solid release, so we definitely needed this extra release cycle for the move to conduits. 1.0 for us means a stable API - now is your last chance to propose good API changes without getting a lot of pushback and requirements for maintaining deprecated APIs.

+

If you look at the 1.0 roadmap we posted, we have accomplished everything except easy testing integration and static html generation. However, we have pull requests for both of those waiting right now!

+

Thanks to everyone that helps improve Yesod. Please upgrade to 0.10 and let us know how to make it even better.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/02/simplifying-resourcet.html b/public/blog/2012/02/simplifying-resourcet.html new file mode 100644 index 00000000..25e880de --- /dev/null +++ b/public/blog/2012/02/simplifying-resourcet.html @@ -0,0 +1,103 @@ + Conduits: Simplifying ResourceT +

Conduits: Simplifying ResourceT

February 23, 2012

GravatarBy Michael Snoyman

This blog post discusses the conduit package. If you are not familiar with it, you can read up on it in the Yesod book chapter on conduits.

+

Earlier today, Oleg sent an email to the Haskell cafe about regions. Yves Parès sent a response that linked to resource-simple, a package I had not heard of until then. Reading the description on the page reminded me of one of the earlier decisions I made in designing conduits. I'd like to explain that decision, and then explain how we can work around it.

+

Originally, I had intended that all conduits would live in the IO monad. This is a fair assumption: the majority of the time, we want to use conduits to perform some kind of I/O (otherwise, why not just use lazy lists?). So for my first stab at the problem, I designed a ResourceT transformer that always assumed an IO monad for its base. Then, all three data types in conduit (Source, Sink, and Conduit) assumed that their actions lived in the ResourceT transformer so that they could safely acquire resources.

+

However, this IO assumption can be limiting. Thre are plenty of sources, sinks, and conduits which perform no resource allocation at all, and we would like to be able to access from pure code. For example, xml-conduit provides a greater parser and renderer for XML documents; it would be a shame to only be able to access it from IO. We could of course use unsafePerformIO, but we don't mention that in polite company.

+

I created an elaborate typeclass system around ResourceT, which would allow us to build monad stacks around both IO and ST. Then we could call ST from pure code, and no need to touch that unsafe stuff!

+

Unfortunately, there are a few downsides to this approach:

+
  • ResourceT doesn't really make sense for ST. You can't safely allocate scarce resources in the ST monad, so we're just pretending for the sake of uniformity. Just have a look at how resourceBracket is implemented for ST.

  • +
  • The type complexity really gets in the way. Look at the presence of both with and withIO for an example.

  • +
  • We are still limited in our monad choices, since we need monads that provide mutable references for ResourceT to work. This turns into a performance penalty, as we'll see later.

  • +
+

It turns out there's a simple solution here: don't bake ResourceT into Source, Sink, and Conduit. Instead, only use it for functions that actually allocate scarce resources, such as sourceFile. There is a downside to this approach: type signatures get a little longer:

+
-- conduit 0.2
+sourceFile :: ResourceIO m => FilePath -> Source m ByteString
+
+-- conduit 0.3
+sourceFile :: ResourceIO m => FilePath -> Source (ResourceT m) ByteString
+
+

However, you can make the argument that this is in fact a Good Thing: we're now explicit in our types as to whether we're performing allocation of scarce resources.

+

I've put together a separate branch on Github for this approach, and have generated some Haddocks. I'm not yet ready to release this code to Hackage, but I wanted to get people's feedback.

+

Beyond the theoretical issues above, I'm sure there are two big questions people want to ask.

+

How bad is the breakage?

+

Not bad at all. The Resource typeclass is completely gone now. You can replace it with Monad. In other words:

+
-- old
+nums :: Resource m => Source m Int
+nums = fromList [1..10]
+
+-- new
+nums :: Monad m => Source m Int
+nums = fromList [1..10]
+
+

Additionally, the lesser-used ResourceThrow and ResourceUnsafeIO classes have been renamed to MonadThrow and MonadUnsafeIO. These classes are not in any way ResourceT-specific, thus the name change. ResourceIO remains as-is.

+

You might have to add a few explicit lift calls now, and in some cases will have to change your type signature to include ResourceT. But overall, this is a minor change.

+

How does this affect performance?

+

For code that will still live in the ResourceT transformer, this will have no performance affect. (I made a separate change to optimize the monadic bind implementation of ResourceT, which does improve performance significantly.) However, if you don't need scarce resource allocations, you can now skip out on the ResourceT overhead entirely. In fact, you can skip out on the overhead of IO and ST as well if you just need to perform pure actions.

+

I implemented a simple Criterion benchmark comparing six different ways of summing up the numbers 1 to 1000:

+
main :: IO ()
+main = defaultMain
+    [ bench "bigsum-resourcet-io" (whnfIO $ C.runResourceT $ CL.sourceList [1..1000 :: Int] C.$$ CL.fold (+) 0)
+    , bench "bigsum-io" (whnfIO $ CL.sourceList [1..1000 :: Int] C.$$ CL.fold (+) 0)
+    , bench "bigsum-st" $ whnf (\i -> (runST $ CL.sourceList [1..1000 :: Int] C.$$ CL.fold (+) i)) 0
+    , bench "bigsum-identity" $ whnf (\i -> (runIdentity $ CL.sourceList [1..1000 :: Int] C.$$ CL.fold (+) i)) 0
+    , bench "bigsum-foldM" $ whnf (\i -> (runIdentity $ foldM (\a b -> return $! a + b) i [1..1000 :: Int])) 0
+    , bench "bigsum-pure" $ whnf (\i -> foldl' (+) i [1..1000 :: Int]) 0
+    ]
+
+

The results are very promising: moving from ResourceT to the Identity monad brings runtime from 1541us to 409us. Unsurprisingly, a straight foldM is still faster (no conduit overhead at all), and a pure foldl' faster yet, but we're definitely closing the gap.

+
benchmarking bigsum-resourcet-io
+mean: 1.541109 ms, lb 1.536687 ms, ub 1.546054 ms, ci 0.950
+std dev: 23.92658 us, lb 20.27472 us, ub 30.16423 us, ci 0.950
+found 3 outliers among 100 samples (3.0%)
+  2 (2.0%) high mild
+  1 (1.0%) high severe
+variance introduced by outliers: 8.475%
+variance is slightly inflated by outliers
+
+benchmarking bigsum-io
+mean: 705.3596 us, lb 703.8689 us, ub 706.8185 us, ci 0.950
+std dev: 7.554517 us, lb 6.639072 us, ub 8.699024 us, ci 0.950
+
+benchmarking bigsum-st
+mean: 737.1096 us, lb 734.7698 us, ub 739.0198 us, ci 0.950
+std dev: 10.77292 us, lb 8.970027 us, ub 13.39969 us, ci 0.950
+found 4 outliers among 100 samples (4.0%)
+  4 (4.0%) low mild
+variance introduced by outliers: 7.532%
+variance is slightly inflated by outliers
+
+benchmarking bigsum-identity
+mean: 409.2451 us, lb 407.7206 us, ub 411.1361 us, ci 0.950
+std dev: 8.671930 us, lb 6.924580 us, ub 12.59325 us, ci 0.950
+found 2 outliers among 100 samples (2.0%)
+  1 (1.0%) high severe
+variance introduced by outliers: 14.217%
+variance is moderately inflated by outliers
+
+benchmarking bigsum-foldM
+mean: 147.9192 us, lb 146.5067 us, ub 149.2992 us, ci 0.950
+std dev: 7.126892 us, lb 6.556864 us, ub 7.773273 us, ci 0.950
+variance introduced by outliers: 46.449%
+variance is moderately inflated by outliers
+
+benchmarking bigsum-pure
+mean: 36.21976 us, lb 36.01451 us, ub 36.39617 us, ci 0.950
+std dev: 970.5240 ns, lb 832.2271 ns, ub 1.192476 us, ci 0.950
+found 2 outliers among 100 samples (2.0%)
+  2 (2.0%) low mild
+variance introduced by outliers: 20.947%
+variance is moderately inflated by outliers
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/announcing-conduit-0-3.html b/public/blog/2012/03/announcing-conduit-0-3.html new file mode 100644 index 00000000..074284e6 --- /dev/null +++ b/public/blog/2012/03/announcing-conduit-0-3.html @@ -0,0 +1,126 @@ + Announcing conduit 0.3 +

Announcing conduit 0.3

March 21, 2012

GravatarBy Michael Snoyman

I'm happy to announce the 0.3.0 release of +conduit. As many readers are +aware, conduit is a library to address the issue of streaming data in constant +space. This release does not come alone; a number of other packages have been +released to support this updated version. See the end of this post for a full +list.

There have been a number of improvements to the library. Quoting the changelog:

ResourceT has been greatly simplified, specialized for IO, and moved into a +separate package. Instead of hard-coding ResourceT into the conduit +datatypes, they can now live around any monad. The Conduit datatype has been +enhanced to better allow generation of streaming output. The SourceResult, +SinkResult, and ConduitResult datatypes have been removed entirely.

For users of the high-level API, nothing has changed. In other words, the +following line is still completely valid:

runResourceT $ sourceFile input $$ sinkFile output

Mid-level API users (conduitState, sourceIO, etc) should also avoid any changes +to their code. Only users dealing directly with the low-level API should need +to change their code. We'll cover the major changes, and some of their +motivation, in the next few sections.

Note: The chapter in the Yesod book on conduits still covers version +0.2. Eventually, I will bring the content up-to-date. The concepts have stayed +completely the same through all versions, and therefore the chapter should +still be mostly relevant. If you're just starting with conduit, I recommend +reading the chapter and then coming back here. Eventually I'll merge the two +together.

Simplified, separated ResourceT

As we discussed previously, +ResourceT has been simplified, targeting just the IO monad. It has also +been released as a separate package, resourcet.

There have been a few minor changes: with and withIO are now replaced by +allocate. There are now a number of typeclasses available.

  • MonadResource is any monad stack with a ResourceT in it.
  • MonadUnsafeIO is a stack with either IO or ST as a base.
  • MonadThrow is a monad that can throw Exceptions.
  • MonadActive is specifically added for ResourceT usage. It tracks whether or not the state of current monad is still active. This is vital for properly implementing lazy I/O for conduits. For non-ResourceT monad stacks, MonadActive indicates that the monad is always active.

Less reliant on ResourceT

A ResourceT is used for safely allocating resources. But if all I'm doing is +printing the numbers 1 to 10, e.g.:

sourceList [1..10] $$ Data.Conduit.List.mapM_ print

who needs it? As a result, the ResourceT transformer is no longer baked into +the Source, Conduit, and Sink types. Instead, functions that need to +allocate resources (e.g., sinkFile) should place a MonadResource constraint +on their inner monad.

Improved Conduit type

Previously, the Conduit type could return a list of return values every time +it was pushed to. This, however, is inadequate. If you have a Conduit That +can produce large amounts of output for a single input (e.g., a decompressor), +you have to allocate it all in memory.

A Conduit has been improved in two ways:

  • After being pushed new input, it can return multiple outputs separated by monadic actions, instead of returning a single list.
  • When a Conduit is closed, it returns a Source. If you want to consume the rest of its output, you can do so. And if you don't care, and just want to ignore it, you can close the Source and not spend any more cycles on it.

You'll see below that there is a new, updated version of zlib-bindings +available as well. This release does away with the previous callback-based API, +and makes it possible to implement a decompressor in zlib-conduit in a fully +constant-memory manner.

No more result types

Originally, conduit had three types for sinks: Sink, PreparedSink, and +SinkResult. We did away with the Sink/PreparedSink distinction in conduit +0.2, and in the process greatly simplified the library and improved +performance. Now we're unifying Sink and SinkResult, with the exact same +benefits. (And yes, the same applies to Source and Conduit.)

In this process, I've come up with a guiding principle of sorts for the design +of conduit. It comes down to: only ever do one thing at a time. As a concrete +example, consider pushing to a Sink in conduit 0.2. We have the type (greatly +simplified):

data Sink input m output = Sink (input -> m (SinkResult input m output))

Seems fairly straight-forward, right? But imagine that we have a pure sink, +which never performs any monadic actions (e.g., fold). We've now tied +together the concept of pushing new data, and that of performing a monadic +action. While this may seem benign, it has two important ramifications:

  • It can drastically slow down code. Consider 417us versus 88us.
  • Taking the opposite approach (having an explicit constructor for monadic actions) allows us to unify the datatypes.

For the second point, consider Source and sourceFile. sourceFile cannot +return any data until it has performed an IO action. But the SourceResult +type in conduit 0.2 requires that either data is available immediately (the +Open constructor), or that the Source indicate that it is closed +(Closed). That's why we needed an extra type Source, which had a record +m SinkResult for pulling from the Source.

However, if we add a third constructor for performing monadic actions to our SourceResult type, we don't actually need the Source type any more. The result looks like:

data Source m a =
+    Open (Source m a) (m ()) a
+  | Closed
+  | SourceM (m (Source m a)) (m ())

Open provides more data, tells you the next Source in the stream, and +provides an action to close the Source early. Closed is pretty boring. +SourceM now allows you to perform an action to get the next Source, or +perform another action to close early.

Here's a slightly long-winded example which should hopefully demonstrate the +point. In real life code, we would just use sourceIO, but hopefully this +makes it clear how to pass control back and forth between the Open and +SourceM constructors.

import Data.Conduit
+import qualified Data.Conduit.List as CL
+import System.IO
+import Control.Monad.Trans.Resource
+import Control.Monad.IO.Class (liftIO)
+
+sourceFile :: MonadResource m => FilePath -> Source m Char
+sourceFile fp =
+    -- Need to start off with a monadic action
+    SourceM initPull initClose
+  where
+    initClose = return () -- haven't opened anything, nothing to close
+
+    initPull :: MonadResource m => m (Source m Char)
+    initPull = do
+        -- Open the file handle, and register a release action
+        (releaseKey, handle) <- allocate (openFile fp ReadMode) hClose
+        -- pass off to the pull function, that does the real work
+        pull handle releaseKey
+
+    pull :: MonadResource m => Handle -> ReleaseKey -> m (Source m Char)
+    pull handle releaseKey = do
+        eof <- liftIO $ hIsEOF handle
+        if eof
+            then do
+                -- file exhausted, close the handle
+                release releaseKey
+                return Closed
+            else do
+                -- more data, get a character
+                c <- liftIO $ hGetChar handle
+                return $ Open
+                    -- The next Source to use, which needs to perform another
+                    -- monadic action
+                    (sourceM handle releaseKey)
+                    -- Early close
+                    (release releaseKey)
+                    -- The newly pulled data
+                    c
+
+    sourceM :: MonadResource m => Handle -> ReleaseKey -> Source m Char
+    sourceM handle releaseKey = SourceM
+        (pull handle releaseKey)
+        (release releaseKey)
+
+main :: IO ()
+main = do
+    str <- runResourceT $ sourceFile "test.hs" $$ CL.consume
+    putStrLn str

Overall, this change probably complicates the writing of low-level code a bit. +However, the simplicity of implementation for the connect and fuse operators, +plus the overall efficiency improvements, reinforce my belief that this was the +right change to make.

Updated packages

You'll notice that, missing from this list, are any of the WAI, Persistent, or +Yesod packages. We are purposely holding off on releasing WAI and Persistent +code- even though it's ready- to help avoid confusion for Yesod users. The +upcoming Yesod 1.0 release will depend on conduit 0.3, and will hopefully be +out in the next few weeks. Distribution maintainers: please do not begin the +upgrade cycle on conduit 0.3 until Yesod 1.0 is released.

  • attoparsec-conduit-0.3.0
  • authenticate-1.1.0
  • blaze-builder-conduit-0.3.0
  • conduit-0.3.0
  • crypto-conduit-0.2.0
  • filesystem-conduit-0.3.0
  • http-conduit-1.3.0
  • imagesize-conduit-0.3.0
  • network-conduit-0.3.0
  • resourcet-0.3.0
  • uri-conduit-0.3.0
  • xml2html-0.1.1
  • xml-catalog-0.6.0
  • xml-conduit-0.6.0
  • yaml-0.6.0
  • zlib-bindings-0.1.0
  • zlib-conduit-0.3.0

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/announcing-yesod-platform.html b/public/blog/2012/03/announcing-yesod-platform.html new file mode 100644 index 00000000..c6f1030e --- /dev/null +++ b/public/blog/2012/03/announcing-yesod-platform.html @@ -0,0 +1,51 @@ + Announcing the Yesod Platform +

Announcing the Yesod Platform

March 15, 2012

GravatarBy Michael Snoyman

There have been lots of emails, blog posts, Reddit threads and Google+ +discussions recently about Cabal dependency hell. I'm certain that the +community as a whole, and Andres Loeh and Duncan Coutts in particular, will be +able to solve these problems thoroughly in the long run. But in the short term, +we still have a problem.

+ +

I posted last week about cabal-nirvana. While it certainly would seem to +solve many of our problems, it's a bit like using a tactical nuke to +kill a mosquito. But, based on the same approach and code, I'd like to announce +a new solution: yesod-platform. This approach- championed very successfully by +Felipe Lessa- involves releasing a meta-package to Hackage which contains +strict version bounds for each dependency of Yesod (excluding bootlibs).

+ +

There's actually not much more to the story than that. Instead of running +cabal install yesod, just run cabal install yesod-platform. +You can also put the dependency in your cabal file. +We have no actual proof yet, but if you put it in your cabal file, you +likely want to make it the first entry, to make sure that the top-down +dependency solver doesn't get confused.

+ +

This is obviously a new, untested technique, and there may be some hurdles +yet to come. But I encourage everyone to give this new approach a shot and see +if it solves your problems.

+ +

What about cabal-dev?

+ +

This is an orthogonal issue. You can use yesod-platform with +cabal-dev if you want. cabal-dev solves the sandboxing issue, while +yesod-platform ensures that you have comatible versions of packages.

+ +

Future improvements

+ +

The only improvement I know of right now is to include some serious +benchmarks with the yesod-platform. This will allow us to make sure that we're +not accidently adopting newer dependencies that have major performance +regressions. But if anyone else has some ideas, I'd love to hear them.

+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/cabal-nirvana.html b/public/blog/2012/03/cabal-nirvana.html new file mode 100644 index 00000000..0dcbd028 --- /dev/null +++ b/public/blog/2012/03/cabal-nirvana.html @@ -0,0 +1,35 @@ + Avoid cabal hell: find nirvana +

Avoid cabal hell: find nirvana

March 6, 2012

GravatarBy Michael Snoyman

Cabal dependency hell has been around for a while now, and it has especially caused issues for larger projects with lots of dependencies (e.g., Yesod). We've put a lot of effort into finding ways to avoid it, but we've still be plagued by problems. This has had the effect of both hindering existing users, and preventing new users from being able to use Yesod.

+

Fortunately, the answer is simple: to avoid hell, you just need to find nirvana. (Note: There have been two very large discussions on this issue recently, one on the Yesod mailing list and another on Google+. The discussions there may help explain some of the motivations if you're left wanting more at the end of this blog post.)

+

The problem

+

Simply stated, Hackage is a zoo. Anyone can upload anything at any time. If I write some code that depends on foobar version 0.1, and someone comes along and makes a breaking change in 0.1.1, my code will (generally) break. We also have the issue of different packages using different versions of the same underlying packages (e.g., baz requires foobar 0.1, and bin requires foobar 0.2), and therefore they cannot be installed side-by-side.

+

Distributions, such as Debian, Nix, and Fedora, need to solve this problem themselves by hand-selecting which packages will be included. This solves the problem completely, but is relatively labor intensive. The bigger problem is that everyone is solving the same problem separately. Also, this work only benefits people using distributions for installing their software; normal cabal users are still left with the wild west.

+

I say none of this "zoo" and "wild west" stuff as an affront to Hackage: Hackage is exactly as it should be, and I don't wish to change it. The problem is, we need an extra layer to provide more stability.

+

The solution

+

There's an obvious solution to the problem as stated: have a centralized list of stable/compatible/blessed packages, and have everyone use it. Making that list is an important issue, and one I'm hoping to engage the community on. But assuming we had that list, all the distributions could use it, and we'd have a much nicer stable system.

+

But we still need one more thing: a solution for cabal users. That's where cabal-nirvana comes in. It's an incredibly simple (101 sloc at last count) program that adds a bunch of version constraints to you cabal config file. It gets the list of necessary constraints from some site (currently I set up a basic one for Yesod at: www.snoyman.com/cabal-nirvana/yesod, which is being used as the default). That way, you will never get dependency hell on the packages covered by the list.

+

I've just uploaded the first version to Hackage (warning: alpha quality software). To get started, you can use the commands:

+
cabal install cabal-nirvana
+cabal-nirvana fetch # get the most recent list of blessed packages
+cabal-nirvana start # apply the downloaded list to your config file
+
+

Two other commands are stop (remove all constraints) and test, which will try (via cabal-dev) to install all of the packages in your config file simultaneously.

+

I've written cabal-nirvana with all of the bad practices I normally fight against: it uses String instead of Text or ByteString, uses the HTTP package instead of http-conduit, and uses the standard type FilePath = [Char] instead of system-filepath. The reason is simple: we want to have the barest number of dependencies possible, to make sure our cabal-hell-defeater is not itself defeated by cabal hell.

+

The repo also has another (optional) executable to help you generate one of these lists of blessed packages. You give it a list of packages you want covered, and it will generate a list of all of the deep dependencies, together with the most recent version of them available from Hackage. After generating this file, you should use cabal-nirvana test on it to confirm that this is a compatible list. To build this executable (cabal-nirvana-generate), pass in the -fgenerate option when building.

+

What's next

+

This is a very preliminary release, mostly intended to start getting feedback and working to a real solution. Some thoughts I've had is that we should have built-in support for different stability profiles (stable, beta, alpha, bleeding), and automatically detect the GHC version and switch lists appropriately.

+

If you're out there using Yesod, please give this tool a try and let me know if it solves your dependency problems (or causes others). If you want to collaborate with me on getting a wider selection of packages into the list, be in touch as well.

+

Future enhancements

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/history-of-persistence.html b/public/blog/2012/03/history-of-persistence.html new file mode 100644 index 00000000..f0d9e991 --- /dev/null +++ b/public/blog/2012/03/history-of-persistence.html @@ -0,0 +1,29 @@ + A History of Haskell persistence +

A History of Haskell persistence

March 6, 2012

GravatarBy Greg Weber

We have a Google Summer of Code proposal to improve Persistent, the preferred database abstraction layer for the Yesod web framework.

+

While discussing the proposal on haskell-cafe, it was brought to my attention a new similar package called structured-mongoDB.

+

Now that we have yet another library accomplishing the same goal, haskellers will wonder what the difference between them is. Let me give a history of persistence in Haskell as I know it.

+

There have always been raw database drivers. These meant that you need to do extra work to serialize your data and that your queries could have typos that wouldn't be detected until runtime. In the case at least of SQL, raw queries are also difficult to compose.

+

One solution given for better persistence was acid-state. This potentially solves every problem, but limits you to an experimental in-process memory store.

+

Another solution was HaskellDB. HaskellDB helps you know your queries are correct and composes well. Its weakness are that you have to learn new terms for standard SQL terms, it may not generate optimized queries, and a lack of automatic serialization into normal Haskell data structures. Probably HaskellDB never got great uptake simply because it didn't have a great maintainer pushing it forward, although there are users that enjoy using it today.

+

Michael Snoyman more recently created Persistent. Persistent showed that through reliance on Template Haskell, one could create type-safe queries with automatic serialization, and this could be done across different databases, SQL (Sqlite, PostgreSQL, or Felipe Lessa's later contributed MySQL backend), or (my contributed) MongoDB (and now there is a pull request for a CouchDB backend). Persistent's main weakness was that it can only satisfy the common 80% usage pattern, and didn't offer as much help when you want to write a raw SQL query, in addition its API did not allow for great composition.

+

Boris Lykah released the Groundhog library. It showed that instead of all the Template Haskell generation of Persistent you could instead use easy to compose combinators. This was a point of collaboration between the 2 libraries, and Persistent absorbed Groundhog's approach. Ultimately Persistent did not merge with Groundhog because Groundhog has some more advanced features that we though would complicate the internals and the types the users had to deal with. Groundhog continues under Boris's stewardship mostly with the goal of being an experiment for advanced features (for example support for Sum types).

+

Persistent underwent recent changes to keep its serialization layer separate from its query layer. We would really like to share Persistent's serialization layer with anyone else that wants to support multiple database backends. I have thrown out the idea of Groundhog relying on Persistent's serialization layer now that it has been separated from Persistent's query layer, and Boris plans to look into it. Persistent is also better at raw queries now: it can give you back nicerly serialized data in many cases, although you still risk typos in your queries.

+

Blake Rain created a Persistent-like library just for MongoDB Blake has not been advertising this, and the fact that Persistent already has support for MongoDB means I don't think many outside his company have used it. Blake's implementation is simpler than Persistent's because it only targets one backend.

+

structured-mongoDB was released recently by Deian Stefan. It is the same as mt-mongoDB right now because it targets just one backend. However, Deian stated that they are considering supporting multiple backends, which would make structured-mongoDB the same as Persistent (and Groundhog, which does not have MongoDB support).

+

I am sure I am missing some other attempts at type-safe querying and automatic data-marshaling: please let me know of them. I do know that there are still a lot of open or at least untackled problems for a persistence layer.

+

I believe the biggest limitation that Haskell puts on us for coming up with better solutions to persistence layers are that records are not name spaced. I have been involved over the last few months in a discussion to try to push forward a solution to this, and we may finally be close to coming up with a solution now.

+

I also believe there is a fundamental problem with the query combinator approach taken by persistent, groundhog, and structured. It does let you compose SQL (and perhaps we will always find it invaluable for that), but It does not allow for precise (raw) queries. We are creating better capabilities in persistent (new rawSQL function that still does serialization), but we still aren't fully there yet. mt-mongodb is working towards a solution for this by using a quasi-quoted query. Quasi-quoting will allow for the query to be parsed at compile time and verified. It would also be possible with MongoDB to try for a non-Quasi-quoted approach. persistent-hssqlppp also attempts to solve this problem by using hssqlppp to validate a QQ SQL query.

+

I wrote this message as a starting point to try to increase collaboration in the realm of Haskell persistence. I would certainly welcome more libraries to the scene if they were tackling these new problems in a way difficult to tackle in an existing library. I believe the greatest weakness of the Haskell ecosystem is a lack of quality libraries, and that we have few resources at our disposal to improve the situation. I also have seen that every time I have collaborated with others in the Haskell community, we end up with something much better.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/more-powerful-conduit.html b/public/blog/2012/03/more-powerful-conduit.html new file mode 100644 index 00000000..00c16238 --- /dev/null +++ b/public/blog/2012/03/more-powerful-conduit.html @@ -0,0 +1,50 @@ + A More Powerful Conduit +

A More Powerful Conduit

March 2, 2012

GravatarBy Michael Snoyman

I've gotten a few emails recently (most recently from Nathan Howell) about a shortcoming of the Conduit datatype. The issue is that a Conduit can only produce output when it is pushed to. However, if you have a Conduit that could produce a large amount of output for a single input (e.g., a decompressor), this could become memory inefficient.

+

I came up with a simple solution: allow a Conduit to return a stream of outputs for a single input. In code, this turns into just a single additional constructor for the ConduitResult type:

+
HaveMore (m (ConduitResult input m output)) (m ()) [output]
+
+

I'll go into more detail on the m () bit below, but it says how to close the Conduit early.

+

Any time you push to a conduit, it can now say "here's some output, and more is on the way." I've implemented this, and I'm happy with this solution. However, I want to make it better.

+

There's one way to do it

+

With the previous API, there was only one way to encode each operation. If you wanted to implement a map, you had to use the Producing constructor with a single element list for the output. A concatMap would look something like:

+
push input = return $ Producing (Conduit push close) (f input)
+
+

However, we now have at least two other ways to encode the same thing:

+
  1. Return a HaveMore constructor which contains all of the output, which will then return then Producing constructor to allow the Conduit to continue.

  2. +
  3. Return the elements one at a time via HaveMore.

  4. +
+

Having these multiple approaches makes the internals of the library a bit ugly, and since there are multiple codepaths, it increases the likelihood of bugs. I also think it's difficult for new users to see so many options.

+

There are two separate issues at play, so let's deal with them separately.

+

All constructors can return output

+

In the current setup, all three constructors can return output. This was necessary previously, but no longer. If we removed the [output] field from both Producing and Finished, then a user would be forced to use HaveMore when they want to return output.

+

My concern here is complicating library usage. A previously simple function like map would now require a few extra hoops to be jumped through. We could address this by leaving the same higher-level interface we have before in conduitState and conduitIO. That would give the downside of having a mismatch between the low-level and high-level API.

+

To chunk, or not to chunk

+

Another question is chunking. Previously, returning a list of outputs was necessary, since we only had one chance to return output. Now, however, we could just return successive HaveMores. This has the downside of- once again- complicating some implementations. It has an additional downside that it might hurt performance. On the flip side, it may improve performance in some cases, since it would be impossible to return empty lists in a HaveMore.

+

Should closing give a Source?

+

And as long as we're on the subject of change, let's look at closing a Conduit. This applies in two circumstances: the feeding source closed, or the consuming sink closed. If the feeding source closes, we want to have an opportunity to produce a bit more output. This is necessary, for example, in the case of compression: we want to build up large chunks of compressed data and then generate output. But the last chunk of output has to be manually flushed once we know there's no more input.

+

On the flip side, if the consuming sink closes, we don't need to produce any more output as it won't be used. If you look at the definition of HaveMore above, it has a field m (), which is how it's closed. This doesn't allow for any new output to be produced, because a HaveMore would only ever be closed if the consuming Sink closed.

+

At this point, I see two problems with the way conduitClose works:

+
  • When closing a Conduit, you can only return a single chunk of values, not a stream of values. I can't think of a use case where you would return a large quantity of output from closing, but this limitation does both me.

  • +
  • In the case of a closed sink, the conduit will still try to produce some extra output which may never be used.

  • +
+

There's an easy solution to both problems: closing a Conduit returns a Source, which provides the last set of data. In the case of a closed Sink, then the conduit functions would simply call sourceClose immediately. In the case of large output, we could take advantage of Source's natural streaming abilities.

+

Feedback wanted

+

I'm writing this post in hope of getting some good feedback from people. Is my desire for one-way-to-do-things worthwhile, or is it better to complicate the internals of the library in exchange for potentially simpler user code? Does anyone have recommendations for better names for any of the constructors?

+

Postscript: prior art

+

While working on this, I reviewed two alternate approaches: enumerator and pipes. Let me explain why I can't reuse their solutions:

+
  • The Enumeratee type from enumerator is very powerful, much more so than a Conduit. It is a general purpose Iteratee-transformer, capable of doing lots of crazy stuff. That's exactly what I want to avoid for conduit: implementing an Enumeratee is far more complicated than implementing a Conduit, since it requires thinking directly about the inner Iteratee. The simplicity of a Conduit comes from the fact that it is a standalone unit.

  • +
  • As usual, pipes look like a simple, elegant solution. But the big thing it's lacking is proper resource management. Notice how much thought goes into Conduit to ensure that all resources are closed as early as possible, even in the case of early termination. It's true that by using ResourceT, pipes is able to avoid completely losing scarce resources, but holding onto a file handle for too long is not much better. I see no way to adapt any of pipes's approaches to conduit and still maintain our strict resource management.

  • +

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/more-pure-conduit.html b/public/blog/2012/03/more-pure-conduit.html new file mode 100644 index 00000000..cea7d1eb --- /dev/null +++ b/public/blog/2012/03/more-pure-conduit.html @@ -0,0 +1,45 @@ + More Pure Conduit (and benchmarks!) +

More Pure Conduit (and benchmarks!)

March 3, 2012

GravatarBy Michael Snoyman

So following up on my previous post, I've run into a few other issues that are a bit of an annoyance in conduit:

+
  • It's annoying having both a Source and SourceResult type (same goes for Sink and Conduit).
  • +
  • Worse yet, these types share a lot of very similar constructors. Compare SinkData with Processing: they're identical.
  • +
  • Every call to the library goes through a monadic bind. Pure code is not only (arguably) cleaner, but it is generally faster.
  • +
+

It turns out that solving all of these problems simultaneously is not only possible, but it also drastically cleaned up the codebase. I think I just netted out 200 less lines of code. Let's look at the transformation applied to Source, the other changes are basically identical.

+

Previously, we had two datatypes defined as:

+
data SourceResult m a = Open (Source m a) a | Closed
+data Source m a = Source (m (SourceResult m a)) (m ())
+
+

The first field in Source allowed you to pull more data, and the second allowed you to close a Source early. SourceResult would either signify that the source is now closed, or provide the next bit of output, along with the following Source. Compare that to the new definition:

+
data Source m a =
+    Open (Source m a) (m ()) a
+  | Closed
+  | SourceM (m (Source m a)) (m ())
+
+

This looks almost identical to SourceResult, except for two changes:

+
  • We've added the SourceM constructor, which has two fields: pulling the next monadic Source, and closing early.
  • +
  • We've unpacked the previous Source fields into the Open constructor, and stripped away the monadic wrapping around the pull.
  • +
+

The point is that, if you need to take a monadic action, you must do so explicitly now via SourceM. For pure code, you can just use Open. Under this new system, we can now declare completely pure higher-level functions, e.g.:

+
sourceListPure :: Monad m => [a] -> Source m a
+sourceListPure [] = Closed
+sourceListPure (x:xs) = Open (sourceListPure xs) (return ()) x
+
+

And this isn't just a matter of convenience: avoiding all of the monadic binding produces a huge speedup. In the bigsum benchmark (which adds the numbers 1 to 1000), the average runtime went from 417us to 88us. I don't expect this kind of speedup for typical use cases (after all, most uses of conduit will in fact be using IO at some point), but this is very encouraging.

+

The downside

+

There is a downside to this change: some of our invariants are no longer expressed in the typesystem. Previously, it was impossible to return leftover data from a Sink or Conduit without first having data pushed to it. That is no longer the case, since you can trivially define:

+
myEvilSink = Done (Just "I inserted data") ()
+
+

As much as I like expressing all invariants in the type system, I think this is an acceptable trade-off.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/pipes-like-conduit.html b/public/blog/2012/03/pipes-like-conduit.html new file mode 100644 index 00000000..c0eafff5 --- /dev/null +++ b/public/blog/2012/03/pipes-like-conduit.html @@ -0,0 +1,69 @@ + pipes-like conduit +

pipes-like conduit

March 26, 2012

GravatarBy Michael Snoyman

Note: the post below was written from a train and a bus, without internet access. Keep that in mind. Since I got home, I've uploaded the relevant Haddocks for conduit 0.4.0.

I don't think people will accuse me of being shy with my opinions. I've been +pretty outspoken that- while pipes certainly seems very elegant- I'm concerned +that it hasn't been designed from the ground up to deal with the same issues +that conduit has always been targeted at. As a quick review:

  • Resource handling. There's a claim that resource management is completely orthogonal to pipes, and can be achieved by using ResourceT. I disagree: while this may ensure that resources are eventually released, there's no guarantee that resources will be freed as early as possible.
  • Nonstandard control flow is not supported. This is a huge pain point we had with enumerator: you have to make sure that the entirety of your code lives in the Iteratee monad to have access to the data stream. conduit introduced the concept of BufferedSource, which allowed us to greatly simplify many packages (WAI, http-conduit, persistent...).
  • I could be wrong here, but I don't think pipes has any support for lazy I/O. This isn't really a big deal, but it is a nice feature in conduit that I'd like to keep. (I'm referring to the Data.Conduit.Lazy module, which has the lazyConsume function.)

Many of you probably saw a blog post a few days ago (sorry, I'm writing +offline, no links) analyzing the types in the conduit 0.3 release, and +pointing out that it would be possible to unify all three main datatypes +(Source, Sink, and Conduit) into a single type.

What followed was a very thorough discussion/debate on Reddit. My side of the +discussion basically came down to:

  • As seemingly obvious as this translation may appear, I won't believe it works properly until I see the code. I'm stubborn like that.
  • The blog post completely ignored BufferedSource, which is central to conduit.
  • Even if everything went perfectly for the first two points, we can't simply declare it more elegant because we did away with two types. Elegance is far too subjective, and particularly when comparing a concrete implementation to an abstract recommendation, it's a meaningless concept.

In other words: this idea seems interesting, but it's far too early to claim +victory. It needs more research. (I was a bit disappointed that this sentiment +seemed ungrounded to others on Reddit, but I digress.)

As it turned out, I was sitting on trains and busses for about 6 hours today, +and was sufficiently curious about this question to spend some time +researching. I implemented Twan's type (with one minor tweak, if I remember +correctly, to get the Monad instance to work) and got to work. Here are my +findings:

  • It is certainly possible to reimplement conduit using this modified type, and the resource semantics all seem to work correctly (i.e., all tests pass).
  • There is the possibility for a unified connect/fuse function that composes Pipes together. All of the original connect/fuse operators (excluding BufferedSource) can be implemented on top of that.
  • For the most part, implementation is greatly simplified by this approach. The only exceptions I noticed were the need to call absurd for the HaveOutput constructor of Sinks, and the need to deal with a meaningless NeedInput constructor for Source.

BufferedSource

OK... but what about BufferedSource? It's actually possible to leave it in +the package precisely as it appears in conduit 0.3. However, while working on +this, I came up with a different approach with I'm actually quite taken with. +To start, let's review the problem.

Imagine you're writing a webserver. You want it to work something like:

let src = sourceSocket socket
+headers <- src $$ getHeaders
+let req = makeRequest headers src
+app req

What's the problem? The makeRequest function needs to provide a way for the +application to read the request body. However, on line 2, when we connected the +source to getHeaders, some of the request body may have been included in the +data chunk read for the headers. As written, our code will discard that data, +and our application will not get a valid request body! Instead, with +BufferedSource, we would write:

bsrc <- bufferSource $ sourceSocket socket
+headers <- bsrc $$ getHeaders
+let req = makeRequest headers bsrc
+app req

A BufferedSource is able to keep track of its state, and keep track of any +leftover chunks from previous calls. This means that our previously discarded +bytes will be kept in a mutable variable inside bsrc, and provided for +makeRequest.

Instead, I'd like to introduce a new approach:

let src1 = sourceSocket socket
+(src2, headers) <- src $$& getHeaders
+let req = makeRequest headers src2
+app req

The difference is $$&, what I call the connect-and-resume operator (names +open for bikeshedding). The idea is to connect the source to the sink until the +sink terminates, then capture the final state of the source, combine it with +any leftovers provided by the sink, and return it, together with the return +value of the sink.

I haven't yet tested this in Warp and http-conduit, but I'm fairly optimistic +that it can completely supplant BufferedSource in both.

Elegance is in the eye of the beholder

So we're done, right? We got rid of two extra datatypes, plus got rid of +BufferedSource, and everything works. Time to release and go home.

Not quite. I still have one concern: is this actually a better solution? Do +people look at this new single type and say, "Hey, it's just a single type, I +can understand that!" or get confused as to why we need to use Void in a +Sink. Or how about the fact that Source and Conduit are now both +Monads, but they contain the result type instead of the output type?

In other words, I want input. I think I prefer the new formulation, though I +think having separate types has distinct advantages. One possibility that came +up today with Yitz Gale was using newtype wrappers for Source, Sink, and +Conduit. This would let us define only meaningful instances for all three, and +make sure that error messages stay simple. But will it result in too much +wrapping and unwrapping?

Thank you

I think I gave off the wrong impression in the discussion on Reddit. Some +people believed I wasn't interested in hearing their opinions. I hope this post +shows that this was not the case: I value input very much, and clearly what +you were saying has a lot of merit. So thank you.

Don't be surprised or offended when I respond to ideas with skepticism. I +believe that the issues at play here are more complicated than most people +appreciate. When I see an idea that has not yet been fully fleshed out, my +initial reaction is to challenge it, to see if it stands up to the test.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/shelly-for-shell-scripts.html b/public/blog/2012/03/shelly-for-shell-scripts.html new file mode 100644 index 00000000..45c72e2e --- /dev/null +++ b/public/blog/2012/03/shelly-for-shell-scripts.html @@ -0,0 +1,59 @@ + Shelly: Write your shell scripts in Haskell +

Shelly: Write your shell scripts in Haskell

March 12, 2012

GravatarBy Greg Weber

Haskell has traditionally been thought of as a great language for pure computation, but a poor one for "scripting". Lets make "scripting" more concrete by defining it as focused on being able to easily have the OS execute commands.

+

However, many Haskellers claim that Haskell is great for imperative programming. If this is the case, why is Haskell seemingly little used for scripting? I believe it is simply poor out of the box interfaces to running external commands, and a lack of development and awareness of good libraries to solve this problem.

+

I have programmed in Ruby for several years. Ruby is supposed to be great for scripting. It is inspired by Perl in how it interacts with the OS, and Perl is geared towards scripting. With greater experience, I have found Ruby to be a poor scripting tool. Ruby does have a minimum of ceremony and crucially provides easy interfaces for interacting with the OS, some of which are baked into the language. But Ruby also requires an interpreter, which requires a Ruby install. A Haskell binary is much easier to deploy if you can make sure it matches your OS and machine word size. Or you can always compile it on the machine in the same way you would interpret Ruby. Another issue with Ruby is that it is slow to startup due to the interpreting overhead. When I used the built in OptionParser library to make command line parsing easier I would get punished with over 100 milliseconds of extra startup time.

+

Ruby's weak-typing makes things slower, but it also creates testing drudgery. You run your program to see if it works, but only after it has invoked several programs (possibly with side-effects that you now need to undo) do you come across a dynamic error that could have been solved in Haskell at compile time. In Ruby we write reams of tests to solve these issues, but testing scripting is especially laborious due to the need to mock system interactions.

+

Shell scripting in a shell language like Bash suffers from the same weak-typing problems. Standard shell languages also suffer from non-intuitive syntax and keywords that I can never rembember -- I always have to reference existing shell scripts.

+

The easiest tool to improve the reliability of scripting is the same as with any other program: a strong but flexible type system, and this is Haskell's strong point.

+

Mostly what has been missing for Haskell is a decent API for systems programming. Haskell's current System API is capable of anything you would want to do, but lacking a coherent and intuitive API.

+

All of this is of course a chicken-and-egg problem: once more Haskellers write scripts they will improve the related libraries or create layers that improve them. Lets look at what is available now.

+

Haskell shell execution libraries

+

HSH

+

This is a fine piece of work, however the focus of the library is on two features that I don't care much about for my use cases.

+
  • directly piping between a shell command and a Haskell function
  • +
  • polymorphic output to get back your desired information about the command execution
  • +
+

So Instead I started using Shellish.

+

Shellish

+

Shelly maintains its own environment, making it thread-safe. Rather than polymorphic output, Shellish uses a state monad to maintain the information about the command execution (stderr, exit code, etc), in addition to environment information. Its implementation created a downside: it always loaded the command output into memory and held it in the state monad. A related bug of the library is that it did not print the command output until the command was finished.

+

Shelly

+

Shelly is my fork of Shellish. I fixed the aforementioned bug, switched the library to use Text everywhere, system-filepath and system-fileio for almost all of its system operations, and changed the interface to keep memory usage down.

+

While stderr is kept in memory in the state monad under the assumption that it should always be small, there are now 2 functions: run, and run_. The first returns stdout, while the second discards it. If you need to process a large stdout, runFoldLines lets you fold over stdout, processing it one line at a time rather than bringing it all into memory.

+

Forking is bad!

+

If this library gains the popularity that it deserves then we will all owe a big debt to Petr Rockai. I am very grateful that he built this library that showed me how to productively script in Haskell.

+

The original Shellish was made before most of the Shelly dependencies existed. This Shelly update is a big change with an incompatible interface change (stdout and a mix of stdout/stderr are not stored every time a command is ran). The original author likes the way Shellish works and doesn't have much time to maintain Shellish, let alone examine an overhaul.

+

On a somewhat unrelated note, Petr is a contributor to Darcs. So naturally Shellish used Darcs as version control, which is fine. However, there is only a darcs repo hosted on his site. I contacted him about a bug months ago via email, after which I started working on my fork. So there was no public visibility of this issue or of my fork. An open source project can't just have a code repo whether it is using darcs, git, or something else. It also need documentation (which can be satisifed with the haddocks on hackage), a way to contact the author (through hackage), and a bug tracker. Git is winning as version control only due to one of its technical merits: speed. The rest of the reason is Github. If your repo is on Github I know I can look at the issue tracker and even look at forks. I am not against using darcs for version control, just please also make sure to have an issue tracker. Shelly is hosted on github.

+

Example Shelly Code

+

In Shellish we always specify command arguments separately from the command rather than as a string. At first this seems like a burden, but it lends itself to cleaner code reuse.

+
import Shelly
import Prelude hiding (FilePath)

sudo_ com args = run_ "sudo" (com:args)

main = shelly $ verbosely $ do
apt_get "update" []
apt_get "install" ["haskell-platform"]
where
apt_get mode more = sudo_ "apt-get" (["-y", "-q", mode] ++ more)
+

Note that an underscore at the end of a function indicates we don't care about the result of running the command: the type will be () instead of Text.

+

Comparison with shell-scripting

+

Here is a larger example: a conversion of Yesod's source installer from bash shell to haskell shelly. The 2 look very similar.

+

Many of the lines look almost exactly the same. Overall I find the Haskell version slightly cleaner. I strongly prefer the Haskell version largely because there is a compiler behind it.

+

Haskell has some downsides though.

+
  • Haskell if/then/else structure is weird in comparison to all other languages
  • +
  • setting up a cabal file just to build a small script. This isn't required, but in practice it is necessary. cabal builds its executables into dist, so then I still write a tiny shell wrapper to launch the executable.
  • +
  • Need qualified names and/or spend time managing imports. Ruby in particular does better here by using OO to naturally resolve method names without conflict.
  • +
  • The Prelude gives us String and other things we don't want that cause conflicts. Unfortunately Haskell needs a major undertaking to banish String and instead use Text. There are also common functions (liftIO, when, unless, forM) for shell scripts that aren't exported from the Prelude. Shelly exports some of these. A solution could be to use an alternative Prelude.
  • +
  • using fromText/toText when converting between a FilePath and a Text. Using FilePath from system-filepath was not a decision I took lightly. It is more comvenient to just define

    +

    type FilePath = Text

  • +
+

But I didn't keep this because I found that in practice there aren't a lot of FilePath conversions required. shelly has toTextUnsafe/toTextWarn helpers for this.

+
  • Can't easily set global variables. Some might say this is just upside, but for a straightforward script I don't find threading state around quite as nice. This can be solved by different techniques though. We can at least reach close to the level of shell convenience by using unsafePerformIO to create top-level IORefs or possibly by doing away with some type-safety and adding a modifiable Map to the existing State Monad.
  • +
+

Conclusion

+

At this point, Haskell is behind in scripting libraries. Much shell scripting is in the context of installation, and other languages have frameworks to help accomplish this. But there is no reason why Haskell cannot catch up quickly. The release of the shake build library gives us a critical tool for specifying dependencies. I think a clean combination of shelly and shake could solve most scripting needs. I started to use them together for an installer.

+

Next time you need to do scripting, particularly for a Haskell project, try it in Haskell first, using a shell library. I think you will be pleasantly suprised.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/start-mezzo-haskell.html b/public/blog/2012/03/start-mezzo-haskell.html new file mode 100644 index 00000000..0d4c088a --- /dev/null +++ b/public/blog/2012/03/start-mezzo-haskell.html @@ -0,0 +1,32 @@ + Starting the Mezzo Haskell Book +

Starting the Mezzo Haskell Book

March 23, 2012

GravatarBy Michael Snoyman

A few weeks ago, following up on a post by Clint Moore, I +wrote on Google+ +that we should start an intermediate Haskell book as a community. The reaction +was very positive, and it seems like something people are really looking +forward to both working on and benefiting from. I started a Github repo +and added a README describing the project.

There are still some questions about both what the content of such a book would be, and the writing style. To hopefully kickstart some discussion of the matter, I put together three sample chapters:

I specifically chose these three topics, as I think they cover the three main +types of content we want in this book: recommended libraries, the language +itself, and practices surrounding the language.

At this point, I would like to ask the community to tear these chapters apart +as much as possible. Not so much for the technical content, but for the +teaching approach. Should the filehash example have started with the "wrong +way," or jumped straight to the correct approach? Should the typeclass chapter +follow motivating examples, or describe the features first? Should the tools +chapter walk the user through typical usage scenarios, or simply state the +purpose of each tool?

I really believe that as a community we can produce a top-notch book that will +make it easier for Haskellers to move from novice to advanced.

Also, I think Mezzo Haskell is a good codename for the book: it makes clear +that this is an intermediate book, covering neither the basics nor the truly +advanced concepts. But I'm not opposed to either giving the book a longer +title, or replacing that name entirely.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/03/summarizing-conduit-questions.html b/public/blog/2012/03/summarizing-conduit-questions.html new file mode 100644 index 00000000..17e972b7 --- /dev/null +++ b/public/blog/2012/03/summarizing-conduit-questions.html @@ -0,0 +1,86 @@ + Summarizing the conduit questions +

Summarizing the conduit questions

March 28, 2012

GravatarBy Michael Snoyman

My last blog post on +the prosposed changes to conduit got quite a discussion on Reddit. I +really appreciate everyone's input, thank you! In the process of the +discussion, a number of questions came up, and I'd like to summarize them here.

By the way, if anyone thinks I'm spending an inordinate amount of time on this +stuff... it's because I am :). This is the last remaining blocking issue for +the Yesod 1.0 release, so I'm trying to make a decision on this stuff quickly. +I don't want to make a bad decision under pressure of course, but if we can +come to some conclusions in the next week, it would be very nice to be able to +use the shiny new conduit 0.4 in Yesod 1.0.

Most importantly: is this a good change?

The first question is the most important. Is the move towards a single datatype +overall a good thing or a bad thing? Obviously the advantages are strong: a +single set of instances to maintain, a single fusion operator, less +constructors to deal with. However, we need to accept that there are also +downsides: error messages are more confusing and code needs to deal with +meaningless constructors (e.g., HaveOutput for Sink).

After playing around with this quite a bit, I'm strongly leaning towards saying +that the benefits outweigh the costs. The clincher for me was when I was able +to reimplement sequence and sequenceSink, and the two functions basically +disappeared. Compare the +old +and +new +versions.

I'm still hoping to hear from some conduit users on this to make sure the +changes won't be off-putting, but I think it's almost certain that the code +well be merged into master.

Side point: newtypes?

The idea came up of using newtypes for the Source, Sink and Conduit +types to try and have a best of both worlds. I still think it would be +beneficial (better error messages, and a nicer Functor instance for Source +and Conduit), but at least for now, I think it's too much overhead to have to +wrap/unwrap everywhere. There's also an argument to be made that a newtype +would hide away the "true nature" of our types, though I'm still on the fence +as to whether users should be confronted with the fact that the three types are +unified.

Type for second record in NeedInput

The NeedInput constructor has two records. The first takes some input and +returns a new Pipe. The second is for indicating that no input is available. +Unlike the early termination records for HaveOutput and PipeM, this record +gives a Pipe, since it's feasible that we may want to output more values +after we've run out of input (the typical example here is a decompression +Conduit).

Said another way: the early termination for HaveOutput and PipeM can only +ever be called when the upstream Pipe closes, not when the downstream Pipe +closes.

Anyway, the idea of this record is that it can't receive any input, since once +it's been called, we know that the upstream pipe has closed and won't be +providing any more input. There are two ways we could model this: set the i +parameter to (), or set it to Void.

However, there's also a third approach: keeping i as it was before. Since +we'll never be providing any more input to this Pipe, it's completely +irrelevant what the i parameter is. If the Pipe ever requests more input, +we'll just call the early termination Pipe again anyway. The advantage to +this third approach is that it simplifies some of the internal code, since we +don't need to juggle different parameters.

I'm leaning towards the third approach, but all three seem equally feasible.

Separate Leftover constructor?

There's a bit of an inconsistency, in that the Done constructor performs two +actions: it returns a result, and gives back any leftover input. Also confusing +is that we can only have 0 or 1 leftover values.

We could address the second issue by changing the Maybe to a list. +Alternatively, we could solve both issues by introducing a new Leftover +constructor, and modifying the Done constructor, like so:

| Done r
+| Leftover (Pipe i o m r) i

I've put this in a branch on Github, +and it certainly works. However, I think I'm most comfortable leaving code as-is:

  • We don't have the concept of chunking (i.e., dealing with more than one value at a time) anywhere else in the type, so why should the leftovers be different?
  • Right now, we have a nice invariant expressed in the types that you can only return leftovers when computation is complete. I think I like that setup.

Source: to Void or not to Void?

In order to ensure that a Sink never yields output, we set the o parameter +to Void. Initially, we set the i parameter on a Source to (), so that +runPipe can just provide an infinite stream of unit values.

However, we can just as well set the i parameter to Void, and then call the +no-more-input record of NeedInput. I'm not going to try and summarize the +arguments back and forth on this one, because there are a lot of them. I will +say that I'm leaning towards Void, just because it gives a very nice parallel +between Source and Sink.

Fuse operators: unify?

There's now a fusion function (pipe) which can fuse together Sources, +Sinks and Conduits. All three fusion operators ($=, =$, and =$=) are +simply type-constrained wrappers around it. ($$ also utilizies pipe, but it +also calls runPipe on the generated Pipe.) The question is: do we need all +three operators, or should we have just one?

The advantage of separate operators is clearer error messages, and more +explicit code. However, it hides the fact that all three types are really one +and the same. (Again, I'm ambivalent as to whether that hiding is a feature or +a bug.) It also means that people have to learn more names.

So should we have a unified fusion operator? And what would it be called?

Note: either way, this next release will still contain the other three, +type-constrained fusion operators, if only to ease migration. If we add a +unified operator, it would be in addition to those three for now, and likely +after a few point releases we would deprecate the three operators.

Bikeshedding: rename the $$& operator

This is likely the easiest. I call $$& the connect-and-resume operator, and +it connects a Source to a Sink, gets a result, and also gives back the most +recent state of the Source. This allows us to continue computation.

Frankly, I chose a pretty bad name for the operator. (In my defense, I did that +on purpose to make sure I didn't become attached to it.) Some other ideas that +have been floated are $$- and $$+. I feel no particular drive one way or +another here.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/announcing-yesod-1-0.html b/public/blog/2012/04/announcing-yesod-1-0.html new file mode 100644 index 00000000..17c8c2b5 --- /dev/null +++ b/public/blog/2012/04/announcing-yesod-1-0.html @@ -0,0 +1,52 @@ + Announcing Yesod 1.0 +

Announcing Yesod 1.0

April 9, 2012

3 years of development. 80 packages. Dozens of contributors. Thousands of commits.

The Yesod team is very pleased to announce the release of Yesod 1.0. As +expected, this was a minor incremental upgrade over version 0.10. You can see +a full changelog on the +Wiki. The only possibly confusing change I'm aware of is the meaning of +approot versus host in the config file; please see the changelog for more +details.

We're very proud of this release. With 1.0, we're signaling that we've achieved +feature maturity with an API we're happy with. We aren't stopping of course; we +already have plans for the next wave of features, mostly focusing on better +client side integration. But we consider 1.0 a strong foundation to build on +top of.

The rest of this post is intended as an introduction to Yesod for new users.


Introductory Screencast

Yesod 1.0 Introductory Screencast from Michael Snoyman on Vimeo.

Why another web framework?

The main goal of Yesod is to provide robustness. We want a web framework that +helps you build secure sites and minimize production bugs. In this endeavor, we follow the +mantra that the compiler is your ally, not your enemy. To make this happen, +we use Haskell's strong typing to eliminate entire classes of bugs and security +holes. This applies to everything from creating valid links +(type-safe URLs), avoiding Cross +Site Scriping (XSS) attacks, and automatic data marshaling.

Those familiar with more commonly used statically typed languages, like +Java or C++, may have already decided that the extra safety is not worth it. +Two complaints from traditionaly statically typed languages are:

  • They're just so verbose!
  • I haven't written a single bug that would have been caught by a compiler.

We believe Haskell solves both of these complaints. Due to type inference, you +rarely have to give a type signature in your Haskell code. Many of us choose to +anyway, and consider it a form of documentation which is enforced by the +compiler. But in most cases it's optional.

As for the second point: it's true that the type systems in Java and C++ make +it difficult to express program invariants well. In Haskell, however, the +expressive type system let's us do much more. If you've ever had an XSS +vulnerability, generated an invalid link, treated a stream of bytes as text, or +just made a typo, the compiler can help you. This means that in Haskell, you +don't have to waste time writing unit tests for the boring, mundane stuff. Let +the compiler handle it for you automatically, and you can worry about the more +important issues.

The result: Yesod is a web framework with a level of productivity rivaling +Rails or Django, but with greater security and much easier code maintenance.

The other advantages

While robustness was our main goal in creating Yesod, we've also achieved other +major benefits as well:

  • Asynchronous made easy. Everyone keeps talking about how important asynchronous programming is for creating servers. In other systems, you have to restructure your code to work with callbacks. Haskell's multithreaded runtime does all the heavy lifting for you. You write simple code that asks for data and sends it, and Haskell's compiler (GHC) will restructure it to use the appropriate event library for your OS. This allowed us to write a powerful and fast webserver in about 500 lines of code.
  • Scalable and performant. Yesod lets you write simple, high-level code, and gives you good performance. But when you need more, you can tune your compiled code for something even faster. Many of Yesod’s base libraries work exactly this way: providing a nice, safe interface for users while getting near-C performance with direct memory access. The GHC compiler ensures we get fast machine code at the end of the day.
  • Light-weight syntax. A lot of web development is boilerplate. Setting up routing tables, creating database schemas, and dealing with forms can all be long, repetitive code. Yesod’s has simple DSLs for templating, persistence, routing, and much more. But more importantly the DSLs are correct: they are all compile-time checked to get rid of the runtime bugs.

Learn more

We'll be coming out with a few more introductory blog posts over the next few +weeks, and hopefully a few more screencasts too. If you're ready to jump in, +the getting started guide gives you +all the information you need to get up-and-running with Yesod, as well as a +number of handy links. If you want a third-party opinion on what makes Yesod great, +check out Yesod excellent ideas.

And later this month, O'Reilly will be publishing our +first book on Yesod. You can already +read it online.

We're very happy with Yesod, and think it can be a valuable tool for many web +developers. We hope you do too!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/cabal-meta.html b/public/blog/2012/04/cabal-meta.html new file mode 100644 index 00000000..277ac5d5 --- /dev/null +++ b/public/blog/2012/04/cabal-meta.html @@ -0,0 +1,32 @@ + Transcending to dependency heaven +

Transcending to dependency heaven

April 5, 2012

GravatarBy Greg Weber

using cabal-meta to transced to dependency heaven

dependency hell

Dependency hell is arguably Haskell's biggest problems. +Collectively it hurts everyone's productivity and makes people think twice before adding another dependency to a project or installing a new project. Yesod is about to do our 1.0 release. This is a statement that we have a mature, if imperfect framework. Cabal is now actually our weakest link. It is the biggest stumbling block for new and experienced Haskell users and a large barrier to contributions.

The latest version of Cabal has a few changes to help the situation out:

  • a new constraint solver, which should make dependency Hell less frequent.
  • a warning before before breaking an existing installation.

Neither of these truly fix an install problem, they just make the problems less likely to occur.

Yesod solutions

Besides making April Fools jokes about Cabal, we:

  • support cabal-dev (an isolation tool) in our tools, such as the development mode server. We also encourage the usage of virthualenv/hsenv which requires no special support on our side
  • released yackage, a local hackage server that helps make the right dependencies visible
  • released cabal-src, a tool for making cabal remember locally installed packages. Note this functionality is already in cabal-dev.
  • released yesod-platform, a meta-package that specifies exact versions of Yesod dependencies. Note that initially we had a tool called cabal-nirvana with a similar approach, but there were some issues with it that will require changes to cabal

Even with all these tools, one day I found myself completely incapable of installng Yesod from source. +The first thing I did was create meta packages like yesod-platform. These do in fact solve the problem. +However, they are difficult to maintain because they are just duplicates of existing information. +I realized that cabal can already do this for me: all I have to do is install everything at once.

The cabal-meta approach

If you run this command, you can easily get a failure:

cabal install foo && cabal install bar

Whereas if you run this command it should almost always work:

cabal install foo bar

So I changed the Yesod installer scripts to use this approach. +However, even though I could install yesod from source, I found it did not guarantee that I could build my Yesod application. +I needed to installi both yesod source and my application at the same time.

Today I am officially releasing cabal-meta, a tool which facilitates installing all dependencies at the same time.

My experience and that of the few others that have already used cabal-meta is that if an installation is possible, it will succeed. +Most of the usage has been on top of hsenv. I am sure there are cases where installs will not succeed and certainly this can break existing installs.

However, the successful combination of hsenv and cabal-meta has changed my attitude about Haskell installs: I am completely fearless. I now have one less source of incidental complexisty weighing me down every time I use Haskell.

sources.txt

When invoked, cabal-meta looks for a file sources.txt. +Each line of sources.txt is either a hackage package, a directory, or a github repo (which is cloned as a directory). +A directory is either a local cabal package or contains another sources.txt to recurse into. +Here is my sourcex.txt for my Yesod application:

./
+sphinx -fone-one-beta
+path/to/yesod/sources

./ refers to the current directory and thus your current project. +sphinx is a hackage package. This is just showing off a somewhat unrelated feature of being able to specify build flags. This lets me stop worrying about build flags and instead just write them down.

To build a Yesod application using the latest code, create a sources.txt in the project directory with:

./
+https://github.com/yesodweb/yesod
+https://github.com/yesodweb/shakespeare
+https://github.com/yesodweb/persistent
+https://github.com/yesodweb/wai

This is showing off another feature of cabal-meta: installing from github. Code is placed in a vendor/ directory and updated on every install. We intend to use cabal-meta for future Yesod beta releases.

Please see the full cabal-meta documentation

Oh no, another Cabal wrapper!

Someone might also rightfully complain that this functionality should be merged into Cabal. I discused this tool a little with Andres, who has already done a great job improving Cabal, and he had some different ideas on the subject. I could spend hours trying to convince everyone that this idea is correct, or I could spend that time writing some code and then we can have a conversation about how this concrete tool actually works, based on real user experience. Also, it is much quicker to write a small wrapper: I can use my shelly library and forget about a whole bunch of concerns the official tool would have. So I believe the wrapper approach is better while still exploring the idea. Once the functionatliy of the wrapper is deemed successfull, its functionality should be merged in to Cabal.

The bigger picture

This tool is partly inspired from the Ruby Bundler tool, in particular the ability to source local directories and remote repostitories. In addition to these features, Bundler has solved dependency hell in Ruby. Bundler has an easier time isolating installs because a dynamic language does not need to recompile an existing library if the same version is required but with different dependencies.

Someone might rightfully complain that these solutions are band-aids over existing issues. To me the bigger issue is that every Haskeller user needs to spend at least an hour learning how to use hsenv/cabal-dev and cabal-meta to properly install their code. They still must continually think about how to properly use their sandboxes. A new Haskeller is going to read articles and see documentation that just invokes cabal and they are still going to end up in dependency hell. Their only sure way out is to reach out to the community, but that may not happen, and they may decide the hassle of Haskell is not worth the effort.

There are 2 Google Summer of Code proposals already that solve some of the root problems of Cabal dependency hell. I am hoping at least 1 will be accepted.

For now though, give cabal-meta a try (along with hsenv/cabal-dev) and be fearless with your Haskell installs!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/client-side.html b/public/blog/2012/04/client-side.html new file mode 100644 index 00000000..51743e6c --- /dev/null +++ b/public/blog/2012/04/client-side.html @@ -0,0 +1,132 @@ + Client Side Yesod, an FRP-inspired approach +

Client Side Yesod, an FRP-inspired approach

April 22, 2012

GravatarBy Michael Snoyman

We've said for a while that our big feature in the post-1.0 world of Yesod +would be better client side integration. Well, we had the 1.0 release about two +weeks ago. That's enough time to relax, time to keep plowing ahead!

There are many different approaches we could consider taking for client side +integration. I'm going to describe one here that I've started working on. But +that doesn't mean any decisions have been made. The purpose of this post is to +get the discussion started on the direction we want to take, and hopefully +start to flesh out more clearly what our goals are.

Motivating case: name concatenator

As usual for something as ambitious as a completely new paradigm for client +side development, let's start with a use case that doesn't reflect any real +world use at all. We want to create a page where you can type in your first and +last names, and as you type them, a span on the page will automatically be +updated with your full name. (Yes, I know this isn't very i18n-friendly...)

Let's think about what we'd have to do if we want to write this manually:

  • Set up some HTML with the input fields and the span.
  • Generate some kind of unique identifiers for each of those three.
  • On page load, bind event handlers to the keyup event of the input fields which will somehow adjust the span.

Not too bad. Yesod will already help us with the unique identifiers with +newIdent. We can use hamlet to put together the HTML without any trouble, and +then just write some Javascript for the event handling. So why isn't this an +acceptable solution to the problem?

  • We have to write in two completely different languages: Haskell and Javascript.
  • There's no way for the compiler to help us with ensuring the Javascript code is correct.
  • We're going to have almost identical code for the two text fields, and there's no immediately obvious way to avoid code duplication.
  • The solution is not composable, and doesn't scale well for large projects.

An approach: FRP-inspired monadic EDSL

I've put together a proof of concept +library. Let's look at an example that implements the behavior I described +above.

getHomeR :: Handler RepHtml
+getHomeR = defaultLayout $ runJS $ do
+
+    (firstWidget, firstVal) <- textInput
+    (lastWidget, lastVal) <- textInput
+
+    let fullVal = firstVal `jsPlus` " " `jsPlus` lastVal
+    fullWidget <- textOutput fullVal
+
+    lift [whamlet|
+<p>First name: ^{firstWidget}
+<p>Last name: ^{lastWidget}
+<p>Full name: ^{fullWidget}
+|]

We use the textInput function to generate two values: a widget containing the +input tag, and some special value. We then use the jsPlus function to +concatenate the first value and the last value, with a space in the middle. We +plug this value into the textOutput function to get a widget containing a +span tag that will display the full name. And finally, we stick all of those +widgets into a final widget and display it.

Personally, this kind of feels like an ideal solution to this specific problem. +I can't guarantee that this approach will scale, or that it will work well for +all kinds of client side code, but it feels like a nice fit right now. I'd love +to get feedback.

The devil's in the details

Let's get into the details a bit. The easiest thing to start with is looking at +the type signatures involved:

data JSValue jstype
+data JSTypeString
+type JSString = JSValue JSTypeString
+type JS sub master = WriterT JSData (GWidget sub master)
+
+runJS :: YesodJquery master => JS sub master a -> GWidget sub master a
+textInput :: JS sub master (GWidget sub master (), JSString)
+textOutput :: JSString -> JS sub master (GWidget sub master ())
+
+class JSPlus a
+instance JSPlus JSTypeString
+jsPlus :: JSPlus jstype => JSValue jstype -> JSValue jstype -> JSValue jstype

We have a new datatype JSValue which includes a +phantom type. This phantom states the +Javascript datatype of the value. We can use this to prevent us from accidently +adding a string and a number together, for example. Notice that JSString is +just a JSValue with a JSTypeString phantom.

We also introduce a new monad, JS, which our functions need to live inside. +runJS is our unwrapper for this monad. When we run it, all necessary +Javascript is automatically generated and added to our Widget.

The textInput function does exactly what I explained before: It gives back a +widget and a value. But now we can see that said value is just a JSString. +Likewise, when textOutput takes in a value, it's a JSString as well.

Finally, we have the jsPlus function, which will only allow you to add +together two things of the same type. I used a typeclass to restrict what could +be added; an alternate approach would be to have a separate function for each +type of "plussing", e.g. jsPlusString, jsPlusInt, etc. I have no strong +feeling on which approach should be taken.

By the way, the reason we could use jsPlus on " " is because of +OverloadedStrings and the following instance:

instance (JSTypeString ~ jstype) => IsString (JSValue jstype) where
+    ...

The generated Javascript

To better understand the next section, let's take a sidetrack to analyze the +generated Javascript. Adding in some indentation:

$(function(){
+    var h2; // first name
+    var h4; // last name
+
+    var h6 = function() {
+        $("#h7").text((h2||'') + " " + (h4||''))
+    };
+
+    $("#h3").keyup(function(){
+        h2 = $(this).val();
+        h6()
+    });
+
+    $("#h5").keyup(function(){
+        h4 = $(this).val();
+        h6()
+    });
+});

We have two variables: h2 and h4. (All the names are auto-generated via +newIdent.) These are used to cache the values in the first and last name +input fields, respectively. Next, we have a function h6, which uses the h2 +and h4 variables to create the full name. It places that result in the +appropriate span tag, h7 being the auto-generated ID of the span tag.

Finally, we bind to the keyup events for both of the input fields. Each time, +we cache the value in the field to the appropriate local variable, and then +call the h6 function to update the span.

Now let's see how we generate such Javascript.

The JS monad

If you look at the definition of the JS monad, you'll see that it's just a +WriterT for JSData. So really, all of the magic for this code lives in that +datatype. Let's look at this thing:

data JSData = JSData
+    { jsdEvents :: Map.Map JSVar (Set.Set JSFunc -> Builder)
+    , jsdVars :: Set.Set JSVar
+    , jsdFuncs :: Map.Map JSFunc Builder
+    , jsdDeps :: Map.Map JSVar (Set.Set JSFunc)
+    }
+
+newtype JSVar = JSVar Text
+newtype JSFunc = JSFunc Text

Let's start with the simple ones, and build our way up. jsdVars is simply a +set of all the variables that we're going to define. Each time we use +textInput, it auto-generates a new variable name and adds it to this set. +That's how we got our list of variable declarations in our output Javascript.

Next we have jsdFuncs, which maps function names (e.g., h6) to their +bodies. This would be generated by textOutput, specifying how to update the +output field.

jsdDeps is a connection between the previous two fields. It specifies which +functions depend on which variables. So for our example, it would look +something like:

Map.fromList [("h2", Set.singleton "h6"), ("h4", Set.singleton "h6")]

To understand how this works, let's look at the definition of JSValue:

data JSValue jstype = JSValue
+    { jsvExpr :: Builder
+    , jsvDeps :: Set.Set JSVar
+    }

A JSValue is just two pieces of information: a Javascript expression, and a +set of variables it depends on. When we pass a value to textOutput, it +creates a new function (in this case, h6) which uses the jsvExpr to update +the span tags, and then creates dependencies between each variable in jsvDeps +and that function. What we're saying is: each time one of the underlying +variables gets updated, please call this function.

Finally, we get to jsdEvents. This is where we specify how to update each +variable. It maps the variable name to a funny type:

Set.Set JSFunc -> Builder

What this is saying is: Tell me which functions depend on my value. Then I'll +set up an event handler that will update the local variable and call any +dependent functions and update them as well.

That's all for now

I'm hoping this sparks a discussion about benefits and weaknesses of the +approach I've set up here, and possible alternatives. If you want to see more +of the internals of how this is implemented, +check out the sourcecode.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/debugging-openid.html b/public/blog/2012/04/debugging-openid.html new file mode 100644 index 00000000..106502d1 --- /dev/null +++ b/public/blog/2012/04/debugging-openid.html @@ -0,0 +1,43 @@ + A Debugging Adventure: OpenID +

A Debugging Adventure: OpenID

April 19, 2012

GravatarBy Michael Snoyman

I got an email today reporting a bug on Haskellers.com. Someone's OpenID (to +protect the innocent, we'll call it https://example.com) was not working. +Here's the story of debugging it.

I started off (of course) by trying to reproduce it. I went to Haskellers and +tried logging in with the OpenID, and sure enough got this cryptic message:

data: end of file

This immediately caught my attention, because I didn't remember ever creating +such an error message. So I thought about the library stack at play here:

  1. haskellers.com
  2. yesod-auth
  3. authenticate
  4. http-conduit
  5. tls

I figured that the error message must have come from tls, because it's the +only library in that stack that I didn't write. So I put together a quick test +case to use http-conduit to connect to the server. And it worked perfectly. So +the bug seemed to not be from either http-conduit or tls.

OK, let's start over from the top of the stack. I took my yesod-auth test +program and tried to connect to log in to example.com. Same error message. So +it's not caused by Haskellers. I tried using my authenticate test program. +Again, same error message. But now I know it must be coming from somewhere in +authenticate, right?

Now's the time to use the golden hammer of debugging: print statements. Using +this powerful and sophisticated technique, I traced the problem to +getForwardUrl, +then to +discover, +then to +discoverYADIS.

At this point, there were two things bothering me:

  • I still had no idea where that error message was coming from.
  • discoverYADIS was being called twice. It succeeded the first time, and failed the second.

It's not surprising that the discoverYADIS function is called twice, it's the very +nature of OpenID. Usually, we connect to the OpenID specified by the user, only +to find an HTML tag or HTTP header telling us to look elsewhere for the rest of +the login information. But why was it failing the second time around?

I got the two URLs that were being requested, and went back to my http-conduit test program. I ran something along the lines of:

simpleHttp "https://example.com/"
+simpleHttp "https://example.com/?xrds"

No problem at all. So both URLs seemed to work. Then I got an idea: maybe it's +a problem caused by connection sharing. So I modified the test program:

withManager $ \m -> do
+    req <- parseurl "https://example.com/"
+    httpLbs req m
+    httpLbs req m

Boom! I got the exact same data: end of file error message. Hurrah! Now +debugging could focus on just http-conduit and tls. Somehow, the request +was failing when we were reusing a connection.

But I already had code for that case. tl;dr: If any exceptions occur when sending the request, and we're reusing an old connection, then start over with a fresh connection.

But what we weren't handling was the case when the exception was thrown when +reading the response headers. Our example.com server was behaving as follows:

  • Accept the first request
  • Send the first response
  • Accept the second request
  • Close the connection

This doesn't seem very logical to me, and perhaps there was something else involved that forced the server to close the connection when it did. Either way, the fix is simple: catch exceptions when reading the response headers.

So there you have it: start to finish debugging of an OpenID bug.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/replacing-cabal.html b/public/blog/2012/04/replacing-cabal.html new file mode 100644 index 00000000..4bc8ae9d --- /dev/null +++ b/public/blog/2012/04/replacing-cabal.html @@ -0,0 +1,51 @@ + Replacing Cabal +

Replacing Cabal

March 32, 2012

GravatarBy Michael Snoyman

There's been a lot of discussion recently about ways to improve cabal. +While we can continue to work to add incremental improvements, such as +the upcoming and much-improved dependency solver, I think there's a +deeper philosophical issue at play, and I would like to explore that.

Let's start by analyzing the central philosophy underpinning cabal, +and its standard workflow. You connect to a central source (Hackage) +of code, download it to your system, build it, and use it. This seems +very straight-forward, and seemingly is the correct approach.

But now I want to sidetrack into some of Yesod's underlying +philosophy, and demonstrate its incompatibility with cabal. Many +people know that Yesod is Hebrew for "Foundation." What you may not +realize is that it's also a term from Jewish mysticism. Jewish +mysticism, also known as Kaballah, includes the concepts of receiving +energy from a source.

While this seems similar on a superficial level to what cabal does, +it's actually quite different. In cabal, you download code, telling +the source precisely what to send and when to send it. In Kaballah, +you would receive. Also, the Kaballistic approach would be that the +energy you receive is pure and perfect. Cabal instead has the audacity +to try and build the code it receives, perverting it into something +else!

So, to rectify this mismatch, the Yesod team is announcing the start +of a new build management tool: cabala.

cabala workflow

Following Kaballistic patterns, we need to open ourselves to receiving +new code. To do so, we will run the command:

cabala receive

This command will block until the cabala server sends a new package. +Of course, cabala is not so arrogant as cabal. You do not get to +choose which package will be downloaded, the source will determine +this automatically for you.

As hinted at above, cabala will not be so impudent as to try to +"improve" the code by building it. That is, of course, blasphemy: +Haskell is the One True Code that created even the angels:

יצרם בדעת בבינה ובהשכל (He create them with knowledge, understanding, and Haskell)

Instead, the code is kept in its original, pure form. Ultimately, we +hope to build a machine capable of running unmodified Haskell. Until +then, we will follow another Kaballistic principle: random pieces of +the code you have received will be displayed to your screen, allowing +you to meditate on their meaning.

Outcome

We can guarantee with 100% certainty that there will be an immediate +drop in cases of dependency hell to 0. Since cabala will perform no +dependency analysis, it's simply not possible. Additionally, we expect +instances of build failures to go down to 0 as well.

Another interesting improvement will be performance. We guarantee that +all programs will run in under 1 second with the new system.

There is, of course, a downside. It may take a significant amount of +time to prepare yourself to properly receive cabala. However, this is +a one-time cost, and the extreme long-term benefits more than amortize +it away. As Haskellers, we should already be used to high learning +curves.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/skinning-conduits.html b/public/blog/2012/04/skinning-conduits.html new file mode 100644 index 00000000..5c6cc679 --- /dev/null +++ b/public/blog/2012/04/skinning-conduits.html @@ -0,0 +1,218 @@ + Many ways to skin a conduit +

Many ways to skin a conduit

April 17, 2012

GravatarBy Michael Snoyman

There's more than one way to skin a cat, and certainly more than one way to +write code. The various options can sometimes be confusing. And in the case of +the conduit library, there are also some routes that you shouldn't take. +You'll see what I mean through the examples.

For the most part, using existing Sources, Sinks, and Conduits is +straight-forward. The problem comes from writing them in the first place. Let's +take a simple example: we want a Source that will enumerate the Ints 1 to +1000. For testing purposes, we'll connect it to a Sink that sums up all of +its input. I came up with six different ways to write the Source, though two +of those are using functions I haven't yet released.

import Criterion.Main
+import Data.Conduit
+import qualified Data.Conduit.List as CL
+import qualified Data.List
+import Data.Functor.Identity (runIdentity)
+
+sourceList, unfold, enumft, yielder, raw, state
+    :: Monad m
+    => Int -- ^ stop
+    -> Source m Int
+
+sourceList stop = CL.sourceList [1..stop]
+
+unfold stop =
+    CL.unfold f 1
+  where
+    f i
+        | i > stop = Nothing
+        | otherwise = Just (i, i + 1)
+
+enumft stop = CL.enumFromTo 1 stop
+
+yielder stop =
+    go 1
+  where
+    go i
+        | i > stop = return ()
+        | otherwise = do
+            yield i
+            go $ i + 1
+
+raw stop =
+    go 1
+  where
+    go i
+        | i > stop = Done Nothing ()
+        | otherwise = HaveOutput (go $ i + 1) (return ()) i
+
+state stop =
+    sourceState 1 pull
+  where
+    pull i
+        | i > stop = return StateClosed
+        | otherwise = return $ StateOpen (i + 1) i
+
+main :: IO ()
+main = do
+    mapM_ test sources
+    defaultMain $ map bench' sources
+  where
+    sink :: Monad m => Sink Int m Int
+    sink = CL.fold (+) 0
+
+    bench' (name, source) = bench name $ whnf (\i -> runIdentity $ source i $$ sink) 1000
+
+    sources =
+        [ ("sourceList", sourceList)
+        , ("unfold", unfold)
+        , ("enumFromTo", enumft)
+        , ("yield", yielder)
+        , ("raw", raw)
+        , ("sourceState", state)
+        ]
+
+    test (name, source) = do
+        let res = runIdentity $ source 1000 $$ sink
+        putStrLn $ name ++ ": " ++ show res

sourceList is probably the approach most of us- myself included- would +actually use in real life. It let's us take advantage of all of the +list-processing functions and special syntax that Haskell already provides. +unfold and enumFromTo are both new functions for 0.4.2 (in fact, I wrote +them for the purpose of this comparison). They correspond very closely to their +Data.List and Prelude counterparts.

yield is a new option we have starting with conduit 0.4. Due to the unified +datatypes, Source has inherited a Monad instance. This allows us to fairly +easily compose together different Sources, and the yield function provides +the simplest of all Sources. In previous versions of conduit, we could have +used Source's Monoid instance instead of do-notation.

raw goes directly against the datatypes. I find it interesting that the raw +version isn't really much more complicated than yield or sourceState, +though you do have to understand some of the extra fields on the constructors. +Finally, we use sourceState. This is one of the oldest approaches, since this +function has been available since the first release of conduit. I think that +this function would compile and run perfectly on conduit 0.0.

The Criterion benchmarks are very informative. Thanks to Bryan's cool new +report, let's look at the graph:

unfold, enumFromTo, and raw all perform equally well. sourceList comes +in behind them: the need to allocate the extra list is the culprit. Behind that +is yield. To see why, look at the difference between yielder and raw. +They're structure almost identically. For the i > stop case, we have return +() versus Done Nothing (). But in reality, those are the same thing! +return is defined as Done Nothing.

The performance gap comes from the otherwise branch. If we fully expand the +do-notation, we end up with:

yield i >>= (go $ i + 1)
+==> HaveOutput (Done Nothing ()) (return ()) i >> (go $ i + 1)
+==> HaveOutput (Done Nothing () >> (go $ i + 1)) (return ()) i
+==> HaveOutput (go $ i + 1) (return ()) i

Which is precisely what raw says. However, without adding aggressive inlining +to conduit, most of this transformation will occur at runtime, not compile +time. Still, the performance gap is relatively minor, and in most real-world +applications should be dwarfed by the actual computations being performed, so I +think the yield approach definitely has merit.

What might be shocking is the abysmal performance of sourceState. It's a full +8 times slower than raw! There are two major contributing factors here:

  • Each step goes through a monadic bind. This is necessitated by the API of sourceState.
  • We have to unwrap the SourceStateResult type.

sourceState was great when it first came out. When conduit's internals were +ugly and based on mutable variables, it provided a clean, simple approach to +creating Sources. However, conduit has moved on: the internals are pure and +easy to work with and we have alternatives like yield for high-level stuff. +And performance wise, the types now distinguish between pure and impure +actions. sourceState forces usage of an extra PipeM constructor at each +step of output generation, which kills GHC's ability to optimize.

So our main takeaway should be: don't use sourceState. It's there for API +compatibility with older versions, but is no longer the best approach to the +problem. Similarly, we can improve upon sourceIO, but we have to be a bit +careful here, since we have to ensure that all of our finalizers are called +correctly. Let's take a look at a simple Char-based file source, comparing a +sourceIO implementation to the raw constructors.

import Data.Conduit
+import qualified Data.Conduit.List as CL
+import Control.Monad.Trans.Resource
+import System.IO
+import Control.Monad.IO.Class (liftIO)
+import Criterion.Main
+
+sourceFileOld :: MonadResource m => FilePath -> Source m Char
+sourceFileOld fp = sourceIO
+    (openFile fp ReadMode)
+    hClose
+    (\h -> liftIO $ do
+        eof <- hIsEOF h
+        if eof
+            then return IOClosed
+            else fmap IOOpen $ hGetChar h)
+
+sourceFileNew :: MonadResource m => FilePath -> Source m Char
+sourceFileNew fp = PipeM
+    (allocate (openFile fp ReadMode) hClose >>= go)
+    (return ())
+  where
+    go (key, h) =
+        pull
+      where
+        self = PipeM pull close
+        pull = do
+            eof <- liftIO $ hIsEOF h
+            if eof
+                then do
+                    release key
+                    return $ Done Nothing ()
+                else do
+                    c <- liftIO $ hGetChar h
+                    return $ HaveOutput self close c
+        close = release key
+
+main :: IO ()
+main =
+    defaultMain [bench "old" $ go sourceFileOld, bench "new" $ go sourceFileNew]
+  where
+    go src = whnfIO $ runResourceT $ src "source-io.hs" $$ CL.sinkNull

The results are much closer here:

We're no longer getting the benefit of avoiding monadic binds, since by its +very nature this function has to call IO actions constantly. In fact, I +believe that the performance gap here doesn't warrant avoiding sourceIO in +normal user code, though it's likely a good idea to look at optimizing the +Data.Conduit.Binary functions. Perhaps even better is if we can get some +combinators that make it easier to express this kind of control flow.

The story is much the same with Sinks and Conduits, so I won't bore you +with too many details. Let's jump into the code first, and then explain what we +want to notice.

import Criterion.Main
+import Data.Conduit
+import qualified Data.Conduit.List as CL
+import qualified Data.List
+import Data.Functor.Identity
+
+main :: IO ()
+main = defaultMain
+    [ bench "mapOutput" $ flip whnf 2 $ \i -> runIdentity $ mapOutput (* i) source $$ sink
+    , bench "map left" $ flip whnf 2 $ \i -> runIdentity $ source $= CL.map (* i) $$ sink
+    , bench "map right" $ flip whnf 2 $ \i -> runIdentity $ source $$ CL.map (* i) =$ sink
+    , bench "await-yield left" $ flip whnf 2 $ \i -> runIdentity $ source $= awaitYield i $$ sink
+    , bench "await-yield right" $ flip whnf 2 $ \i -> runIdentity $ source $$ awaitYield i =$ sink
+    ]
+  where
+    source :: Monad m => Source m Int
+    source = CL.sourceList [1..1000]
+
+    sink :: Monad m => Sink Int m Int
+    sink = CL.fold (+) 0
+
+    awaitYield :: Monad m => Int -> Conduit Int m Int
+    awaitYield i =
+        self
+      where
+        self = do
+            mx <- await
+            case mx of
+                Nothing -> return ()
+                Just x -> do
+                    yield $ x * i
+                    self

There are five different ways presented to multiple each number in a stream by +2. CL.map is likely the most obvious choice, since it's a natural analogue to +the list-based map function. But we have two different ways to use it: we can +either left-fuse the source to the conduit, and then connect the new source to +the sink, or right-fuse the conduit to the sink, and connect the source to the +new sink.

We also have an awaitYield function, which uses the await and yield +functions and leverages the Monad instance of Conduit. Like map, we have +both a left and a right version.

We also have a mapOutput function. In that case, we're not actually using a +Conduit at all. Instead, we're modifying the output values being produced by +the source directly, without needing to pipe through an extra component. Let's +see our benchmark results:

There are three things worth noticing:

  1. Like previously, the high-level approach (using await and yield) was slower than using the more highly optimized function from Data.Conduit.List.
  2. There no clear winner between left and right fusing.
  3. mapOutput is significantly faster than using a Conduit. The reason is that we're able to eliminate an entire extra Pipe in the pipeline.

mapOutput will not be an option in the general case. You're restricted in a number of ways:

  • It can only be applied to a Source, not a Sink.
  • You have to have transformations which produce one output for one input.
  • You can perform any monadic actions.

However, if your use case matches, mapOutput can be a very convenient optimization.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/working-together.html b/public/blog/2012/04/working-together.html new file mode 100644 index 00000000..11c50722 --- /dev/null +++ b/public/blog/2012/04/working-together.html @@ -0,0 +1,82 @@ + Working Together +

Working Together

April 10, 2012

GravatarBy Michael Snoyman

It's no secret that there are different approaches to web development in +Haskell. Arguably, the two most discussed frameworks in recent times have been +Snap and Yesod (with no offense intended to Happstack). I can't speak for the +Snap team, but from the Yesod team, our approach- initiated by myself- has been +to mostly ignore what's going on with Snap. We collaborate together where we +can (which is actually fairly often), but for the most part we are two +frameworks, heading in two directions, and there's no point at constantly +rehashing arguments about the "better" approach.

While in theory I think this tactic of letting each framework develop +separately is correct, I was sorely mistaken. As evidenced by the questions +raised by newcomers on the forums discussing the Yesod 1.0 release, it's +obvious that a major question on people's minds is: what makes these two +frameworks different?

I can understand that for some Yesoders, this was a bit disheartening. We have +no intention of comparing ourselves to Snap. We don't even see the two +frameworks as competing (more on that later). But the response of some of us in +both recent and past discussions has not always been of the calibre that it +should. I myself am guilty of the same, and I'd like to offer an apology for +offense given to developers and users of other systems.

Besides the obvious issues of decorum, there are two main reasons why asserting +Yesod's superiority is not an appropriate way to respond to questions:

  1. Aggressively defending Yesod doesn't make us look any better.
  2. Yesod is not superior to Snap.

Yes, read that second point again. I'll repeat it: Yesod is not superior to Snap. +They are different. Do I prefer Yesod? Absolutely. If not, I wouldn't +be working on it. Are there people out there who prefer Snap? Yes. But +certainly they simply haven't yet been englightened as to the superiority of +Yesod, right? No. See point 2 again.

Yesod provides a high level interface for web development based on simple DSLs +for routing and persistence, an indentation-sensitive templating system, and +pervasive type safety. I'm less of an expert on Snap, so this could be wrong, +but Snap provides a powerful combinator approach to routing, a logic-free +system for templating (Heist), and (to my knowledge) doesn't get in the +business of persistence. They are superficially similar, but fundamentally +different.

I happen to have a lot of thoughts- which I haven't really shared with anyone +yet- on bridging the gap between the two approaches. Heist is in no way baked +into Snap, and could easily be used with Yesod. Similarly, Persistent (or +Happstack's acid-state) could be paired up with Snap. Yesod provides its own +combinator based form solution, but I've been curious to see what we could do +with digestive-functors, which (at least to my eyes) is the de facto forms +approach for Snap. Going even more low-level, Snap's combinator based +approach to routing could be used with Yesod. (To clarify: we can make Yesod +applications without any Template Haskell or Quasi-Quotations. I've done proofs +of concept before, and think this could be something interesting to discuss +later.)

But ideally, I could see integration run even deeper. There is absolutely no +technical reason we need two separate web servers. I've been pushing for +adoption of WAI by other frameworks for a long time. It's currently being used +by other frameworks like Webwire and Scotty, and by standalone applications +like Hoogle, but I would like Snap and Happstack to come to the table to +discuss the possibility of a truly universal interface. Can we achieve it +tomorrow? Probably not. But we should identify the things which are preventing +it.

These are all wonderful long-term goals. But in the short term, there's a lot +we can do to work together on a non-technical level. I think we all agree that +Haskell is a wonderful language, and would like to see its more mainstream +adoption (you know, fail to avoid success at all costs). Any bickering we have +(for which I take responsibility) only weakens Haskell's appeal. Which is +really a complete shame: the community is one of Haskell's strongest assets. +I've never worked with such an intelligent, cooperative, helpful, and friendly +group of people before.

Instead of ignoring what the other groups are doing, it's time to coordinate. +Newcomers are confused about which framework to start with? Instead of touting +our own virtues on Reddit and Stack Overflow, we should have a single Wiki page +that gives fair, unbiased, and comprehensive descriptions of the distinctions +amongst the frameworks. Someone loves Hamlet but wants to use acid-state, +while using Snap's routing? They should be able to ask us about it on +web-devel, and we'll talk it out and come up with a solid example of how it's +done.

It might seem like Snap and Yesod are sworn enemies who are devoted to each +other's destruction. But that's not the case. I've personally had very friendly +interactions with each and every main developer of Snap, and I both like and +respect them. I hope I've been able to inspire the same. (The same goes for +Happstack by the way, that's just not my focus right now.) There are no deep +rifts, blood feuds, or grudges amongst us, just some surface level fighting.

I say it's time to present a unified front. We need to continue the arguments +to help improve all the frameworks, but keep them professional. We're not here +to tear each other down, but to build all of us up. I hope I'll be able to help +repair any damage that I've done in the past, and hope all teams will join with +me now.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/04/yesod-js-todo.html b/public/blog/2012/04/yesod-js-todo.html new file mode 100644 index 00000000..99d29456 --- /dev/null +++ b/public/blog/2012/04/yesod-js-todo.html @@ -0,0 +1,106 @@ + More Client Side Yesod: todo sample +

More Client Side Yesod: todo sample

April 23, 2012

GravatarBy Michael Snoyman

Following up on our previous post outlining a possible client-side approach for Yesod, I wanted to follow up with a slightly more concrete example. It seems the rage today is to provide a todo list example, so here's mine. All of the code is available from the yesod-js Github repo.

This is all still very much experimental, and no one has made any decisions yet regarding whether we'll be pursuing this approach (or any others). The discussion is still completely open, I'm just adding some more fuel to the fire.


Let's put together a simple example app that will keep a server-side list of todo items, and allow the client to update them. First, our language extensions and imports.

{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell, KindSignatures,
+             TypeFamilies, FlexibleContexts, GADTs, MultiParamTypeClasses,
+             FlexibleInstances, TypeSynonymInstances
+  #-}
+import Yesod.Core
+import Yesod.Persist
+import Data.Text (Text)
+import Database.Persist.Sqlite
+import Network.Wai.Handler.Warp (run)
+import Yesod.Json
+import Yesod.Form.Jquery (YesodJquery)
+import Network.HTTP.Types (status201, status204)
+import Yesod.Javascript
+import Data.Aeson hiding (object)
+import Control.Applicative ((<$>), (<*>))
+import Text.Lucius (lucius)

We'll store our data in Persistent, and set up some serialization with JSON. This is all pretty boilerplate.

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Todo
+    text Text
+    done Bool
+|]
+
+instance ToJSON (Entity Todo) where
+    toJSON (Entity tid (Todo text done)) = object
+        [ "id" .= tid
+        , "text" .= text
+        , "done" .= done
+        ]
+instance FromJSON Todo where
+    parseJSON (Object o) = Todo
+        <$> o .: "text"
+        <*> o .: "done"
+    parseJSON _ = fail "Invalid todo"

Now our app. We'll have a pretty standard REST interface, allowing GET, PUT and DELETE.

data App = App ConnectionPool
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/todo TodosR GET PUT
+/todo/#TodoId TodoR GET DELETE
+|]

And a bit more boilerplate.

instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlPersist
+    runDB f = do
+        App pool <- getYesod
+        runSqlPool f pool
+instance YesodJquery App

Let's implement our RESTful interface. This is all standard Yesod stuff, nothing client-side specific. However, we're not providing any HTML representations for now. That could be easily changed so we could have a more robot-friendly site, but that's not our purpose here.

getTodosR :: Handler RepJson
+getTodosR =
+    runDB (selectList [] []) >>= jsonToRepJson . asTodoEntities
+  where
+    asTodoEntities :: [Entity Todo] -> [Entity Todo]
+    asTodoEntities = id
+
+putTodosR :: Handler ()
+putTodosR = do
+    todo <- parseJsonBody_
+    tid <- runDB $ insert todo
+    sendResponseCreated $ TodoR tid
+
+getTodoR :: TodoId -> Handler RepJson
+getTodoR tid = runDB (get404 tid) >>= jsonToRepJson . Entity tid
+
+deleteTodoR :: TodoId -> Handler ()
+deleteTodoR tid = do
+    runDB (delete tid)
+    sendResponseStatus status204 ()

Now we'll start our client side stuff. All client-side values get wrapped in JSValue, with a phantom type to indicate the actual datatype. Let's generate some getters to access the fields of a todo item.

jsTodoText :: JSValue (Entity Todo) -> JSValue Text
+jsTodoText = jsGetter "text"
+
+jsTodoDone :: JSValue (Entity Todo) -> JSValue Bool
+jsTodoDone = jsGetter "done"

We'll also set up a constructor to generate a new todo item. We have to use jsCast to erase the phantom types of the individual values.

jsTodo :: JSValue Text -> JSValue Bool -> JSValue Todo
+jsTodo text done = jsonObject
+  [ ("text", jsCast text)
+  , ("done", jsCast done)
+  ]

In theory, this kind of code could automatically be generated as part of Persistent (along with the ToJSON/FromJSON instance generation, which Persistent can already handle). One thing to note is that the underlying jsGetter function is not typesafe. By giving explicit signatures here, we're adding back type safety.

Next, we'll implement our homepage. Our interface doesn't (yet) allow you to mark items as done, but we provide the CSS class anyway.

getHomeR :: Handler RepHtml
+getHomeR = defaultLayout $ do
+    toWidget [lucius|
+.done {
+    color: #999;
+}
+|]
+    runJS $ do

Let's pull in the todo items via Ajax. ajaxJson takes a type-safe route and returns two values: the data pulled from the server, and the name of the function that can be used to reload the data. We'll use that later, when we add new items.

        (todos, reload) <- ajaxJson TodosR

Now we'll format our todo items into HTML. There are a number of functions here worth mentioning:

  • htmlOutput will take a JSValue Html and produce a Widget.
  • wrapTag will wrap a JSValue Html with an extra tag. wrapTagClass does the same, but will also apply a class-attribute.
  • jsjoin will concatenate a JSValue [Html] into a JSValue Html. This is performed client side with the Javascript join function.
  • jsfor maps over a list of items. Under the surface, it uses Underscore.js's map function.
  • jsif is essentially Javascript's ternary operator ?:.
  • jsToHtml turns text into HTML. Notice that we're getting the same XSS-protection that Yesod is known for, even at the client side.

I'm not sure how I feel about this block, but it certainly works. The end result is a simple unordered list containing all of the TODO items.

        list <- htmlOutput $ wrapTag "ul" $ jsjoin $ jsfor todos $ \todo ->
+            wrapTagClass "li"
+                (jsif (jsTodoDone todo) "done" "notdone")
+                $ jsToHtml $ jsTodoText todo

We want to display how many items we have total, so we use jslength and jsShowInt to get a textual representation of the item count, and then get a widget with textOutput.

        countWidget <- textOutput $ jsShowInt $ jslength todos

Now we start on the interface for adding new tasks. We'll use textInput to get an input widget, and the JSValue being input.

        (taskWidget, taskValue) <- textInput

Now we'll set up a Javascript function body which will use Ajax to submit the new todo item. Notice that we don't have to explicitly pull the value from the input field; using taskValue automatically gets the latest value for us.

Our last parameter is the reload function we got earlier. This says that, each time we submit the form, we want the data to be reloaded from the server. We could theoretically do a local update here instead, but for future features it will be important to have the server-generated ID available.

        putter <- putJson TodosR (jsTodo taskValue jsFalse) reload

And now we create a submit button, which will call putter whenever it's clicked.

        submitWidget <- button [whamlet|Add Item|] putter

Finally, we just piece it all together:

        lift [whamlet|
+<h2>Item count: ^{countWidget}
+^{list}
+<form>
+    Enter new task: #
+    ^{taskWidget}
+    ^{submitWidget}
+|]

And more boilerplate...

main :: IO ()
+main = do
+    pool <- createSqlitePool "todo.db3" 5
+    runSqlPool (runMigration migrateAll) pool
+    toWaiApp (App pool) >>= run 3000

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/05/keter-app-deployment.html b/public/blog/2012/05/keter-app-deployment.html new file mode 100644 index 00000000..27efedbc --- /dev/null +++ b/public/blog/2012/05/keter-app-deployment.html @@ -0,0 +1,74 @@ + Keter: Web App Deployment +

Keter: Web App Deployment

May 11, 2012

GravatarBy Michael Snoyman

tl;dr: There's an experimental deployment system available for testing.

I +mentioned +on the Yesod and web-devel mailing lists last week that I was unsatisfied with +the existing Yesod deployment scripts. I worked on the problem a bit this week, +and now have some code worth testing. For now, this system is called Keter, +though I expect that to change once someone gives me a better idea.

If you're wondering, Keter is Hebrew for "crown." (You should all be very proud +of me; it took a lot of self restraint not to call this post "Yesod's crowning +achievement.") Those familiar with +kabalah might notice a +connection between Yesod and Keter. But I digress.

Advantages

This system already provides quite a bit of convenience for users:

  • Allows updating your app with zero downtime.
  • Checks that new versions of your app actually work before switching to them.
  • Automatically configures PostgreSQL databases as necessary.
  • Monitors processes and restarts them if they crash.

Note: As pointed out on Reddit, +this tool by itself is not sufficient to guarantee you'll never have downtime +with deployments. One issue is database migrations, which can still cause +downtime. My point is that with this approach, as opposed to the previous +deployment scripts, it is possibility to have zero downtime between versions.

Usage

Keter is a single executable. The program takes a single argument: a folder +from which to run. This folder will have an incoming subfolder, where you'll +copy in your keter bundles (more on this in a moment).

Keter relies on a preinstalled Nginx to act as a reverse proxy. It will write +out an nginx config file (for now, hardcoded to +/etc/nginx/sites-enabled/keter) and then tell Nginx to reload (via +/etc/init.d/nginx reload) on any changes.

It also requires a running PostgreSQL server. It will create new accounts and +databases as necessary via sudo -u postgres psql. Again, the exact approach +used is currently hard-coded, but will be made more extensible going forward.

Keter Bundles

In order to have Keter manage an app, you must package it up in a keter bundle. +This is simply a GZIPed tar file with a .keter extension. There is only one +required file in this bundle: config/keter.yaml. Let's look at a sample +keter.yaml file for a scaffolded Yesod site:

exec: ../dist/build/pgtest/pgtest
+args:
+    - production
+host: pgtest
+postgres: true

exec gives the location of the executable, relative to the config file +itself. args is the list of command line arguments. host gives the hostname +that should be bound to, and postgres indicates whether or not a PostgreSQL +database should be created. If postgres is true, then a randomly named +database will be created, and the information will be passed to your app via +environment variables. Persistent is already configured to accept these +variables, so no additional work is necessary on your part.

You might be concerned about that host value. Doesn't it violate DRY to have to +specify the host in keter.yaml, and then specify the approot in +settings.yml? The answer is yes. That's why updating approot is not +required. Keter will set an APPROOT environment variable automatically, based +on the host value, and Yesod will automatically use that to override the +value in settings.yml.

In other words: that 5-line Yaml file is just about all you need to do to +configure your app for deployment. Making a bundle is equally easy, e.g.:

rm -f pgtest.keter
+tar zcfv pgtest.keter dist/build/pgtest/pgtest static config

Once Keter is more fully developed and ready to be used, we'll set up the +scaffolding to include the keter.yaml file, and add a command like yesod +keter to create these bundles automatically.

How it Works

The projects README file actually gives a +pretty thorough breakdown of the components involved in the code. (Note: as +mentioned below, logging has not yet been implemented.) One thing to note is +that old versions of apps will be automatically terminated (via SIGTERM) and +their folders deleted when a new version is available. This has two important +ramifications:

  1. If you want to handle long-running connections, you need to catch SIGTERM and let your app continue to run.
  2. You can't put anything important in your local folder. If you need to persist data, either write it elsewhere in the filesystem, or put it in your database.

As for the code itself, it makes heavy use of message passing. Each component +(Nginx manager, process watcher, Postgres configuration) is handled by a +separate thread, and messages are passed over Chans. I think this actually +makes the code fairly easy to work with, though input from others is greatly +appreciated. (This is really my first time making such heavy use of this +style.) There isn't much in the way of code comments yet, but that will be +fixed going forward as well.

TODO

I still intend to add a comprehensive logging framework. However, I consider +myself to be quite a n00b at proper log file maintenance. If anyone wants to +brainstorm on the best approach here, I'd appreciate it. Once the log framework +is in place, I intend to stick a web frontend on the whole thing to give you +access to app logs, in addition to status of apps and possibly more statistics. +Input here is welcome as well.

There are also likely plenty of ways to clean up the code. If anyone has ideas, +please let me know and/or send a pull request.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/05/keter-its-alive.html b/public/blog/2012/05/keter-its-alive.html new file mode 100644 index 00000000..f613953c --- /dev/null +++ b/public/blog/2012/05/keter-its-alive.html @@ -0,0 +1,102 @@ + Keter: It's Alive! +

Keter: It's Alive!

May 17, 2012

GravatarBy Michael Snoyman

Take a good look at this site. Do you see something different? Well, to quote +Captain Jack Sparrow, it's something not there to be noticed.

This site is now being deployed via Keter. +In other words, it's alive!!! Keter is monitoring the yesodweb.com +application process, handling logs and log rotation, configuring Nginx as +necessary, and automatically deploying code updates. (If yesodweb.com used a +database, Keter would handle that too.)

I've always believed heavily in dogfooding, which is why I'm rolling out Keter +now. However, this should still be considered alpha quality code. I'll be +keeping a close watch on the server to make sure nothing funny goes on. +However, I think the code base is at the point where it makes sense to start +some more serious testing.

I'm going to avoid going into the implementation details in this blog post, and +instead focus on the end user. If people are interested in a post on the +technical aspects of Keter itself, let me know and I'll try to write one.

Prepare the Server

You'll need to install two things on the server: Nginx and PostgreSQL. To do so +on a Debian based system, just run:

sudo apt-get install nginx postgresql

Build the Executable

Since Keter is just another package on Hackage, you can build it with a +simple cabal install keter. However, doing so on your server would entail installing an +entire Haskell toolchain, which is not something we generally +encourage. So for now, the recommended approach would be to do the cabal install +on a local system with the same architecture and distribution as your +web server, and then copy the executable to the server. If you happen to be +running Ubuntu 12.04 64-bit, I've made a +copy of my executable available online.

Now, a few caveats:

  • This code will only work on Linux for now, since it uses inotify for checking for file system changes. I'm hoping that once our Google Summer of Code project gets underway, I'll be able to leverage the cross platform file watching code Luite has been working on and which Mark will hopefully be releasing.
  • There are a few assumptions built into the executable, like where to write nginx config files (/etc/nginx/sites-enabled/keter) and the name of the user for running PostgreSQL admin activities (postgres). This seems to work perfectly on Debian-based systems, but may not translate well elsewhere. If people are using other distributions on their servers and want to add support to Keter, please be in touch.

Once you have the executable, place it in /usr/bin on your server. (Of +course, you can choose a different location, just keep in mind your changes as +you continue.)

Run on Startup

Next you'll want to make sure that keter is run on startup. This will depend +again on your distribution. I'm going to assume using Ubuntu and Upstart. The +following script works for me:

# /etc/init/keter.conf
+start on (net-device-up and local-filesystems and runlevel [2345])
+stop on runlevel [016]
+respawn
+
+console none
+
+exec /usr/bin/keter /opt/keter

Notice that keter takes a single argument: the folder to use as its base of +operations. You can use whatever folder you want, but /opt/keter seems like a +good choice to me.

You can start running Keter immediately with sudo start keter.

Bundle Your App

An application is sent to Keter as a keter bundle. This is a single GZIPed +tarball with a .keter filename extension, and it contains your executable, +resources, and your keter config file. This last file must be placed at +config/keter.yaml. Let's set up a simple example. Suppose you have the +following inanely boring web application:

-- hello.hs
+{-# LANGUAGE OverloadedStrings #-}
+import System.Environment (getEnv)
+import Network.Wai
+import Network.Wai.Handler.Warp
+import Network.HTTP.Types
+import Control.Monad.IO.Class
+import System.IO (hFlush, stdout)
+
+main :: IO ()
+main = do
+    putStrLn "Application is starting"
+    liftIO $ hFlush stdout
+    portS <- getEnv "PORT"
+    let port = read portS
+    run port $ \req -> do
+        liftIO $ putStrLn $ "Received a request at: " ++ show (pathInfo req)
+        liftIO $ hFlush stdout
+        return $ responseLBS status200 [("content-type", "text/plain")] "Hello World!"

There's one Keter-specific aspect to this code to mention: reading the PORT +environment variable. This is how an app is told where to listen for requests. +Nginx will work as a front proxy and receive all requests from clients, and +then pass them off to the appropriate web app based on hostname. This is the +only change necessary to get an app to work with Keter. All Yesod scaffolded +sites support this functionality already, and it should be trivial to add to +Snap and Happstack applications too.

OK, now that we have our app, let's look at our keter.yaml file:

# config/keter.yaml
+exec: ../hello
+host: www.example.com

This is pretty simple. exec says where to find the executable. This will be +relative to the keter.yaml file, which is inside the config folder, so we +use ../hello. The host setting says which hostname to listen on. As a side note, the +hostname will also be provided to your app via the APPROOT environment +variable, which is how Yesod apps generate absolute URLs. This can be safely +ignored: Yesod handles this automatically, and the data likely isn't necessary +for other frameworks.

There are two other options you can specify in your keter.yaml file:

  • args is a list of command line arguments to pass to your application.
  • postgres is a boolean indicating whether a PostgreSQL database should be created for this app. If true, then the database parameters will be provided via environment variables. (Persistent can parse these automatically.)

So a basic config file for a Yesod site called myawesomeapp running on www.myawesomeapp.com and using PostgreSQL would be:

exec: ../dist/build/myawesomeapp/myawesomeapp
+args:
+    - production
+host: www.myawesomeapp.com
+postgres: true

Coming back to our simple hello world app, here's a shell script that can +bundle up your executable and config file:

#!/bin/bash -ex
+ghc --make -threaded hello.hs
+strip hello
+rm -f hello.keter
+tar czfv hello.keter hello config/keter.yaml

This will produce a hello.keter bundle which is all ready to be deployed.

Deploying

This is the (relatively) easy part. Copy your bundle to /opt/keter/incoming. +That should be all there is to it. Now the caveats:

  • You may be missing libraries or other resources on the server, which can cause your app not to run. If after copying the bundle to the server your app doesn't start, try running the executable file itself manually.
  • If it takes your app more than 60 seconds to start answering HTTP requests, Keter will determine that it's unresponsive and shut it down.
  • The folder provided for your app should not be considered a good place for permanent storage, as Keter will wipe it out every time you redeploy your app. If you need permanent storage, than either (1) store it elsewhere in the filesystem, or (2) put it in a database/S3/whatever. The second option is definitely recommended in general, as it will make it much easier to migrate to a different server later.

Oh, and obviously you'll need to set up your DNS to point to your server. Maybe +we'll provide some ability to automate this via Amazon Route 53 in the future.

Logs

Keter keeps fairly detailed logs of its own activities, plus logs all stdout and stderr output from each application. Logs each go into their own folder, and are automatically rotated. To see what's been happening with Keter, look in /opt/keter/log/keter/current.log. For example, the log on yesodweb.com contains:

2012-05-17 11:16:35.08: Unpacking bundle '/opt/keter/incoming/yesodweb.keter' into folder: /opt/keter/temp/yesodweb-0
+2012-05-17 11:16:35.29: Created process: config/../dist/build/yesodweb/yesodweb

Each app log gets placed in /opt/keter/log/app-<appname>, with error output +going in the err subfolder. Keter does nothing to modify the contents of +these files, it simply pipes directly from the app to the file.

What's Next?

The main thing now is testing: making sure it's working correctly under all +circumstances. Keter will give fairly detailed diagnositcs about itself, and +logs every single exception that gets thrown. This should make it easier to +track down any problems. I've run a fair amount of testing myself, and +everything seems to be in order, but obviously we won't know that for certain +until more reports come in.

There are also some features I'd like to add:

  • We want to automate deployment of this as much as possible, most likely by getting distribution packages. In Ubuntu, for example, I want to set up a PPA. Unfortunately, I have no experience with PPAs or Debian packages at all, and would really appreciate help on this.
  • Similarly, for even easier deployment, I'd like to provide an AMI (Amazon Machine Image) so that setting up a new host is basically a one-click process.
  • I originally said that I wanted Keter to provide a web interface for viewing status and log files. I've changed my mind on this a bit: this shouldn't be built into Keter, but instead be provided as a separate app that can be added to Keter. This will also allow people to customize things easily, and for us to have different options (maybe someone wants a Happstack-based app?). If anyone's interested in writing this app, let me know, it shouldn't be too difficult.
  • Integration with some kind of build server. Goal: push to Github, code is built and deployed automatically. I think I was unclear on what I wanted last time I stated this: the idea is not to replace Cabal. Cabal is a wonderful piece of architecture, and is the de-facto standard for building Haskell code (for good reason). My goal is to have something that sits on top of Cabal.
  • Improvements to Yesod for log files. This isn't actually a change in Keter at all, but one which Keter has made me aware of. When we set up the new logging system in Yesod, we turned off buffer flushing for performance reasons. I was a bit uneasy about it at the time, but I think at this point it's pretty obviously the wrong approach, as it prevents the log files from being written in any meaningful way (especially when deploying a new app). I've worked around this in yesodweb.com by flushing the log every second, but that's just a temporary workaround; we need to get a better solution in the libraries themselves. I also think we need to improve the log format itself, likely by moving to moving to Apache style logging.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/05/next-conduit-changes.html b/public/blog/2012/05/next-conduit-changes.html new file mode 100644 index 00000000..e9a04e75 --- /dev/null +++ b/public/blog/2012/05/next-conduit-changes.html @@ -0,0 +1,87 @@ + Next set of conduit changes +

Next set of conduit changes

May 29, 2012

GravatarBy Michael Snoyman

In my previous blog post, I mentioned that I had more conduit changes planned. This blog post is intended to discuss those changes. You can see the progress in the conduit05 branch on Github.

This thought process got kicked off by a question from Hiromi Ishii about usage of yield and await. But as long as we're playing around with the library, I think it's a good time to get out a few other ideas.

As a tl;dr: these changes should make it easier to write conduit code without touching library internals and statically guarantee a few minor invariants. Very little code in the wild should break with these changes, and fixing that code should be very mechanical. And since conduit 0.4 is already stable and working, I see no need to rush this change out, so it would be great to have a longer discussion period.

Step 1: Housecleaning

There are a few simple changes I've wanted to make for a bit, and have been discussing offline with some people (Mathias Svensson in particular).

  • We discussed the idea of replacing usage of Void with foralls, which would in theory make functions with type Sink automatically generalize to any Pipe. In turn, this would make it much easier to combine Sinks into Conduits. After playing with this a bit, I think that the type signatures and language extensions necessary are too difficult to work with. Instead, we'll generalize the types of the functions exported by conduit, so that- for example- mapM_ is now Pipe a o m () instead of Sink a m ().
  • The second field in the NeedInput constructor is intended as the "no more input" field. Initially, its input type was Void, which prevented users from providing it input after finalization began. For simplicity of usage, this was changed just before the 0.4 release to i, which weakens static guarantees. In retrospect, that was a mistake, and we'll move back towards Void.
  • This isn't an API change, but Mathias and I discussed ways to make the codebase easier to follow. This comes down to some basic refactorings and standardized namings. That will probably be part of this upgrade as well (some of it has already started).

Step 2: Leftovers

Just before the 0.4 release, I raised a question in a blog post about having a separate Leftover constructor. To quote myself:

There's a bit of an inconsistency, in that the Done constructor performs two actions: it returns a result, and gives back any leftover input. Also confusing is that we can only have 0 or 1 leftover values.

I felt more comfortable at the time sticking the leftover values in the Done constructor, as that's pretty close to how enumerator works, and after all, enumerator is a great library and does lots of things really well. But the question's been nagging at me for a while since then. So I think it's worth taking the plunge and moving over to a separate Leftover constructor. This comes down to two simple changes:

  • Add a constructor Leftover (Pipe i o m r) i
  • Change the Done constructor from Done (Maybe i) r to Done r.

This actually simplifies a lot of the code, but I already knew that from previous experiments. There are really two things pushing me over the edge this time:

  • I've had time to actually analyze this in depth, and I believe that this does not lead to any data loss issues.
  • It lets us get rid of one of our invariants (to some extent): you can now return leftovers without consuming input. (Yes, that solves Gabriel's complaint, I even used his code as the test case.) I still strongly recommend not doing this: it's been well known in the enumerator world for a while now that doing so is a bad idea.

I'm very much interested in opinions on this, but it seems like the right move to me.

Step 3: Better await/yield

I apologize for making this the end of the post, after I teased you all with promises of a solution to Hiromi's problem. Let me start by framing the issue. The following code, though it seems intuitively correct, enters an endless loop:

sourceTChanYield :: MonadIO m => TChan a -> Source m a
+sourceTChanYield ch = forever $ do
+    ans <- liftIO . atomically $ readTChan ch
+    yield ans

The reason for this is that you've provided no escape route for the code. Actually, due to the usage of forever here, some people might be saying, "Well, obviously it loops forever, what would you expect?" The answer is that the await/yield approach comes with a loaded implication from the pipes world, which has a subtle yet very important distinction from conduit:

Just because one part of the pipeline goes down, does not mean the rest of the pipeline goes down.

I'm beginning to think this is the core distinction between these two packages, and actually has led to a very deep difference in understanding when discussing this topic. I don't want to get into that now, since it's not really our focus, but would like to continue that discussion another time. As you'll see in the rest of this post, I think both approaches have their place. (And I think pipes 2.0 has come to that same conclusion with its Frame concept.)

So let's start with the question: why shouldn't taking down one piece of the pipeline take down everything else? A simple- and incomplete- answer is that it wouldn't give us a chance to perform resource finalization. More generally, it won't allow us to perform any kind of operations after one piece of the pipeline completes. Imagine trying to implement consume where, if you check if there's anything left in the stream, your code stops running.

But this behavior is exactly what we're looking for in sourceTChanYield above: I want to automatically stop running as soon as no one wants any more output from me. I thought about how to implement this over the weekend, and came up with two changes to the Pipe datatype:

  • Since we can't perform any resource finalization, remove all finalization concepts from the constructors.
  • Instead of providing a second field for "no more input" in the NeedInput constructor, have the pipeline shut down if no input is available. A side effect of this is that await would now have the type Pipe i o m i instead of Pipe i o m (Maybe i).

I thought that these changes looked very familiar. Once I got back home this week, I went ahead and had a look at Pipe type from pipes 1.0, and sure enough, it's isomorphic to what I came up with. Now we obviously can't just drop conduit's type for pipes 1.0's solution: the latter won't support many of our needs, including resource finalization and connect-and-resume. However, it is a very convenient approach for implementing a number of functions, including sourceTChanYield.

So here's the idea: we'll provide SPipe as a simplified version of Pipe, which shuts down automatically and doesn't deal with resource allocation. yield and await will work in SPipe instead of Pipe, and the function toPipe will convert an SPipe to a Pipe. With all that in place, sourceTChanYield can now be implemented as:

sourceTChanYield :: MonadIO m => TChan a -> Source m a
+sourceTChanYield ch = toPipe $ forever $ do
+    ans <- liftIO . atomically $ readTChan ch
+    yield ans

And we'll provide await' and yield' as well, which will be the same as the current non-apostrophed functions. This should mean a simple upgrade process, but with the compiler letting you know that you can switch to a simpler abstraction if you want.

Step 3a: Adding in finalization

That's all well and good, and will hopefully solve Hiromi's issue. But it's still not a completely general solution, meaning we'll still need to go back to the original constructors and write everything out the long way in order to implement something like sourceFile. Or do we?

We can really break down sourceFile into three steps:

  • Open a file handle and register a cleanup function to close the handle in case of exceptions. (This is where resourcet comes into play.)
  • Loop over the contents of the file.
  • Close the file handle explicitly as soon as the "inner loop" completes.

This actually sounds a lot like bracket, doesn't it? So I present to you the horribly named (bikeshedding welcome) bracketSPipe:

bracketSPipe :: MonadResource m
+             => IO a                   -- ^ allocation
+             -> (a -> IO ())           -- ^ release
+             -> (a -> SPipe i o m ())  -- ^ inner loop
+             -> Pipe i o m ()

Using this, we can easily implement our (horribly inefficient) Char-based file pipes:

sourceFile :: MonadResource m => FilePath -> Source m Char
+sourceFile fp =
+    bracketSPipe
+        (putStrLn "opening source" >> openFile fp ReadMode)
+        (\h -> putStrLn "closing source" >> hClose h)
+        loop
+  where
+    loop handle = do
+        eof <- liftIO $ hIsEOF handle
+        unless eof $ do
+            c <- liftIO $ hGetChar handle
+            liftIO $ putStrLn $ "Read from source: " ++ show c
+            yield c
+            loop handle
+
+sinkFile :: MonadResource m => FilePath -> Sink Char m ()
+sinkFile fp =
+    bracketSPipe
+        (putStrLn "opening sink" >> openFile fp WriteMode)
+        (\h -> putStrLn "closing sink" >> hClose h)
+        (forever . go)
+  where
+    go handle = do
+        c <- await
+        liftIO $ putStrLn $ "Writing to sink: " ++ show c
+        liftIO $ hPutChar handle c
+
+conduitFile :: MonadResource m => FilePath -> Conduit Char m Char
+conduitFile fp =
+    bracketSPipe
+        (putStrLn "opening conduit" >> openFile fp WriteMode)
+        (\h -> putStrLn "closing conduit" >> hClose h)
+        (forever . go)
+  where
+    go handle = do
+        c <- await
+        liftIO $ putStrLn $ "Writing to conduit: " ++ show c
+        liftIO $ hPutChar handle c
+        yield c

And just to make sure everything's working correctly, I've left in some debug output. If we run this test program:

src1 = sourceFile "sfiletest.hs" $= CL.isolate 3
+src2 = CL.sourceList "world"
+
+main = runResourceT $
+    ((src1 $= conduitFile "conduit") >> src2)
+    $$ sinkFile "sink"

We get the output:

opening sink
+opening conduit
+opening source
+Read from source: 'i'
+Writing to conduit: 'i'
+Writing to sink: 'i'
+Read from source: 'm'
+Writing to conduit: 'm'
+Writing to sink: 'm'
+Read from source: 'p'
+Writing to conduit: 'p'
+Writing to sink: 'p'
+closing source
+closing conduit
+Writing to sink: 'w'
+Writing to sink: 'o'
+Writing to sink: 'r'
+Writing to sink: 'l'
+Writing to sink: 'd'
+closing sink

As we would hope, the source and conduit close as early as possible, and the sink closes after its extra input is consumed.

Let the arguments begin!

When we were working on conduit 0.4, we were also trying to get Yesod 1.0 out the door, and so we had a limited discussion period. There is no such pressure this time around. Now's a great time to bring up questions, ideas, and concerns about conduit (as has been happening quite a bit this week).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/05/response-conduit-bugs.html b/public/blog/2012/05/response-conduit-bugs.html new file mode 100644 index 00000000..bc4a95f5 --- /dev/null +++ b/public/blog/2012/05/response-conduit-bugs.html @@ -0,0 +1,18 @@ + Response to "Conduit Bugs" +

Response to "Conduit Bugs"

May 28, 2012

GravatarBy Michael Snoyman

I was actually planning on writing an entirely different blog post today, which +was going to show how some ideas from version 1.0 of pipes could help +simplify some conduit-based code. That post will have to wait a few days, as I +think this one is more important.

Many people in the Haskell community have probably seen Gabriel Gonzalez's conduit bugs blog post. Normally, I would restrict my response to such a post to comments on Reddit, and allow interested readers to find my response there. However, given that the blog post makes such categorical (excuse the pun) claims about the incorrectness of conduit, I feel an obligation to give a more official response.

Firstly, the Reddit discussion is very informative. Of the four general claims made in the post, two of them are completely refuted: the MonadTrans laws are only violated if you hold to a belief that equality means exactly the same constructors, as opposed to moral equivalence. The claim of doubled release calls is not true at all, and I hope Gabriel will update his post to clarify this mistake.

The claim about violation of the category laws is a weak claim. Even the blog post acknowledges that conduit makes no claims to actually follow these laws. Further, it quotes parts of the conduit documentation that states explicitly that the code used to demonstrate the "bug" is invalid conduit code. I'd like to highlight that point, and use it to address the only remaining point from the blog post.

It's not a bug

If the original blog post were renamed "conduit corner cases" or "conduit gotchas" or "unintuitive behavior in conduit," I would not be writing this response. This response is triggered by the claim that conduit is exhibiting buggy behavior. Gabriel is using a very specific- and in my opinion incorrect- definition of the term bug: that the types allow you to write a buggy program.

It is entirely possible to create a buggy program with conduit. I do not deny it. Instead of attempting to encode all invariants in the type system, conduit takes the approach of expressing most invariants in the type system, and relegating others to documentation. The reason for this is twofold: simplicity and performance.

Let's take one of the examples from the original blog post:

residue x = Done (Just x) ()

If you look in the documentation, one of the stated invariants of conduit is:

It is a violation of the Sink invariants to return leftover data when no input has been consumed.

(Note: as I mention in my response on Reddit, there is in fact a documentation bug, since the invariants of the package were accidently removed from the documentation in the move from 0.3 to 0.4. I will correct that in the coming days.)

Could this invariant be encoded in the type system? Yes. In fact, version 0.2 of conduit did so, by having separate Sink and SinkResult types. I debated whether or not it was a good idea to trade static safety for simplicity in this case, and decided that simplicity won out. You can argue with my decision here, but to call this behavior buggy is incorrect.

For performance, let's look at the end of the blog post:

However, this is quite easy for conduits to fix. All you do is remove the finalizer field from the PipeM constructor and have pipeClose ignore PipeM actions completely, only associating finalizers with HaveOutput.

This is actually incorrect even with regards to correctness: removing the finalizer field from PipeM would mean that, for example, sourceFile could not guarantee that the file handle is closed immediately. (If Gabriel would like to show concrete code demonstrate that I'm incorrect, I'd be intersted to see it. I'm always happy to simplify a library if possible.) But having that second field is vital for minimizing work done. If the first field needed to be run even when no more output is requested, sourceFile would always end up reading an extra chunk of data. I would consider this to be buggy behavior.

Putting it all together, I think there's a simple and fundamental difference between pipes and conduit, which leads to the mistaken claims of bugs:

conduit exposes internals of the package to the extent that you can actually produce invalid code. pipes believes in preventing this kind of usage statically.

You can argue that the pipes approach is better, and I'll argue that conduit's approach is better. But let's call a spade a spade, and admit that conduit's approach isn't buggy.

The remaining claim: invalid Monad instance

This is by far the most important claim in the original blog post. It breaks down into two claims. The first claim is easily refuted, since in order to demonstrate the problem, you need to return leftovers without consuming input. As stated above, this is a violation of the invariants of conduit.

(As a side note, there is an off-hand comment in the post about two cases of data loss. The other case is documented behavior, and explained clearly in the appendix on conduits. The problem is inherent to the problem domain, and pipes has only avoided the issue by dropping a very important feature (leftover handling) from the library. It's obviously possible to "solve" problems by removing features, but that's an unacceptable solution.)

The problem here is again one of documentation: an undocumented invariant. This invariant has been discussed on the mailing list, but never actually made it into the Haddocks. Simply stated: conduit finalizers can only clean up resources which they allocated. This is demonstrated in the conduit codebase itself: sourceHandle and sinkHandle do not close their Handles, since they did not allocate them. sourceFile and sinkFile, on the other hand, did allocate their own Handles, and thus must clean them up.

So again, it is possible to construct code with conduit that is incorrect, but you have to violate invariants to do so.

The future of conduit

One of Garbiel's claims is that we could solve these problems by hiding the constructors. You might be surprised to hear me agree with this 100%. I do not believe it is ideal that conduit exposes users to its internals (and in fact, as Garbiel rightly points out, forces usage of these internals to get correct code). At the same time, I believe it was the correct decision for now to expose these internals, as we don't have a clear idea yet of what a higher-level interface would look like. Hiromi Ishii and I recently had a discussion on haskell-cafe discussing some helper functions, and I believe that the future of conduit will in fact see a number of such functions replacing direct usage of the constructors. (I still plan to export the constructors from the .Internal module, however.)

In other words, conduit is a young package. It has rough edges. If you're not careful, you can cut yourself. But the package (to the best of my knowledge) works correctly, as documented. There are ways that we can make the package nicer, and I'm hoping to do so incrementally, with input from the community.

The conduit invariants

This list will soon make it into the conduit package itself. If anyone can think of invariants that I have omitted, please let me know.

  • A value of type Pipe cannot be reused. Some Pipes (e.g., sourceHandle) have implicit state associated with them due to the very nature of the underlying I/O actions.
  • No leftover input may be returned via Done without first consuming input.
  • A finalizer may only finalize a resource previously allocated by the same Pipe. If this invariant is respected, then (barring exceptions) a finalizer will run if and only if the resource was allocated.
  • Not quite an invariant, but: when transforming streams via a Conduit, it is possible to have data loss. Please see the appendix for more information.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/06/attoparsec-position.html b/public/blog/2012/06/attoparsec-position.html new file mode 100644 index 00000000..72201c8b --- /dev/null +++ b/public/blog/2012/06/attoparsec-position.html @@ -0,0 +1,15 @@ + Position information in attoparsec +

Position information in attoparsec

June 12, 2012

GravatarBy Michael Snoyman

I think for many of us, attoparsec has supplanted Parsec for most parsing. Especially with its new(-ish) text support, it's a great option for almost all parsing needs. Except position information.

My motivating example was xml-conduit: this is a library I use at work a lot, and is in fact part of tools that are shipped to many of our clients. It's quite awkward that in the case of parse errors, there's no indication of where in the file the parse error occurred.

Adding position information to attoparsec itself is a non-starter. I don't know the internals myself, but Bryan says that it would introduce too much performance overhead. So the question is: can we get this position information without changing attoparsec? The answer is a qualified yes.

Continuing with xml-conduit: It has an attoparsec parser which parses individual tokens. A token would be something like an element beginning, or a processing instruction, or raw content. Let's take the case of an element beginning. We don't care about the position information of the individual components of the token (e.g., where an attribute starts); we only want to tell the user where the token started, and maybe where it ended.

This drastically simplifies the problem. All I need to do is count up the lines and columns leading up to this token. And to do that, all I need to know are how many lines/columns were consumed by all the previous tokens.

To address this, I've made some modifications to the devel branch of attoparsec-conduit. There is now a function called sinkParserPos which takes an initial position and a parser, and returns the updated position and the parsed result. Internally, it counts up lines and columns from each chunk of data it sends to attoparsec, with special handling for parse completion (we don't want to count unconsumed data).

On top of this, I've built a very simple conduitParserPos. This function will repeatedly apply a parser to an input stream and produce a stream of parsed values, until the input stream is exhausted. However, the output stream contains both the parsed value and the start and end position (aka PositionRange) of the parsed value.

In xml-conduit, I just made changes to leverage conduitParserPos and thread these position values a little bit later into the parse stream. This means we get nice position information for both parse errors (e.g., <foo<) and for non-well-formed XML (e.g., <a><b></a>).

Although I implemented the technique in attoparsec-conduit, there's nothing conduit-specific about it. Anyone can layer this technique on top of attoparsec.

It's not a panacea: we couldn't get nice position information for Hamlet with this approach, for example, since Hamlet parses the entire document as a single entity, not as a stream of tokens. But for those cases where the approach works, it lets you keep using attoparsec, but giving slightly more human-friendly error messages.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/06/complicating-conduit.html b/public/blog/2012/06/complicating-conduit.html new file mode 100644 index 00000000..17c501d0 --- /dev/null +++ b/public/blog/2012/06/complicating-conduit.html @@ -0,0 +1,43 @@ + Complicating conduit? +

Complicating conduit?

June 8, 2012

GravatarBy Michael Snoyman

The main target of this blog post is the existing base of conduit users. As many of you probably know already, there have been lots of discussions about theoretical approaches to this problem domain being discussed (including the new streaming-haskell mailing list). I think we've come up with a lot of very cool ideas. My main question is: are these ideas an improvement to conduit from a user perspective, or just unneeded complication?

I've put up the most recent Haddocks, which include a pretty thorough tutorial on conduit. I'll try to do a decent job explaining the changes here for those already familiar with conduit.

Note: Since conduit 0.4, Source, Sink, and Conduit are all unified into a single type, Pipe. If I mention Pipe, I'm referring to that underlying unifying type, not a type from the pipes package.

The Good

I think some of the changes are "obviously" good, for some value of obvious. This mostly focuses in on the high-level interface exposed to the user. We introduced the await/yield combination in conduit 0.4, which in theory makes it much easier to create new Pipes. However, as mentioned previously, it's not quite as nice as it could be, since conduit 0.4 does not have auto-termination.

Simply stated, auto-termination means that the following would actually work:

mapM_ yield [1..]

In conduit 0.4, this will loop forever, since we've provided no escape route. Earlier, I'd mentioned the idea of creating a separate "terminating pipe" which would allow the above code to work. Felipe pointed out, however, that having two sets of yield functions with different semantics would be confusing, and I agree.

Since then, I played around with the finalization semantics of conduit, inspired by a few ideas from Gabriel. I've put together a new approach (and documented it in the Haddocks), which now allows us get auto-termination. This means that, when you call yield, your Pipe will terminate immediately if the downstream Pipe completes.

Additionally, I've added some high-level functions for handling finalization (e.g., bracketP). So at this point, I believe a high-level interface consisting of await, yield, leftover, and bracketP should cover creation of the vast majority of Pipes. As a result, we can:

  • Move the Pipe constructors to a separate module (Data.Conduit.Internal), and recommend people avoid using them directly.
  • Deprecate usage of the (now antiquated, and quite inefficient) sourceIO, conduitState, etc. functions. They were already going out of fashion, but having such a simple alternative really does them in.

I think these changes are pretty non-controversial. I fully intend to continue exporting the constructors, as I believe there are corner cases that will still need them. But I've rewritten about 10 conduit-based libraries for testing (including warp and http-conduit), and except for one case, the high-level interface described here was sufficient.

The questionable

Here's where I need some input. Over the past few months, pipes, pipes-core, and conduit have been converging on similar designs. conduit adopted the single datatype approach, and pipes and pipes-core both added finalization support. However, there are two major differences still remaining in their current versions: the pipes family allows upstream return results, while conduit does not. I've expressed elsewhere that I don't think that pipes's current approach to upstream results is a good idea, but it was chosen to allow for a Category instance. Personally, I prefer intuitive behavior to adhering to a set of laws. The reason this decision is necessary is because of use cases like:

return 5 >+> idP -- `return 5 =$ idP` in conduit

In the conduit 0.4 world, there's no way to get this to result in a value of 5. In fact, the types themselves won't allow it: the code above would simply not compile, as upstream Pipes must have a return value of (). However, to satisfy the right identity of Category, it must be allowed to work.

The second distinction is leftover support. This is the idea that one Pipe can consume some input, and then give some of it back. A prime example of this would be in ByteStrings processing: you may want to take a ByteString, consume a few bytes from it, and give the rest over to the next Pipes in the monadic chain. For example:

res <- CL.sourceList ["foo", "bar", "baz"] $$ do
+    x <- CB.take 4
+    y <- CB.take 4
+    z <- CB.take 4
+    let toStrict = S.concat . L.toChunks
+    return $ map toStrict [x, y, z]
+print res
+-- output: ["foob","arba","z"]

This is currently not supported by pipes or pipes-core. Again, the Category instance comes into play: imagine the following code (from Paolo Capriotti):

CL.map id =$= s                   ==   s 
+CL.map id =$= (leftover x >> s)   /=   leftover x >> s 

In other words, left identity is lost, because the leftovers from the right-hand side of the fusion operators are discarded. This isn't some unknown failing of conduit: it has been documented for a long time in the Haddocks. The approach in conduit has been that, while discarding leftovers may violate the Category laws, the fact is that leftover support is vital for any practical streaming data library, and so we've included the feature, together with warnings of how it can cause problems.

I believe we now have solutions for both of these differences that let us keep the power of conduit while fulfilling the Category laws. Let me explain them, and their downsides.

Upstream results

This idea came from Chris Smith. The idea is that an upstream Pipe can return a result value which is different than downstream, and then downstream can receive it. Then the identity Pipe would simply return the same result provided by upstream.

In practice, this works by adding a fifth type parameter to Pipe: the upstream result. This actually gives a great parallelism: each Pipe gives an output and result type, which represent the stream of data it will produce, and the final result to indicate that the stream is done. On the flip side, we have the input and upstream result types, which indicates the stream of data it will receive, and the final result that will indicate that the incoming stream is done. (The final type parameter is the underlying monad.)

This means that right identity now works, e.g.:

return 5 >+> idP === return 5

You can actually use that code in the devel branch for conduit 0.5, and it works. It also means that we don't have to play any games with result types as is necessary with pipes and pipes-core currently. So the following would work just fine:

x <- runPipe $ sourceList [1..10] >+> consume
+-- equivalent to: sourceList [1..10] $$ consume
+print $ x == [1..10] -- True

Besides right identity, another advantage to this is the ability to have upstream results. You can see an argument from Paolo for why this is useful. I'm not convinced by that argument (as you can see in the discussion), but it does add an extra feature.

What's really interesting about all of this is that it's actually incredibly close to how conduit works right now. If you restrict the upstream result type to be (), it's the same as conduit 0.4. This is something important to keep in mind for later.

One other addition to the library would be adding in an awaitE function:

awaitE :: Pipe i o u m (Either u i)

This would allow you to get either the next piece of input from upstream, or the upstream result value. We can still provide await for those (common) cases where you don't care about the upstream result:

await :: Pipe i o u m (Maybe i)

So, to break it down simply: the advantages are that we get a right identity and upstream result types are allowed. The downside is that there's an extra type parameter floating around.

Leftovers

Let's look at the simplest Pipe that needs leftovers: peek. It's implemented as:

peek :: Pipe i o u m (Maybe i)
+peek = await >>= maybe
+    (return Nothing)
+    (\i -> leftover i >> return (Just i))

The problem is that if peek is to the right of a fusion operator, that leftover value will be implicitly dropped. For example:

peek >> consume           -- no data loss
+(idP =$= peek) >> consume -- lost the first element

Notice my use of the term "implicit": I don't think anyone is arguing that the data loss itself is a problem (I've discussed the inherent problems of data loss in streaming data many times before). The problem is that there's no indication that it's happening.

One possibility for solving the leftovers issue is to layer it on top of a Pipe type that has no leftover support. However, to my knowledge, no one has come up with a working solution to that yet. More importantly to me: it would be terribly inconvenient to use. We'd need to be constantly converting from our normal Pipe to this LeftoverPipe (or PutbackPipe in pipes-core terminology), and I think it would kill usability.

So instead, I came up with a different solution, which introduces (tada!) another type parameter for leftovers. Here's the idea: we have a type parameter saying which kinds of leftovers are being given back by a certain Pipe. In the case of peek above, it would be the same as the input parameter, e.g.:

data Pipe l i o u m r
+peek :: Pipe i i o u m (Maybe i)

But a Pipe like consume which never calls leftover wouldn't need to constrain the l parameter to be equal to i. Instead, it would look like:

consume :: Pipe l i o u m [i]

And now the trick: the composition operators would only function on Pipes which have a Void leftover type, i.e.:

(>+>) :: Monad m => Pipe Void a b r0 m r1 -> Pipe Void b c r1 m r2 -> Pipe l a c r0 m r2

This means that it's impossible to implicitly lose leftovers through composition, as we're guaranteed by the types to have no leftovers here. We would then have one more function:

injectLeftovers :: Monad m => Pipe i i o u m r -> Pipe l i o u m r

This allows us to explicitly "inject" leftovers back into the Pipe until the Pipe is done consuming input, and if there are any leftovers remaining, they are discarded. So this means we can keep all of our current functionality, but actually get indications from the type system when we're discarding data.

Note that if we constrain the leftover parameter to be identical to input, we get the same behavior as conduit 0.4.

The disadvantages are the fact that we have (another) extra type parameter, and the inconvenience of explicitly injecting the leftovers.

Keeping the old interface

Before you decide if you like this change or not, let me add in one more piece of information. For both added type parameters, I noted that we could get the current behavior of conduit by constraining the type parameters in some way. It turns out that we can keep our old interface almost entirely intact.

type Source    m o = Pipe () () o    () m ()
+type Sink    i m r = Pipe i  i  Void () m r
+type Conduit i m o = Pipe i  i  o    () m ()
+
+($$)  :: Monad m => Source m a    -> Sink a m b    -> m b
+($=)  :: Monad m => Source m a    -> Conduit a m b -> Source m b
+(=$)  :: Monad m => Conduit a m b -> Sink b m c    -> Sink a m c
+(=$=) :: Monad m => Conduit a m b -> Conduit b m c -> Conduit a m c

Yes, you're reading that correctly: all of our main interaction points with the conduit library can remain the same. We have the exact same type parameters to Source, Sink, and Conduit, and the connect and fuse operators do the same thing. Under the surface, these operators are making a call to injectLeftovers, so they retain the implicit leftover discarding of the previous versions.

You might be thinking, "If we have all this extra power under the hood, but we still drive it the same way, isn't this a no-brainer?" Well, there are still two aspects of this change that can affect users:

  1. Error messages. GHC will spit out all six type parameters, in all their glory. This can be a bit confusing.
  2. Writing general code.

That second point is already a bit of an issue, so let me expand. Consider the peek function we described earlier. If we had to express it in one of the above three types, we would need to choose Sink, as it is the only one that allows a non-() result type. So the signature would be:

peek :: Sink i m (Maybe i)

Under the hood, in conduit 0.4, this expands to:

peek :: Pipe i Void m (Maybe i)

Now suppose we're trying to construct a Conduit, and we want to leverage existing functions. So we do something like:

myConduit :: Conduit Foo m Bar
+myConduit = do
+    ...
+    x <- peek
+    ...

It doesn't compile, because the output type for myConduit is Bar, while Sink constrains the output type of peek to Void. To get around this issue, conduit 0.4 provides the sinkToPipe function. But the better solution is to define library functions to use the most general type available whenever possible, not the Source, Sink, or Conduit version.

The problem is exacerbated a bit with these two extra parameters. While it can be annoying to have to work in this general way, I think we have two approaches to mitigating the problem:

  1. Let GHC be your friend. Write your code with the simpler type first, then remove the type signature and ask GHC what its type is.
  2. Providing similar sinkToPipe functions for automatically generalizing types.

To explain the latter:

sourceToPipe  :: Monad m => Source m o    -> Pipe l i o u m ()
+sinkToPipe    :: Monad m => Sink i m r    -> Pipe l i o u m r
+conduitToPipe :: Monad m => Conduit i m o -> Pipe l i o u m ()

So now that you've got the facts, the question is: are these changes worth it?

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/06/conduit-0-5.html b/public/blog/2012/06/conduit-0-5.html new file mode 100644 index 00000000..52b96ae8 --- /dev/null +++ b/public/blog/2012/06/conduit-0-5.html @@ -0,0 +1,130 @@ + conduit 0.5: announcement and example +

conduit 0.5: announcement and example

June 29, 2012

GravatarBy Michael Snoyman

I'm happy to announce release 0.5 of conduit. conduit is a package for dealing with streaming data, making it easy to compose different forms of data production, transformation, and consumption. It fits the same solution space as the enumerator/iteratee paradigm, though it takes a different approach, intended to be easier to understand and use. Unlike lazy I/O, it guarantees prompt resource finalization and does not introduce exceptions into pure code.

This release is notable since it provides a simple, efficient, high-level interface for creating Sources, Sinks, and Conduits. Direct usage of the constructors should almost never be necessary. There are also some more powerful features available just under the hood, such as upstream results and explicit leftover discarding. These changes also allow for more Category-like behavior.

The new package includes a fairly thorough tutorial in the Haddocks themselves. I recommend going through that to start off with. Even if you've been using conduit for a few versions already, this tutorial makes explicit a few details that have never (to my knowledge) been completely clarified previously, such as exactly how finalization works.

One final note: this release only includes conduit and some accompanying packages like attoparsec-conduit and zlib-conduit. It does not include wai, persistent, yesod, and a few other conduit-based packages. Those will be released in time, when they are fully tested and ready for release. Please do not file issues on Github asking for release of those packages; we will do so when the Yesod team decides that they are ready for release.

For the remainder of this post, I'd like to step through some slightly more sophisticated examples of conduit usage, based on a mailing list question about creating a network server.


The network-conduit provides some high level helpers for creating network servers and clients. We'll start off with the same basic shell, and provide a different application for each example. So we'll start with:

import Data.Conduit
+import Data.Conduit.Network
+
+main :: IO ()
+main = runTCPServer (ServerSettings 4000 HostAny) app
+
+app :: Application IO
+app = ...

So what exactly is an Application? It's just a function that takes a source and a sink, and runs them. So let's start off with a really simple server: echo.

app src sink = src $$ sink

All we need to do is connect our Source to our Sink, and conduit handles the rest. Any data received is automatically routed right back to the Sink. Notice that there's no explicit looping, no need to directly handle the intermediate data, and no need to explicitly deal with termination.

But that's boring. Let's say we want to automatically upper-case all of the input before echoing it back. Let's think about what steps need to happen to make that work. Firstly, we need to decode the data from binary to textual data. (We could cheat and just use some functions from Data.ByteString.Char8, but that's neither correct nor nearly as fun.) Then we need to upper case each bit of text, re-encode the data, and send it back out. With a few more imports, that's a piece of cake:

import qualified Data.Conduit.List as CL
+import Data.Conduit.Text
+import Data.Text (toUpper)
+
+app src sink = src
+            $$ decode utf8
+            =$ CL.map toUpper
+            =$ encode utf8
+            =$ sink

Notice how declarative this approach is: each step we outlined becomes another component of our pipeline. Let's try something else: for each chunk we receive, we'll print out the size (in bytes) of the chunk.

import qualified Data.ByteString.Char8 as S8
+app src sink = src
+            $$ CL.map (\bs -> S8.pack $ show (S8.length bs) ++ "\n")
+            =$ sink

At this point, we see a pattern developing: we seem to be keeping the src and sink on the outside and just playing around with the inside of the pipeline. Let's go ahead and abstract out that pattern:

app src sink = src $$ conduit =$ sink
+
+conduit :: Conduit ByteString IO ByteString
+conduit = CL.map (\bs -> S8.pack $ show (S8.length bs) ++ "\n")

Or for our first example:

conduit = decode utf8 =$= CL.map toUpper =$= encode utf8

So that brings up a question: why does network-conduit provide you with a Source and Sink? Can't an Application just be a Conduit? The answer is that providing a Source and Sink separately is strictly more powerful than just using a Conduit. As we'll see later, this can allow us to do some neat tricks with more advanced features like connect-and-resume.

Control flow

So far, our examples have just been infinite pipelines: they keep processing in the same way until the client closes the connection. Let's introduce some control flow: a program that echos everything until it receives the word "quit" as the first four letters in a chunk.

conduit = do
+    mbs <- await
+    case mbs of
+        Nothing -> return ()
+        Just bs
+            | "quit" `S8.isPrefixOf` bs -> return ()
+            | otherwise -> do
+                yield bs
+                conduit -- loop

Instead of an infinite loop, we now explicitly call out to await and yield to read and write data, respectively.

Interleaving other I/O

Let's create a simple file server: you'll send it a filename, and it sends you back the entire contents of the file. To do this, we'll need to slightly modify our program: instead of living in the IO monad, it needs to live in the ResourceT IO monad, to allow for exception safe file access.

import qualified Data.Conduit.Binary as CB
+
+main :: IO ()
+main = runResourceT $ runTCPServer (ServerSettings 4000 HostAny) app
+
+app :: Application (ResourceT IO)
+app src sink = src $$ conduit =$ sink
+
+conduit :: Conduit ByteString (ResourceT IO) ByteString
+conduit = CB.lines =$=
+          awaitForever (CB.sourceFile . S8.unpack . S8.takeWhile (/= '\r'))

I purposely punted here on the issue of filename character encoding; normally I would use system-filepath and the filepath-conduit package, but for simplicity I'm just using the Char8 unpack function. Also, the Data.Conduit.Binary.lines function only strips the newline character (\n), not the carriage return (\r). Since most telnet clients will send both (CRLF), we should manually strip it out.

awaitForever is a nice convenience function that will call the inner function as long as there is more input available. Of course, we can combine our quit approach from above and have manual looping control:

conduit =
+    CB.lines =$= loop
+  where
+    loop = do
+        mbs <- await
+        case mbs of
+            Nothing -> return ()
+            Just bs
+                | "quit" `S8.isPrefixOf` bs -> return ()
+                | otherwise -> do
+                    CB.sourceFile $ S8.unpack $ S8.takeWhile (/= '\r') bs
+                    loop

Client side

network-conduit provides a very similar interface for producing network clients. Let's see a simple example:

{-# LANGUAGE OverloadedStrings #-}
+import Data.Conduit
+import qualified Data.Conduit.List as CL
+import Data.Conduit.Network
+import Data.ByteString.Char8 ()
+
+main :: IO ()
+main = runTCPClient (ClientSettings 4000 "localhost") client
+
+client :: Application IO
+client src sink =
+    src $$ conduit =$ sink
+  where
+    conduit = do
+        yield "hello"
+        await >>= liftIO . print
+
+        yield "world"
+        await >>= liftIO . print
+
+        yield "goodbye"
+        await >>= liftIO . print

Nothing too surprising going on here. The main purpose of this section is to set the stage for the final example.

Proxy server

One of the motivating use cases for conduit in the first place was creating HTTP proxy servers. Previously, with enumerator, most people (myself included) found it too difficult to combine different pieces together to get a working proxy server. (It can be done, using multiple levels of nested Iteratees, but it's a pain.)

So let's go ahead and put together a simple network proxy server. It will work as follows:

  • Client connects.
  • Client sends server port number on a single line.
  • Client sends server hostname on a single line.
  • Proxy connects to server.
  • Proxy sends a "Successful connection" response to client.
  • Forever:
    • Client sends chunk to proxy.
    • Proxy sends same chunk to server.
    • Server sends chunk to proxy.
    • Proxy sends that chunk to client.

Using standard socket-based (or Handle-based) functions, this isn't too difficult: you would just have a bunch of send and recv calls going against two different sockets. The point is that, since your application controls the flow of execution, you can easily interleave different sources. Conduit (and enumerator) introduce a certain inversion of control which makes such interleaving difficult.

So conduit provides an "escape route" to give control flow back to your application. This is called connect-and-resume. While this may sound a bit scary, it's actually not so bad: you connect a source to a sink until the sink is done. Then, instead of just getting back a result, you get a result and a new resumable source. You can then connect-and-resume that resumable source again... and so on.

First, let's look at our main function. We start by listening for a client connection:

main = forkIO $ runTCPServer (ServerSettings 5000 HostAny) proxy

Within proxy, we need to get the port and hostname, and make a connection to the given server. Let's define some helper functions to get a single line, and to get the port/hostname pair:

takeLine = do
+    let linefeed = 10
+    bss <- CB.takeWhile (/= linefeed) =$ CL.consume
+    CB.drop 1 -- drop the newline
+    return $ S8.takeWhile (/= '\r') $ S8.concat bss
+
+getPortHost = do
+    portBS <- takeLine
+    hostBS <- takeLine
+    return $ ClientSettings (read $ S8.unpack portBS) (S8.unpack hostBS)

Next we'll define our proxy function using connect-and-resume (the $$+ operator). We'll connect our source to the getPortHost sink, and then get back the client settings and a new ResumableSource. We'll pass on that ResumableSource for our read loop (proxyLoop):

proxy :: Application IO
+proxy fromClient0 toClient = do
+    (fromClient, clientSettings) <- fromClient0 $$+ getPortHost
+    runTCPClient clientSettings (proxyLoop fromClient toClient)

From proxyLoop, we need to send the successful connection message to the client, get a ResumableSource for reading from the server, and start looping:

proxyLoop fromClient toClient fromServer0 toServer = do
+    yield "Connected to server" $$ toClient
+    -- convert fromServer0 from a normal Source to a ResumableSource
+    (fromServer, ()) <- fromServer0 $$+ return ()
+    loop fromClient fromServer
+  where

The inner loop itself is pretty straight-forward: it follows the four steps from above directly:

    loop fromClient fromServer = do
+        (fromClient', mbs) <- fromClient $$++ await
+        case mbs of
+            Nothing -> close fromClient' fromServer
+            Just bs -> do
+                yield bs $$ toServer
+                (fromServer', mbs) <- fromServer $$++ await
+                case mbs of
+                    Nothing -> do
+                        yield "Server closed connection" $$ toClient
+                        close fromClient' fromServer'
+                    Just bs -> do
+                        yield bs $$ toClient
+                        loop fromClient' fromServer'

There are two tricks here. The first is the $$++ operator. It's the same as the $$+ connect-and-resume operator, but it works on an existing ResumableSource instead. You can think of it as "continue resuming." The second is those calls to close. When you use normal conduit connecting, the Source and Sink are both closed for you automatically. However, with ResumableSources, we need to leave the Source open to be used later. That means that when we're done with them, we need to explicitly close them. Doing so is easy: just use the connect-and-close ($$+-) operator:

    close x y = do
+        x $$+- return ()
+        y $$+- return ()

Connect-and-resume isn't something you'll often need in the world of conduits, but it's incredibly useful for the corner cases when you want it.

Full source

Below is the full source for the server, proxy, and client, in a single Gist for easy fork-ability. I hope this tutorial helped demonstrate the power of conduit, and give a guide on how to use it. If there are any questions, or recommendations for how to clarify any points, please let me know!

By the way, Felipe pointed out that it would be nice to see proxyLoop implemented with threads to avoid deadlocks. I purposely chose the implementation of proxyLoop here to better demonstrate connect-and-resume, but for the curious, here's a threaded implementation:

proxyLoop fromClient0 toClient fromServer0 toServer = do
+    yield "Connected to server" $$ toClient
+    m <- M.newEmptyMVar
+    tid1 <- forkIO $ do
+        fromServer0 $$ toClient
+        M.putMVar m True
+    tid2 <- forkIO $ do
+        fromClient0 $$+- toServer
+        M.putMVar m False
+    x <- M.takeMVar m
+    if x
+        then killThread tid2
+        else killThread tid1

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/06/updated-conduit-docs.html b/public/blog/2012/06/updated-conduit-docs.html new file mode 100644 index 00000000..6ea2fdbf --- /dev/null +++ b/public/blog/2012/06/updated-conduit-docs.html @@ -0,0 +1,20 @@ + Updated conduit docs +

Updated conduit docs

June 1, 2012

GravatarBy Michael Snoyman

Over the past few versions, various portions of conduit documentation has +ended up on this blog, in the Yesod book, in Mezzo Haskell, or the Haddocks +themselves. It's become a bit of a mess.

So following on the excellent example of pipes, I've decided to consolidate a lot of this information into the Haddocks. I've generated the Haddocks and made them available online if anyone wants to have a look.

Note that this documentation reflects the upcoming conduit 0.5. This code has not yet been released to Hackage, as it needs more testing and more feedback. I'm hoping this post will help encourage the latter :).

Garbiel and I have been discussing some possible changes to conduit offline, and the discussion on Reddit on Paolo's blog post has been very informative.

In addition to the changes I mentioned last time, I've put in a few more changes to the conduit codebase (mostly based on Gabriel's advice):

  • The main Data.Conduit module will no longer expose the internals of the Pipe datatype. Instead, a few primitives are exposed, and (hopefully) everything else can be implemented in terms of those. So far, I've been able to rewrite all of the .List, .Text, and .Binary functions using that interface, which is very encouraging.
  • Instead of manually dealing with resource finalization via the constructors, a few combinators (addCleanup and bracketP) should provide all the capabilities necessary.
  • I've relocated most of the "utility" functions (e.g., conduitState, sourceIO) to a separate Data.Conduit.Util module. These functions are less efficient and more clumsy than the primary interface. I'm not (yet) removing these functions, but am considering deprecating them.

tryYield

To deal with the fact that Pipes don't automatically terminate, I've modified +the Pipe yield function (now named tryYield). It now has type:

o -> Pipe i o m r -> Pipe i o m r

The distinction is that, instead of specifying the next +Pipe to run via monadic composition, it is now specified via the second +argument. In other words, we can rewrite:

infinite = yield () >> infinite

as

infinite = tryYield () infinite

The difference is that the first one really won't ever terminate: monadic binding indicates that the following Pipe must always run. With tryYield, it will only be run if the downstream Pipe is still accepting input. I believe this should remove a landmine from the conduit API.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/announcing-baseless-assertion.html b/public/blog/2012/07/announcing-baseless-assertion.html new file mode 100644 index 00000000..e37a3e22 --- /dev/null +++ b/public/blog/2012/07/announcing-baseless-assertion.html @@ -0,0 +1,15 @@ + Announcing baseless-assertion 0.1 +

Announcing baseless-assertion 0.1

July 14, 2012

GravatarBy Michael Snoyman

I'm happy to announce the first release of baseless-assertion. This release is highly experimental: it has not seen any real-world usage yet. I highly encourage all potential users to test it out as soon and as often as possible.

baseless-assertion is a tool to help streamline discussions around Haskell. It is meant to deal with the all-too-prevalent usage of a statement made without any evidence to back it up. baseless-assertion comes in three forms:

  1. A Fry meme:

    Not sure if tapping into a deep universal truth Or just making a baseless assertion

  2. A text version: "Not sure if referencing some universal truth, or just making a baseless assertion."

  3. Textual reference to a non-present image: baseless-assertion.jpg. (Note: baseless-assertion.png is an acceptable usage as well. Please do not use baseless-assertion.gif, as it would imply non-present animation.)

Example Usages

  • Overzealous enumerator/conduit/pipes fan: There's never a use case for lazy I/O, my pet library is always better. baseless-assertion.jpg
  • Overzealous Yesod/Snap/Happstack user: My favorite framework is far better at my pet feature than any other web framework. baseless-assertion.jpg
  • Overzealous Haskeller: The only way to write a program without bugs is with a type system at least as powerful as Haskell's. And any type system more powerful than Haskell's doesn't let you write useful programs. baseless-assertion.jpg

As you can see, baseless-assertion is easy to use, and has a consistent calling convention.

More complicated uses

Not all usages of baseless-assertion are as simple as implied above. In some cases, a comment may contain multiple paragraphs, some of which are a baseless assertion, and some of which are accurate. In such a circumstance, it is recommended that you quote the unfounded statements and apply baseless-assertion individually to each one. In this case, we recommend use pattern (3) from above (textual reference to a non-present image).

Invalid usages

There are many cases where baseless-assertion may seem relevant, but is in fact invalid. Let's provide a few examples:

  • "I believe that Python is the greatest language ever." The only assertion being made is about the speaker's own preferences. Given that we can generally assume the speaker to know his/her own preferences, this is in fact not baseless. More generally, usage of terms such as "in my opinion," "in my experience," or "anecdotally" will disqualify a possible usage of baseless-assertion. (Feel free to continue a normal, healthy discussion, however!)

  • "Haskell has a stronger type system than Javascript." Granted that there is not actually any basis for this assertion in the quote alone, context must be taken into account as well. If this statement was made to a group of people somewhat familiar with the two languages, it is not a baseless assertion per se. If it was stated in the middle of a high stakes poker game, it would qualify as a baseless-assertion, however.

  • "Clearly, Java is the only real programming language today." Since sarcasm was implied, baseless-assertion cannot be used. However, given the lack of intonation in textual discourse, this can be difficult to ascertain.

In general, if you believe you have seen an invalid usage of baseless-assertion, engage the user in dialog to try and clarify the situation.

Enjoy

As mentioned, this release is experimental. It's yet to be proven to actually solve real world problems, but it certainly has a strong theoretical basis. (baseless-assertion.jpg.) I'm looking forward to community feedback on ways to improve this tool, and thereby simplify the process of Haskell discussions.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/announcing-wai-1-3.html b/public/blog/2012/07/announcing-wai-1-3.html new file mode 100644 index 00000000..73b51783 --- /dev/null +++ b/public/blog/2012/07/announcing-wai-1-3.html @@ -0,0 +1,15 @@ + Announcing WAI 1.3 +

Announcing WAI 1.3

July 13, 2012

GravatarBy Michael Snoyman

I'm happy to announce the 1.3 release of WAI and its related packages. WAI is the Web Application Interface for Haskell, providing a generic and efficient interface between web servers and applications. This release includes WAI itself, the Warp web server, and various other utilities and applications.

The main features for this release are:

  • Upgrade to conduit 0.5
  • Warp flushes request body after processing response body. This allows you to process the request body while generating the response. While this can be useful, be warned that this can cause deadlocks with some HTTP clients.
  • simple-sendfile now sends headers in the same system call as the sendfile itself, resulting in much better performance for static file serving.
  • Simplified wai-extra's request body parsing, uses standard conduit types instead of special BackEnd type.
  • Drastically cleaned up wai-app-static (both internals and the user-facing API).
  • warp-tls automatically sniffs the request and determines whether to serve over HTTP or HTTPS.
  • Split off the mime-types package from wai-app-static.

This release will serve as the basis for the upcoming Yesod 1.1 release. As such, if you are a Yesod user, you should not upgrade to this new version of WAI until the 1.1 release of Yesod. We are still maintaining a 1.2 branch, and will backport any bug fixes as necessary.

Please see the chapter in the Yesod book for a general overview of WAI, or check out some of the more prominent packages on Hackage:

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/clarification-classy-prelude.html b/public/blog/2012/07/clarification-classy-prelude.html new file mode 100644 index 00000000..21e8f9a4 --- /dev/null +++ b/public/blog/2012/07/clarification-classy-prelude.html @@ -0,0 +1,100 @@ + Clarification: classy prelude +

Clarification: classy prelude

July 16, 2012

GravatarBy Michael Snoyman

Firstly, I wanted to mention that the markdown engine powering this +blog is now available on Hackage. +I made a small release +announcement. +Basically: it's experimental, and I'd love feedback on how it works for others, +but it should be quite usable as-is.

Anyway, onto the main event.


When I announced classy +prelude, there was quite a +mixed reaction from the community. There was quite a bit of positive feedback, +and lots of people seemed interested. And on the flip side, a number of people +very unequivocally declared it a horrible, horrible idea.

I'm not going to mince words: I think a couple of the detractors are making a +number of baseless +assertions, +attacking strawmen, and engaging in hyperbole. Declaring war on a new package +and swearing to never use it or any of its dependencies to bring about its +eventually demise does not really fit into a normal, healthy discussion. Making +claims about "brittle typeclass extensions" without any clarification is not +helpful. When discussions devolve to such a level, I don't think there's any +point in engaging.

Part of the fault in all of this is that I did not clarify the purpose of the +library well enough in the initial post, instead focusing more on how it works. +I will try to rectify that in this post, and in doing so hope to answer some of +the more well-stated arguments against classy prelude.

I strongly encourage discussion about classy prelude, and certainly want to +hear critiques. But please try to make them based on actual facts with well +reasoned arguments, not just asserting that typeclasses are horrible or that +this library will break all of Haskell's type safety.

Let's start over: what is classy prelude?

  • It is a library which leverages type classes to provide name overloading, +thereby reducing the number of import statements made and decreasing the +syntactic overhead of qualified imports.

    Said another way: it's nothing more than syntactic sugar. There may +certainly be better theoretical approaches to such syntactic sugar (a new +namespace language extension, or a better module system, or maybe something +like TDNR), but none of those actually exist today. In today's Haskell, the +only approach possible to achieve this goal is typeclasses. (If someone knows +something I don't, please say so.)

  • The motivation here is a simple hypothesis: programmers are lazy. Writing:

    "foo" ++ "bar"

    is far easier than:

    import qualified Data.Text as T
    +
    +T.pack "foo" `T.append` T.pack "bar"

    Therefore, people will tend towards the easier path, all else being equal. +The goal is to lower that resistance to the right way to do things, and +therefore encourage better code overall.

  • The purpose of classy prelude is not to encourage writing polymorphic +code based on the typeclasses provided. Though it's certainly possible to +write code such as:

    {-# LANGUAGE NoImplicitPrelude #-}
    +import ClassyPrelude
    +import ClassyPrelude.Classes
    +
    +foo :: (CanLookup t k v, CanInsertVal t k v)
    +    => k
    +    -> v
    +    -> t
    +    -> Maybe v
    +foo x y z = lookup x $ insert x y z

    That's not my intention. Instead, the idea would be to actually nail this down to concrete types, e.g.:

    {-# LANGUAGE NoImplicitPrelude #-}
    +import ClassyPrelude
    +
    +foo :: (Eq k, Hashable k) => k -> v -> LHashMap k v -> Maybe v
    +foo x y z = lookup x $ insert x y z

    Compare that to the equivalent without classy-prelude:

    import Data.HashMap.Lazy (HashMap)
    +import qualified Data.HashMap.Lazy as HashMap
    +import Data.Hashable
    +
    +foo :: (Eq k, Hashable k) => k -> v -> HashMap k v -> Maybe v
    +foo x y z = HashMap.lookup x $ HashMap.insert x y z

    When used this way, the only difference from classy-prelude is syntactic.

  • That said, if people want to try to write polymorphic code with +classy-prelude, I see no problem with trying it out. It's true that there +are no typeclass laws defined for the classes provided, and therefore such +generic code may not work correctly, but it's certainly worth trying it out. +Which brings me to the most important point...

  • classy-prelude above all else is an experiment. It is in no way +intended to be a replacement for the actual prelude (and probably never +will be). I've used it in a few smaller projects at work to remove redundant +code... and that's it. If someone sent me a pull request on one of the Yesod +libraries today which used classy-prelude, I would reject it, because the +library is not ready for prime time.

    Are there questionable choices made right now? Absolutely. Some people have +pointed out that trying to unify a ByteString map and conduit's map +into a single class may be overkill. I completely agree: it may be +overkill. However, I vehemently disagree with anyone claiming that they know +it's wrong. How can you know it's wrong, bad, evil, and kills kittens until +you've actually tried it?

So far, my experience has been that error messages do not get significantly +more confusing, that code tends to just work the way you'd expect it to (since +it's all just syntactic sugar for existing, well-tested and thoroughly +type-safe libraries), and that the code becomes more legible since we've +removed a bunch of unnecessary line noise (i.e., qualified usage of functions).

Are there downsides to this library? Certainly. Is it going to become the +de-facto library used for all Haskell development? (Almost) certainly not. Has +it already proven itself useful in some actual, real world code? Yes. And that +last point is the important one: even if you personally can't see a need for +this, others (myself included) do. Even if you believe that it violates every +principle of Haskell you hold dear, your belief isn't enough to win an +argument. And even if you try to declare a holy crusade against this thing, you +won't kill it. If people find it useful, they'll use it. If not, then it will +die without your help.

What I'm really saying is this: let's bump up the level of interaction here. If +you see a flaw, demonstrate the flaw. If you believe something is wrong, +explain that you think it's wrong, but don't start claiming to have +absolute truth on your side. I'm happy to engage in a healthy conversation +about this library, but I have better things to do with my time then engage in +pointless flame wars.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/classy-prelude.html b/public/blog/2012/07/classy-prelude.html new file mode 100644 index 00000000..fa9d04a8 --- /dev/null +++ b/public/blog/2012/07/classy-prelude.html @@ -0,0 +1,41 @@ + The classy prelude +

The classy prelude

July 12, 2012

GravatarBy Michael Snoyman

tl;dr: Announcing the beginning of classy-prelude, an alternate prelude which makes it easier to use conflicting names via typeclasses, and thereby encourages usage of more powerful libraries like text and unordered-containers. I'll release the code to Hackage, as soon as Hackage comes back up.

A few months ago, Max Cantor braved the treacherous journey into the barren wastelands of northern Israel to pay me a visit /melodrama. We discussed a number of different ideas then, one of which being an alternate prelude.

There have been a number of different alternate preludes attempted before, trying to do such things as fix the numeric type classes or automatically generate Javascript code. Our goal was entirely different.

For me, it's based around a very simple idea: programmers are lazy. Let me give a practical motivating case. Suppose you're on line 247 of the file you're currently editing, and you need to modify some value wrapped in a Functor. You'd ideally like to write f <$> x. But have you imported Control.Applicative yet? If you want to write that "correct" code, you'll need to:

  1. Scroll up to your import list.
  2. Look for Control.Applicative.
  3. If it's not there, type in import Control.Applicative ((<$>)).
  4. Try to find where you were in the code.
  5. Try to remember what you were doing.

I'm sure I'm not the only Haskeller who has simply written fmap f x in this situation, even if we'd prefer the former. The solution to this is pretty simple: just export more common functions and operators. (While we're at it, we'll export common datatypes like ByteString.) But this is a relatively mundane problem. Let's look at some other very common cases:

  • Do you represent textual data with String or Text? The former lets you use ++, concat, and a bunch of other functions in the Prelude. The latter forces you to import qualified Data.Text as T, and then use T.append and T.concat.
  • You need to build up a lookup table. Do you use Data.Map or a simple associated list? This has important ramifications on program correctness, as the associated list approach doesn't promise you the same invariants as Data.Map.
  • If you want to start using conduit (or enumerator), you'll likely end up importing about 3 modules: the core module, one providing list-like primitives, another providing binary functions... and so on.

Solution: type classes

To sum up the problem more succinctly: we have too many name collissions. We deal with this via qualified imports, but this introduces a large number of import statements and tedious usage of module prefixes.

So Max and I came up with a simple solution: just create typeclasses for these common, shared functions, and export them from a modified prelude. This means that your standard 20-odd line set of imports turns into:

{-# LANGUAGE NoImplicitPrelude #-}
+import ClassyPrelude

Wherever possible, we reuse existing typeclasses. For example, ++ is just an alias for Data.Monoid.mappend, and concat for Data.Monoid.mconcat. Since most of our common types (String, Text, ByteString, Map, ...) provide Monoid instances, we immediately get a much more useful operator.

In other cases, such as the length function, no such typeclass exists. For those cases, we define a new typeclass. The implementation is pretty simple in this case:

class CanLength c i | c -> i where
+    length :: c -> i
+instance CanLength [a] Prelude.Int where
+    length = Prelude.length
+instance CanLength ByteString Prelude.Int where
+    length = S.length
+instance CanLength LByteString Int64 where
+    length = L.length
+instance CanLength Text Prelude.Int where
+    length = T.length
+instance CanLength (Map k v) Prelude.Int where
+    length = Map.size
+instance CanLength (Set x) Prelude.Int where
+    length = Set.size

Notice the use of functional dependencies to state the datatype used to represent the length. I considered using type families, but believe that for our purposes here, the brevity of fundeps really pays off. This really shows in giving us relatively short error messages.

Downsides

There are of course some downsides to this approach.

  • As just mentioned: error messages are less helpful.
  • In some cases, explicit type signatures are necessary. For example, length . pack . "hello" is ambiguous, since length and pack could be working on either strict or lazy text, or a string, or possibly something else entirely.
  • Some seemingly simple functions (like map) need a bit more type machinery around them, as depending on the data structure in question, map can be more or less polymorphic.

As a result of these, we think that classy-prelude is a good choice for experienced Haskellers looking to speed up their development time, not for beginners. It's best to get accustomed to the types available monomorphically first.

A harder example

To give an idea of some of the more complicated aspects here, let's take a complicated example: filter. We want to support filter for lists, ByteStrings, Maps, and even conduits. To see the problem, we need to look at the different type signatures:

filter :: (a -> Bool) -> [a] -> [a]
+filter :: (Word8 -> Bool) -> ByteString -> ByteString
+filter :: (a -> Bool) -> Map k a -> Map k a
+filter :: Monad m => (a -> Bool) -> Conduit a m a

To sum up:

  • For lists, Maps, and Conduits, the type of the predicate is polymorphic. For ByteString, it is always Word8.
  • For lists and Conduits, the input type and predicate type are the same. For Map, we have an extra type variable (k).
  • For lists, ByteStrings, and Maps, there are two arguments, while for Conduits, there is only one. Said another way, the first three return a function, while the Conduit case returns a Conduit value.

So how do we represent these four different functions with a single typeclass? Firstly, we break it down into the part which is similar: each function takes a predicate a -> Bool and returns some value. So let's represent that with a typeclass:

class CanFilter f a where
+    filter :: (a -> Bool) -> f

We don't want to get into undecidable or overloaded instances, so in order to handle lists, ByteStrings and Maps, we'll create another helper class that represents the case when filter returns a function:

class CanFilterFunc c i | c -> i where
+    filterFunc :: (i -> Bool) -> c -> c

Our functional dependency is essentially say "we have some container c which holds i, and only i." In theory we might want to get rid of that fundep, and then we could express things like "you can filter a ByteString like a collection of Word8s or a collection of Chars." However, I've opted to avoid such expressions for now.

Next we need to provide an instance of CanFilter for instances of CanFilterFunc:

instance (b ~ c, CanFilterFunc b a) => CanFilter (b -> c) a where
+    filter = filterFunc

This might be a bit surprising: why do we use b -> c instead of c -> c, and what's the purpose of b ~ c? The answer is that we're trying to help GHC with type inference. If we used c -> c, it would mean "here's an instance for when you want a function that takes c and returns c. What we're defining instead is an instance for any unary function, and then with the b ~ c equality constraint we're saying, "oh, and we only allow this to work if the input matches the output." This means that if GHC is only able to identify the type of either the input or the output, type inference still succeeds.

Instances of CanFilterFunc are pretty simple; here are the three mentioned above.

instance CanFilterFunc [a] a where
+    filterFunc = Prelude.filter
+instance CanFilterFunc ByteString Word8 where
+    filterFunc = S.filter
+instance Ord k => CanFilterFunc (Map k v) (k, v) where
+    filterFunc = Map.filterWithKey . Prelude.curry

Finally, our conduit instance:

instance (Monad m, r ~ r') => CanFilter (Pipe l i i r m r') i where
+    filter = CL.filter

Again, we use the equality constraint trick to force the upstream and downstream result types to match. (If you haven't been following the recent conduit changes, don't worry about it.)

Future changes

I wrote the library so far by porting a project at work over to it, and adding in functionality as needed. I think this is the best approach to building up a useful prelude, and I'd like to ask others to help out in this as well. For now, it might be best to just copy in the ClassyPrelude module to your project and add things as necessary, and afterwards send a pull request.

For immediate upcoming features, I'm planning on adding support for vector and unordered-containers datatypes, as well as aeson for JSON parsing. (Edit: I already added vector and unordered-containers support.) Max and I have also been discussing reversing the conduit/classy-prelude dependency, and providing instances in conduit itself. (Edit: For now, we have a separate classy-prelude-conduit package.) I'd like to hold off on that till classy-prelude stabilizes.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/haskell-vm.html b/public/blog/2012/07/haskell-vm.html new file mode 100644 index 00000000..107dabb1 --- /dev/null +++ b/public/blog/2012/07/haskell-vm.html @@ -0,0 +1,20 @@ + The Haskell Virtual Machine +

The Haskell Virtual Machine

July 10, 2012

GravatarBy Greg Weber

The Haskell Platfrom on a Virtual Machine

I sent a patch to update a vagrant Haskell VM to the latests version of the platform. The author published a blog article titled Haskell on Heroku when originally releasing, but a Haskell VM has little to do with Heroku.

Why Vms

  • installation isolation
  • easy to snapshot & rollback
  • shareable
  • run Linux software and use Linux package managers on Mac/Windows hardware
  • properly match the environment your software will run in

The last point is very important for server software. +Your VM environment should match your deployment environment. +Installation on your server should be automated, and that same automation should be able to install your dependencies on your local VM. +This means only learning how to install to one platform, but also that you have less worries about bugs being introduced because of the difference between the environments. It also means you can statically link on a VM and push that binary to production.

The first point about installation isolation is very important for Haskell. The more levels of isolation you have, the less you will deal with install/dependency issues. Using A VM for some of your Haskell work (perhaps a paticular application) lets you avoid using cabal-dev or hsenv since you mostly want one canonical set of cabal packages.

VM difficulties

VMs do add some hassle. A good portion of this hassle has been mitigated by the VM provisioning tool called Vagrant that the above VM uses.

In the past, VMs were less mature and computers were less powerful. +Today, even the free and open source Virtualbox is very capable. +I have a very capable CPU, a lot of RAM, and an SSD hard drive, and I cannot tell the difference between using the VM or my native system.

My workflow on a Mac

I plan on blogging about this more in the future. Occasionally I fire up an XCode project on my Mac. But everything that is deployed to a server I run in a VM. My Mac is my work computer, and I also run a Linux VM with a GUI that serves as my personal computer.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/new-logging-system.html b/public/blog/2012/07/new-logging-system.html new file mode 100644 index 00000000..a925b205 --- /dev/null +++ b/public/blog/2012/07/new-logging-system.html @@ -0,0 +1,15 @@ + Yesod's new logging system +

Yesod's new logging system

July 5, 2012

GravatarBy Michael Snoyman

There have been essentially three separate efforts to going on to get logging added throughout Yesod:

  • Greg spearheaded the Yesod.Logger module and the file-location package, which allow you to add log messages that contain a log level and the source file location where the message was sent.
  • Kazu wrote the wai-logger (and later fast-logger) package to allow for very efficient concurrent output to file handles. This was originally written for use in his mighttpd2 web server (based on Warp). Greg additionally wrote the request logger middleware, based to some degree on wai-logger.
  • We had a fair amount of existing logging code in place already which didn't use either of the approaches above. For example, the warpDebug function basically had its own implementation of the request logger mentioned above.

When I started working on keter and was reviewing log output, I decided one of the major goals for the 1.1 release would be to rework the logging system top to bottom to fit the following goals:

  • Use Kazu's efficient output code wherever possible.
  • Simplify the user-facing aspect of the implementation.
  • Keep the user-friendly output Greg provided for development.
  • By default, have output always auto-flush so that Keter log files don't lose data.

I finally got a solid block of time to work on this, and I'm very happy with the results. You can look at some of the discussions about this; I just wanted to give a brief overview of the approach we've ended up with.

Refactoring fast-logger

There are a few important aspects to Kazu's packages for our purposes:

  • The efficient Handle code.
  • A DateRef value that makes current time rendering cheap.
  • A log formatter that uses Apache's log format.

The third piece there depends on wai, but the other two are completely general-purpose. Previously, the DateRef code was in wai-logger, which introduced an unnecessary dependency on wai. The first minor tweak I made was to move DateRef to fast-logger.

The second is more important. Greg introduced a type called Logger for encapsulating the concept of a Handle that can be written to efficiently. I moved this code to fast-logger as well, with one addition: Logger now encapsulates both a Handle and a DateRef. In other words, it has everything you need to start producing timestamped logs.

Finally, I set up a function called mkLogger to create a Logger from a Handle. To deal with auto-flushing, it takes an additional parameter to determine whether flushing should be performed after each write.

RequestLogger

Now that the Logger type is provided by fast-logger itself, we can let wai-extra's RequestLogger middleware depend on it directly. This definitely cleaned up the code quite a bit.

The next step was to create a settings type to handle the different possible configurations for request logger, basically: where to log to, which format to use (Apache or Greg's developer mode), and details like whether to use colors.

In the end, we now have a single mkRequestLogger function, the settings type, and two convenience pre-built middlewares: logStdout (which uses the Apache format) and logStdoutDev (which uses the colored, developer output).

MonadLogger

Felipe came up with a very nice addition: we should create a general typeclass for monads which allow you to log messages. Then, the functions from Yesod like logDebug could be run from a number of different monads, not just a Handler.

The result is MonadLogger. This typeclass has a single function for logging messages, including the log level (warn, error, debug, etc) and the location in the source code where it occurred. The module provides instances for all of the common monad transformers (including ResourceT).

As for the common base monads (IO, ST, and Identity): they all have instances as well, but they don't do anything. The idea here is that you can have code that will log statements, but you can control the logging behavior using the base monad.

In the specific case of Yesod, the Handler and Widget monads have both been given instances of MonadLogger which actually print results, using methods you define in the Yesod typeclass. In other words: you still have full control of logging in these monads.

Persistent

Finally, we get to add a feature we've wanted for a while: logging of Persistent SQL queries. The approach is simple: add a MonadLogger superclass to the SQL code, and inject a log statement whenever performing a database query or action. If the base monad is IO, then no logging will occur. But if you're running this from within yesod, the messages will be included with the rest of your logs. (The SQL statements all have a log level of "SQL", so if you want to keep SQL out of your logs, you'd just modify your messageLogger method to ignore those messages.)

If you're using Persistent from a non-Yesod context, and you want to have SQL statement logging, you'd need to create a separate base monad that knows how to log messages. Most likely I'll add such a monad transformer to monad-logger in the future. It should basically be a newtyped ReaderT holding onto the function to writing log messages.

Check it out

I've sent a pull request with the changes for wai-logger, fast-logger, and adding the new monad-logger package. The beta branches of wai, persistent, and yesod are all up-to-date with these changes. If you have specific ideas for the logging system or would like to see other improvements, please bring them up!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/resumablesource.html b/public/blog/2012/07/resumablesource.html new file mode 100644 index 00000000..f5c717d5 --- /dev/null +++ b/public/blog/2012/07/resumablesource.html @@ -0,0 +1,65 @@ + conduit 0.5: ResumableSource and finalization +

conduit 0.5: ResumableSource and finalization

July 22, 2012

GravatarBy Michael Snoyman

One of the more advanced features of conduit is connect-and-resume. I've +discussed the motivation for this elsewhere, but it essentially comes down to +simplifying interleaving of input streams. One very practical ramification of +this is that the http-conduit API can be drastically simpler than the +previous http-enumerator API.

For the most part, connect-and-resume is a technique which hides under the +surface, and you don't have to really think about it too much. Starting with +conduit 0.5 (and http-conduit 1.5), its semantics have changed a bit and it +is now a bit more user-facing, which has led to a bit of confusion. I want to +explain why it changed, and how to adapt your code.

Core issue: finalization

At the core of this change sits finalization. Let's take a relatively simple +example: reading data from a file. And let's simplify further and ignore +exceptions. In the normal conduit workflow (i.e., not using +connect-and-resume), the basic workflow is:

  1. Wait till the downstream Sink asks for input.
  2. Open the file handle.
  3. Pull a chunk of data and provide it downstream.
  4. Repeat (3) as long as downstream needs input.
  5. When either downstream is done, or no data is left in the file, close the +handle.

Let's ask a simple question: what happens if the Sink never asks for input? +We could in theory open the file and immediately close it, but that's +inefficient. Instead, we would like to do nothing regarding the file reading in +this case. Keep that in mind.

Now, let's take into account connect-and-resume. The basic idea is that, when +downstream is done, we don't immediately close the file. Instead, we return a +new something (to be defined later) which lets you continue reading the +file from where you left off.

something == Source

So we used some kind of "something" above to be the frozen state of the file +reading. Let's say for a moment that we wanted this something to be a plain old +Source. That works out really nicely: we can reuse all of our existing tools +for dealing with Sources for this frozen things.

The problem comes in when this frozen Source is never actually used again. +Remember in our first normal connection example, we said that- if the file was +never actually read from- no finalization needs to take place. But in our case, +the file has already been read from, and therefore we do need to perform +finalization, even if the Source isn't actually used.

This was precisely the situation in conduit 0.4. The solution was to always +call a Source's finalizer. This means that in some cases, finalizers needed to +be defined where they logically did not apply (e.g., how to finalize a +sourceFile when you've never read from it). In more technical terms, it means +that the PipeM constructor needed to have a finalizer defined as well, which +led to lots of redundant code.

conduit 0.5: something == ResumableSource

Starting in conduit 0.5, we've refactored this situation a bit. Finalizers are +now associated only when yielding a value downstream. This means that if a +Source is never pulled from, no finalizer is registered. This automatically +addresses our issue of the unused sourceFile.

The problem comes in with connect-and-resume. In this case, we need to perform some finalization in all cases. To accomodate this, we have a new datatype:

data ResumableSource m o = ResumableSource (Source m o) (m ())

A ResumableSource is just a Source and its associated finalizer. There are three basic operators for interacting with a ResumableSource:

  • $$+ connects a normal Source to a Sink, and returns a ResumableSource.
  • $$++ connects a ResumableSource to a Sink, and returns a new ResumableSource.
  • $$+- connects a ResumableSource to a Sink, and then closes it.

The changes in http-conduit

Hopefully this puts us in a good position to understand what's going on with +http-conduit. Simplifying things just a bit, the function to make an HTTP +request in http-conduit 1.4 was:

http :: Request -> IO (Source IO ByteString)

In http-conduit 1.5, this changed to:

http :: Request -> IO (ResumableSource IO ByteString)

The reason is that we have a finalizer associated with our Source: we need to +shut down our socket, even if we never pull any data from it. In conduit 0.4 +and http-conduit 1.4, we could rely on the fact finalizers are always run, +even if the Source isn't used. But in 1.5, in order to ensure that the socket +is closed, we need to use ResumableSource.

This actually works out to a fairly minor change in usage. In many cases, you +can simply replace $$ with $$+-, and continue using the library exactly as +you did before.

Getting a Source back

In some circumstances, you may really want to get a Source out of your +ResumableSource. (For example, you may have a pre-existing API like wai +that you're trying to conform to.) In order to allow this, conduit provides +the unwrapResumable function, which gives you both a Source and a +finalizer.

But the finalizer and Source it gives you are both specially set up to avoid +double-finalization. If the Source is used, then the finalizer doesn't +perform any action. On the flip side, if the Source is never used, then the +finalizer will perform the originally registered finalization. This way, for +instance, your socket will be closed once, and precisely once.

Conclusion

I hope this post clarifies the API changes in http-conduit 1.5. If not, +please let me know, and I can try to elaborate a bit more.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/07/shelly-update.html b/public/blog/2012/07/shelly-update.html new file mode 100644 index 00000000..745958f3 --- /dev/null +++ b/public/blog/2012/07/shelly-update.html @@ -0,0 +1,47 @@ + State of the Shelly (Haskell scripting lib) +

State of the Shelly (Haskell scripting lib)

July 27, 2012

GravatarBy Greg Weber

What Shelly is not

  • A better way to write untyped shell scripts

The only fundamental advantage that non-shell languages have over shell is platform consistency. +Shelly should work fine on Windows if you aren't using it to invoke unix commands.

Of course there are also fundamental downsides to non-shell languages. Distributing them is not easy. +Shelly is just a Haskell library: it cannot solve distribution issues. +I use Shelly for my work, personal, and Haskell community scripts and don't have any great difficulties distributing my scripts.

The purpose of Shelly

  • A convenient way for Haskell code (that may not be primarily concerned with scripting) to invoke other programs
  • A way to bring more type-safety (and other Haskell features) into your shell scripts
  • Faster scripts than some other non-shell alternatives

Shelly is not an attempt to re-invent shell scripting like PowerShell. +It is still executing strings and collects the returned string. +You can start building up command specific interfaces that increase type-safety, but Shelly doesn't add much help here, you are really just using Haskell. +Building up command specific interfaces takes additional effort, so you are probably going to do it for the minority of commands being executed.

The benefit of type safety comes largely in the code that processes the results of the commands you need to run or whenever you build up an abstraction in your script (which includes just defining a function).

Even trivial Ruby scripts can be noticeably slow to startup and parse options. Scala needs to wait for the JVM to startup. Haskell hits a sweet spot of good type system and fast startup execution.

Whats new?

Since I originally published an introductory article I have been making a lot of refinements.

A list-oriented interface

Anton K made an amazing contribution of a list-oriented interface. You can automatically batch file operations without using forM, etc. +He named it Shelly.Pipe, referrring to shell piping, but I am wondering if the name is overloaded by Haskell's streaming Pipe concept and if we should rename it to Shelly.List

Good error messages

Shelly defines a monad, previously ShIO. On request from a user I also changed the ShIO monad name to just Sh. I switched from using type to newtype. I now have to do some extra type class deriving, but the error message is a much friendlier Sh rather than listing out the detail of the Monad transformer.

Shelly already has verbosity settings that can print out executed commands and their results. +But after starting to use shelly at work, I had some errors that were not that easy to track down, and I began to question my usage of Haskell.

I can't recommend that anyone without existing expertise in Haskell use it for impure code since it doesn't have stack traces. +When something goes wrong, stack traces are the minimum amount of information that should be at your disposal. +And yet I contribute to a web framework and a scripting library! The nice thing about a web application is that requests are generally isolated from eachother, run a small amount of code, and log requests, so tracking down failures becomes easier. +The fact that Haskell does not have stack traces is a testament to how type-safe it is, but also to the fact that it has not seen a great deal of real world usage.

At yap.TV where I work, we like FP and use Scala (it has stack traces!) to serve up APIs, but we don't want to pay the JVM startup cost for scripts. So this is where Haskell should be able to shine, but I had to do something about the error messages.

Shelly now gives you something different than stack traces: detailed logging. +In most cases this should be more useful than a stack trace. +Shelly keeps a log of API usage and writes it out to a file in a .shelly folder when an exception is thrown. +Previously I dumped it to the screen, which was easy, but sometimes made for just too much information. The current .shelly folder implementation probably still needs some more iterations. +This is all in addition to the verbosely settings that will print out commands and their output as the program is running. +Shelly's own error messages strive to give necessary detail and in some cases it will catch Haskell exceptions and re-throw them with better messages.

There are some downsides to the logging approach to errors.

  • You could have an exception from code that isn't logging its usage. Note that shelly will still dump its log on failure. But if you do some other IO that can fail, you should use liftIO to bring it into Sh and use trace or tag to log what they are doing.
  • The logging as implemented is essentially a memory leak. I don't use shelly in long-running processes. A long-running program should re-enter the shelly monad to clear the previous logged data. In the future shelly may alwasy write to the log file and automatically delete it when there are no errors.

A variadic command runner

I didn't know writing variadic functions was so easy in Haskell -- that is if you return the same type every time. +I had a lot of issues teaching the Haskell type system how to return either Sh a or Sh ().

shelly $
+  listing <- run "ls" ["-a", toTextIgnore "foo"]
+  listing <- cmd "ls" "-a" "foo"

If all this did was remove brackets and commas, I would not have bothered. The real point is that shelly can now take as an argument any value that can be turned into a FilePath. Thanks to John Milikin for suggesting this approach. In practice it means you can now use a FilePath as an argument without extra boilerplate conversion. Shelly's filepath combinators </> and <.> work the opposite way, automatically converting a Text to FilePath. This means you can write large shelly scripts that very rarely need to convert between Text and FilePath. I now execute almost all shell commands with cmd. However, cmd loses compositional capabilities, so when I build up abstractions over commands I use run.

cmd also creates a greater need for using Haskell's little known (and documented), but very useful defaulting feature. +This is at the top of my scripts now:

{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}
+{-# OPTIONS_GHC -fno-warn-type-defaults #-}
+import Shelly
+import Data.Text.Lazy (Text)
+default (Int, Text)

Relative paths

A normal Haskell program that uses relative paths or changes the working directory is not thread-safe because another thread could change the working directory, which is essentially a mutable global variable. Shelly fixes this by keeping track of the shelly working directory in its state, which is always accessed in a thread-safe manner. cd just changes Shelly's state. The first thing all Shelly functions do with a relative FilePath is make it absolute using the Shelly working directory. This is normally transparent to the user, except that ls and find would always give back absolute paths. I added a relativeTo function, which ls will now use to keep directory listing relative (if it is given a relative path). find calls ls and also benefits.

Find functions

I generalized the find function into several 'find*' functions that one can use to fold over instead of accumulating a list and also filter out directories or files. Directory filtering can make finds much more efficient. Folding also has potential to increase efficiency, but I have not done any performance analysis of the file finding code.

Backgrounding and shelly-extra

There are two use cases for backgrounding.

  • launching daemons
  • concurrency/futures

My standard reply to launching daemons is: do not use shelly (or any shell script) for this, other than to launch a daemon launcher. +If you use Shelly to launch a daemon with an ad-hoc approach you are probably going to end up with some ad-hoc issues. +I use shelly to launch monit, and I plan on trying out angel, which is written in Haskell. +Your OS probably has its own daemon launching tools also.

Shelly now has a futures implementation in a separate shelly-extra package (thanks to a github contributor). +The futures require SafeSemaphore, and I want to keep shelly's dependencies to a minimum, so I plan on moving functionality that needs extra dependencies to shelly-extra package.

I like the end result of the futures implementation because:

  • It doesn't require modifications to the shelly package.
  • It guarantees your program will wait for all concurrent jobs to finish

Generally shell scripts contain a lot of quick commands, but when you have the occasional commands that are noticeably long and independent of other commands, you can easily run them concurrently.

escaping False

By default, shelly will escape shell characters. You can set this off or on with escaping.

SSH

Shelly now has a crude way to run commands over SSH that should be improved in the future.

sshPairs_ "server" [
+    ("cd", ["adir"]),
+    ("ls", ["*"])
+  ]

It takes the list of command pairs and combines them with '&&' and runs them over ssh unescaped. +Haskell does not have a client side SSH library, but I am beginning to wonder if it is truly a pre-requisite for simple SSH needs, or if we can get by on the existing system ssh command.

The future

  • Improve SSH interaction
  • Fix how the PATH env variable is handled
  • Move from lazy Text to strict Text

I have been making a lot of bug fixes and small API changes and bumping the package versions very quickly. I am getting pretty happy with the API now though, and nearing a stable API 1.0 release.

It is great to hear from those successfully automating tasks with Shelly. Its fun to try to discover new concepts with Haskell, but you can also just enjoy using it to automate some boring tasks.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/08/announcing-yesod-1-1.html b/public/blog/2012/08/announcing-yesod-1-1.html new file mode 100644 index 00000000..3c109ff3 --- /dev/null +++ b/public/blog/2012/08/announcing-yesod-1-1.html @@ -0,0 +1,79 @@ + Announcing: Yesod 1.1, Persistent 1.0, and a new initiative +

Announcing: Yesod 1.1, Persistent 1.0, and a new initiative

August 7, 2012

GravatarBy Michael Snoyman

The Yesod team is happy to announce the 1.1 release of Yesod, and the 1.0 +release of Persistent. Yesod is a web application framework for Haskell, which +allows you to leverage type safety to help mitigate entire classes of bugs, +while allowing you to write your code quickly and concisely. Persistent is +Yesod's most commonly used data storage layer, featuring full compile time +checking of database interactions.

These releases maintain a large degree of backwards compatibility, and as such, +upgrade should be fairly simple. As users begin the upgrade process, we'll put +together a collection of notes for handling upgrades and post them here.

Changelog

Yesod

  • Upgrade to conduit 0.5
  • Hierarchical routing.
  • Control of sitewide Hamlet settings in the scaffolding.
  • Easily add additional template languages in Settings.hs.
  • yesod add-handler automates the process of adding a new route, creating the handler module, creating stub handler functions, and updating Application.hs and the cabal file.
  • yesod keter builds a keter bundle. With a config setting, it will also upload it for you.
  • By default, response bodies are now fully evaluated before sending to avoid empty responses when pure code throws an exception. DontFullyEvaluate is provided to override this default.
  • Better control of uploaded file handling, defaulting to temporary file system storage.

Persistent

  • Upgrade to conduit 0.5
  • Sum types
  • Support for sqltype=... attribute.

A new initiative: add-ons

Yesod has a very strong set of core packages. We provide many features out of +the box with a standard platform install. And our development approach is very +open, allowing many people to submit features back into these core packages.

Overall, this has been a great approach. Users have a well known set of +libraries they can rely on, and those libraries are very featureful. However, +this centralized development approach has a few downsides:

  • It doesn't give much room for experimentation. Either the code makes it into +the core packages, or it's not in at all.

  • There are only so many hours in the day, and only so many people on the +Yesod core team. If every feature anyone ever dreams of has to go through +us, we'll become a bottleneck.

We're not actually dealing with any form of technical problem. Yesod is already +designed to be highly modular. Most of the "core components" of Yesod, like +forms and authentication, are provided as separate packages anyway. There's +actually very little in the yesod-core package itself.

Greg and I have discussed this some, and we think it's time to start a new +initiative in the community: writing add-ons. An add-on approach has a number +of advantages:

  • A new contributor doesn't have to deal with any of the complexity of the +main Yesod libraries, instead getting to focus on much smaller pieces of +code.

  • There's no overview process necessary: you can write your code, put it on +Github/BitBucket/wherever, upload to Hackage, and email the mailing list, +without anyone needing to review your code. (You can of course still ask us +to review code if you want.)

  • If you have some crazy idea you'd like to experiment with, you can do so. +Since it's not part of the core packages, you can play around as much as you want.

So how do you create an add-on to Yesod? I'd say it breaks down into one of three +categories:

A collection of helper functions

A great example of this is +yesod-goodies. The idea is +to just take some commonly used code, or some code to interface with an +external system like Gravatar, and package it up. Often times, this code won't +even have anything to do with Yesod specifically: the Gravatar code in +yesod-goodies, for example, is just interested in generating the appropriate +URLs.

There are some things to keep in mind when writing this kind of code, such as +keeping your type variables more generic. For example, in a typical Yesod site, +you could write something like:

someFunction :: Int -> Handler RepHtml

But in generic code, you can't use the Handler type alias. Instead, you'll +need to use GHandler (generic handler), and leave type variables empty for +the sub and master sites, e.g.:

someFunction :: Int -> GHandler sub master RepHtml

Pre-built Widgets

Remember that Yesod was built with reusable components in mind. One of the +strongest examples is Widgets, allowing you to bundle together HTML, CSS, and +Javascript. One possibility is to take some external API (like Google Maps) and +package up some front end to it (for example, +Haskellers has some location selecting code in +it).

Like Handler, you'll need to change your type signature a bit, e.g.:

mapSelector :: GWidget sub master ()

And you can also use type classes to specify some additional requirements. For +example, if you're going to be using jQuery, you could write something like:

usesJquery :: YesodJquery master => GWidget sub master ()
+usesJquery = do
+    master <- lift getYesod
+    addScriptEither $ urlJqueryJs master
+    ...

Subsites

The other major reusable component in Yesod is subsites. This allows you to +package up entire sets of route handlers and reuse them across multiple sites. +There's a very solid example of a chat +subsite in the Yesod book. +Other existing subsites are for authentication and static files. And another +subsite others have been working on is an admin subsite (similar to Django's +admin system).

Conclusion

I think this initiative will allow more users to become involved in Yesod +development, and let more interesting bits of code come out of the woodwork. +None of this is to say that the Yesod core team is going to start turning away +pull requests. Quite the contrary, we'll still happily be accepting added +features that are solid and stable. (And of course bug fixes and performance +enhancements will continue as well.) This also doesn't mean you're on your own: +if you have an idea and want some help bringing it to fruition, bring it up on +the mailing list, there are lots of great people there who can give amazing +feedback.

As this add-on process progresses, I'm also hoping users start writing more +blog posts about their experiences in writing these add-ons, and eventually we +can get a community-driven set of guides for new add-on writers. I also believe +that in the near future, there will be even more advantages available to add-on +authors, but that idea will have to wait for a later blog post.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/08/classy-prelude-good-bad-ugly.html b/public/blog/2012/08/classy-prelude-good-bad-ugly.html new file mode 100644 index 00000000..cfd747be --- /dev/null +++ b/public/blog/2012/08/classy-prelude-good-bad-ugly.html @@ -0,0 +1,171 @@ + ClassyPrelude: The good, the bad, and the ugly +

ClassyPrelude: The good, the bad, and the ugly

August 3, 2012

GravatarBy Michael Snoyman

First a message from our sponsors: Yesod 1.1 is fully tested and ready for +release. I'm holding off until the beginning of next week, since I hate making +major releases on a Friday (if there are any bugs, I won't be around on +Saturday to fix them). If you're really eager to check it out, you can get the +latest and greatest from Github.


I promised to write this blog post a week or two ago, but never got around to +it. Dan Burton reminded me about it yesterday... so here's paying off my debt +:).

So far, most of the discussion around +classy-prelude has focused +around the "classy" bits. As Felipe mentioned a few weeks ago, there's a lot +more going on than just the classy parts. So I'd like to break open the +discussion more and explain what exactly is going on.

Before we start, I think I need to give a bit of an explanation of my +development strategy, as I think some people don't understand my approach. When +attacking a problem for the first time, I throw in the kitchen sink. Many +people prefer to take a more iterative approach, adding features one step at a +time after a lot of careful testing. And in many cases, I think that's the only +valid approach.

But for a project like classy-prelude, I'm interested in being much more +experimental. That means I'm happy to throw in some half-baked ideas to see if +they work. Don't mistake that as me saying I fully endorse and recommend this +approach. On the contrary, as I hope this post makes clear, I'm highly +skeptical of some the choices I made. But I'm also a firm believer that we +won't know if the choices were bad unless we try them out.

So without further ado, let's break down the classy-prelude, starting with +the "obvious good" category and moving further into the unknown. (And I think +it goes without saying that statements like "obviously good" are subjective.)

Win: Full compatibility with standard libraries

I saw the contrary claim made many times in various discussions, so let's put +it to rest. Unlike other prelude replacements, classy-prelude does not try +to fix flaws in the base package. Functor is not a superclass of Monad, +fail still exists, and Num is unchanged. That's not to say that I think +that the base package did everything correctly (I don't), but rather, by +sticking to the standard typeclasses, classy-prelude code should work with +any existing library without a hitch.

From a user perspective: you should be able to use classy-prelude in one +module, ditch it in another, depend on some packages that use it and others +that don't, without any problem. In fact, I've done exactly that in some of my +code. If you use classy-prelude in your library, your library users should +not be affected beyond the fact that they have to install classy-prelude.

As an aside, I'd like to see some of those bigger changes make it into base +ultimately, and I think an alternate prelude is a great way to play around with +the ideas. But such an alternate prelude wouldn't be much use for real-world +use, which is why I haven't taken that route with classy-prelude.

Win: Don't export partial functions

I really wish Prelude didn't export head, tail, and a bunch of other +partial functions. So classy-prelude doesn't. More generally, I've been +taking a whitelist approach to which Prelude functions are exported. If I've +left out one of your favorite functions, it's likely because I just didn't get +around to it yet, not due to any hatred of it. If there's some total function +in Prelude that you'd like added, just send a pull request.

Win: Export commonly used functions from other modules

I prefer using <$> to fmap in most circumstances. I like to use first and +&&&, and a huge number of my modules import mapMaybe. All of these +functions/operators have names that are well recognized in the community, I +believe are unambiguous, and are very generally useful. Since the main purpose +of classy-prelude is to save you from extra keystrokes, let's just export +them all by default.

The selection here is clearly going to be pretty opinionated. And the list +isn't really that exhaustive right now (again, pull requests welcome). But +while we can argue over exactly which functions should be exported, I think +this is a pretty solid advantage.

Win: Export datatypes

How many modules have you written with lines looking like:

import Data.ByteString (ByteString)

And how many times do you just import Data.ByteString qualified and use +S.ByteString? How often is ByteString imported from Data.ByteString.Lazy +instead? And do you make the alias for the module S or B?

These are all thoroughly uninteresting questions. Typing out the code is +boring. And trying to remember which convention is used in the particular +module you're working on is just a pain. So in classy-prelude, we have a +simple convention: export the datatype. If there's a lazy variant, prefix it +with L. So we end up with ByteString, LByteString, Text, LText, +Map, HashMap, and so on.

There's nothing earth-shattering here. It just removes an annoyance.

Probably a win: Generalize using existing typeclasses

I like Monoid a lot. I think it's a great typeclass. And I think it's a shame +that in Prelude, concat only works for lists and not arbitrary Monoids. +Similarly, I wish that ++ was just mappend. So in classy-prelude, I've +done just that.

This is a bit controversial, because it can make error messages a bit more +confusing. But let me reinforce something that I probably didn't clarify +enough: classy-prelude is not intended for beginners. I don't think that a +Haskell tutorial should be using it for examples. classy-prelude is intended +for Haskellers who are already battle-tested with GHC's error messages, and +won't shy away from some more general types.

One change I didn't make, but was considering, was using . and id from +Control.Category. I'm not sure how much of a practical benefit that would +provide people, while making error messages likely much more complex. But if +people want that change, let's discuss it.

More debatable win: recommend better datatypes

I've gone on record many times saying that I dislike type FilePath = [Char]. +system-filepath is +(yet another) wonderful package by John Millikin, and I use it extensively in my +personal code. (It's also making its way more solidly into the Yesod ecosystem, +though we frankly don't do that much filesystem access in Yesod.)

In classy-prelude, I export system-filepath's FilePath type. This is +likely the most "breaking change" we have, though it doesn't really prevent you +from continuing to use [Char] for all your filepaths. classy-prelude also +exports a bunch of the functions and operators for manipulating filepaths, like +basename and </>.

Again, this is an opinionated decision. But I think moving over to +system-filepath could be a huge win for the Haskell community in general, and +regardless of classy-prelude's future, I hope it catches on.

More controversial: create a bunch of new typeclasses

Now we get to the fun stuff. classy-prelude defines a bunch of new +typeclasses for functions which are commonly imported qualified. I mentioned it +previously, but I don't think my point got across, so I'll say it again. The +purpose here has nothing to do with equational reasoning. This is purely a +technique for name overloading, nothing more.

Let's expand on that a bit. One of the nice things about typeclasses is that +they are usually associated with a set of laws that define their behavior. This +lets us write code against a typeclass and know, based on the laws, how it will +behave. We don't need to know if we're dealing with a list of a Set, we know +that mappend empty foo is the same as foo.

That's not my purpose here. The purpose is purely about name overloading. I +haven't stated a semantics for what lookup needs to do, because the answer is +it depends on the container. This means that classy-prelude is not +decreasing the cognitive load of the programmer in any meaningful way. If there +are semantic differences between the insert functions for a HashMap and a +Map, you still need to know about them. (To my knowledge, no such difference +exists.)

So let's say it again: the only purpose for this technique is to allow name +overloading.

Based on this, I think it's fair to claim that classy-prelude doesn't make +code more buggy due to its lack of associated laws. The libraries you're using +already don't conform to a set of laws. classy-prelude is just making it +easier to use them. If you write type-generic code and try to use it for +multiple container types (not a practice I recommend), it's possible that +you'll get buggy code due to semantic differences. But the same applies if you +write a function using Data.Set, copy-paste it, and then replace Data.Set +with Data.HashSet.

All that said, I'm not saying that a set of associated laws isn't a good thing. +I'd love to add them. And if classy-prelude continues, I'm sure we will add +them. But for the initial proof-of-concept experimental release, I don't think +it was worth the investment of time.

One final idea here is to leverage some existing collections of typeclasses +(Edward Kmett's reducers package +in particular) instead of defining our own typeclasses. Again, for the +proof-of-concept, that idea was premature, but going forward I'm hoping to look +into it. There are obstacles we'd have to overcome (like ensuring good +performance), but I don't think anything is insurmountable.

Very controversial: make those typeclasses work with conduit

I was surprised not to see as much of a discussion about this point as the +previous one. In my mind, this was by far the most controversial choice in +classy-prelude. It's fair to say that I put it in more out of curiosity if it +would work than anything else.

Let's take filter as an example. A relatively simple typeclass to model it +would look like:

class CanFilter container element | container -> element where
+    filter :: (element -> bool) -> container -> container

This would work for lists, ByteString, Set, and so on. However, here's the +(simplified) signature of filter from Data.Conduit.List:

filter :: (i -> Bool) -> Conduit i m i

This doesn't fit in with our CanFilter class above. CanFilter has a +function that takes two arguments, whereas in conduit filter has just one +argument.

So here's the trick. We recognize that the first argument is the same (a +predicate), and just leverage currying. Recognizing that CanFilter's filter +can be viewed as a function of one argument returning a function, we split it +up into two typeclasses:

class CanFilter result element where
+    filter :: (element -> bool) -> result
+class CanFilterFunc container element | container -> element where
+    filterFunc :: (element -> bool) -> container -> container

And then we define a single instance of CanFilter that uses CanFilterFunc +for all cases where we return a function:

instance (container ~ container', CanFilterFunc container element)
+  => CanFilter (container -> container') element where
+    filter = filterFunc

(If you're curious about the equality constraint, see this excellent Stack +Overflow +answer.)

We can then separately define an instance for conduit, which does not overlap +at all, and allows us to reuse the name filter in significantly different use +cases. Frankly, I think it's pretty cool that we have this kind of flexibility +in the type class system. But from a practical standpoint, I'm not sure it's +really a great trade-off:

  • Since the two filter concepts work fairly differently, it may be more +confusing than anything else to lump them together.

  • Error messages get significantly more confusing.

  • I don't think that conduit usage in this sense is prevalent enough to +warrant the costs.

In other words, if there's something I'd want to cut out first from +classy-prelude, it's the gymnastics which it pulls to accomodate conduit +instances. It was definitely a fun part of the experiment, and I'm glad to have +tested it. If anyone has an opinion either way on this, let me know.

Moving forward

There are a number of minor bugs in the typeclass definitions in +classy-prelude, requiring more explicit type signatures than should be +necessary in some cases. Those kinds of things are easily fixed. If people find +specific examples, please bring them to my attention.

Dan Burton started a project called +ModularPrelude +which takes a different approach to the namespace issue, replicating first +class modules via the record system. I think it's definitely an interesting +approach, and think it can coexist very well with classy-prelude. But it's +not enough for my taste: I prefer the relative terseness of typeclass-based +code.

However, putting that project together with my points in this post, I think +it's important to note that there is a very large part of classy-prelude +which has nothing to do with typeclasses. Dan called this BasicPrelude.hs, +and I think it makes a lot of sense to provide that separately (though at least +for now, I'll keep it in the same package for convenience).

The idea is simple: I think a lot of people agree with me that some major +aspects of this prelude are no-brainers, and would like to use them. People are +understably more wary of the experimental bits. (As I've said before, so am I: +I won't allow classy-prelude to be used in yesod in its current state.) So +let's create a stable basis, and encourage experimentation. As I mentioned at +the beginning, since we have full compatibility with base, there's no real +concern of fragmentation.

Once I remove the conduit aspects of the library, I think it will open it up +for much better analysis of typeclass laws. It might be possible to completely +drop a number of typeclasses and use the reducers versions instead. For +others, more experimentation might be necessary.

And as usual: feedback is welcome, especially the constructive kind :).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/08/joining-forces-advance-haskell.html b/public/blog/2012/08/joining-forces-advance-haskell.html new file mode 100644 index 00000000..943f79a9 --- /dev/null +++ b/public/blog/2012/08/joining-forces-advance-haskell.html @@ -0,0 +1,15 @@ + Joining forces to advance Haskell +

Joining forces to advance Haskell

August 13, 2012

GravatarBy Michael Snoyman

When I started working on Yesod (almost four years ago now!), my objective was simple. I strongly believed that Haskell was the best language out there for general development, and wanted to have a full set of tools both for getting web development accomplished quickly, and for leveraging Haskell's safety features. I believed that by having a set of high-level development tools available to us in Haskell, developers in general would be able to keep the rapid development we've become accustomed to, but drastically reduce the occurrence of bugs and simplify maintenance.

Four years later, I still think it is the right decision. The Haskell community has grown, Yesod has blossomed, and I've personally had the opportunity to use Haskell as my nearly-exclusive language for the past two years. I think my beliefs from four years ago have been affirmed: Haskell has drastically simplified maintenance, and I've seen a massive decrease in the number of bugs in produced code. And I don't think I'm alone. Haskell has quickly moved from an obscure research language to a heavily utilized and more heavily discussed language. Yesod has moved from a small project I used for some freelance projects to a major framework with a great team of developers and a wonderful community of users.

What we've done is great. And I want to do more.

So in that vein, I'm very excited to announce that I am taking the position of Lead Software Engineer at FP Complete. FP Complete is a great company, with the goal of driving the broader adoption of Haskell. Haskell has already accomplished so many things, and done so many things right. I believe FP Complete is positioned to effectively push Haskell the rest of the way into industry. Aaron Contorer (CEO of FP Complete) and I have discussed our roadmap at length, and I think the plans he has laid out really address the major obstacles to Haskell adoption. As we start making more information available over the next few months, I think you'll agree with me that FP Complete is providing something truly valuable to the Haskell community.

So what does this mean for Yesod? A lot, actually. FP Complete will be bringing Haskell to commercial users, and Yesod will be a big part of that. FP Complete is very supportive of the entire open source Haskell community, and will be contributing directly to it. I will continue fully in my role as maintainer and lead developer for Yesod. This move to FP Complete will not be taking my focus off of the open source community at all. On the contrary, I expect greater opportunities to be able to make contributions back.

In addition, FP Complete will be providing a number of other resources for the community to take advantage of. We’ll be working intently to lower the barrier to entry to Haskell and Yesod for developers. And part of our solution will include giving existing Yesod developers an opportunity to create components to be used by others.

There's a lot on our plates as we're ramping up for our first product release towards the end of the year. We have a lot of great ideas, and I'm looking forward to getting to share them with you as they develop. I'm also looking forward to continuing to interact with and get feedback from the wonderful Haskell community.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/08/webinar-oreilly.html b/public/blog/2012/08/webinar-oreilly.html new file mode 100644 index 00000000..a75d1e92 --- /dev/null +++ b/public/blog/2012/08/webinar-oreilly.html @@ -0,0 +1,90 @@ + O'Reilly Webinar: Designing Type-Safe Haskell APIs +

O'Reilly Webinar: Designing Type-Safe Haskell APIs

August 6, 2012

GravatarBy Michael Snoyman

I'm going to be giving a webinar with +O'Reilly this Thursday (10 AM Pacific) +titled "Designing Type-Safe Haskell APIs." If the topic interests you, please +sign up. I'd love to see you all +there (figuratively speaking, of course).

The talk itself will mostly focus on actual code and techniques. For timing +reasons, I'm leaving out most of the "motivation" section, explaining why +we should care about designing with type safety in mind. So I'm going to try +and give that part of the talk right here.

My focus for motivation is developers in the real world writing real software. +There are many other use cases where type safety has even more obvious +benefits. If you want to prove correctness of your code, for example, strong +typing is essential. But for our purposes, the audience is normal development +projects: putting together some web site, writing a tool to process data, +manipulating XML files, etc.

In other words, I'm looking at the realm where people are writing code in Java, +Python, Ruby, and a slew of other languages. The question is: why should we +bother with type safety and Haskell? What practical advantage does it have over +these other languages?

The first thing to recognize is that type safety isn't a feature in and of +itself. Imagine you're pitching a new product to some company, or trying to +convince your employer that a new Haskell-based tool will be a great +investment. They say, "Why? What does it provide over our existing product?" If +your response is "It's type safe," you're not going to be convincing anyone.

In other words, type safety is a means to an end.

So what is that end? Well, let's look at what it does. A simple explanation is +that type safety moves runtime bugs to compile time errors. (I'll cover this +concept more in the talk.) But again, such a claim won't sell your software. So +this is also just a means to an end. And that end is minimizing bugs in +code.

Minimizing bugs is a feature that you can sell people on. If I'm comparing +product A and product B, and I have some reason to believe product B will have +less bugs, I'm very likely to buy it. It can win out over other aspects of the +product, such as cost (do you really want to pay for bugs?), ease-of-use (it sure +was easy to generate that invalid output), or extra capabilities (hey look, +this product can do 50 things badly).

But now that we've narrowed in on our main goal (minimizing bugs), we have to +also acknowledge that neither type safety nor Haskell hold a monopoly here. +That are a large number of techniques for minimizing bugs in code: unit +testing, static analysis, quality assurance (QA), and many others. So we have a +new question: why is type safety such a great tool for minimizing bugs?

For one, type checking is run automatically. Excluding abuses of unsafe +functions, you can't compile Haskell code that doesn't type check. On the other +hand, you can easily write, run, and deploy code that fails unit tests. But +that's really more of a process problem. The real issue is if you're relying on +QA to catch all your bugs. The QA process is time-consuming and expensive. Type +checking is cheap.

But type checking has another major advantage. It covers your entire program +automatically. Any invariant enforced by the type system is guaranteed to be +correct throughout your codebase. Compare this with unit tests, where you need +to write a unit test for each and every place where something could go wrong. +And even so, for the invariants it enforces, the type system will give total +coverage, versus unit tests which will only cover the specific cases you've +included.

A common rebuttal to that last point is that the type system can't really +enforce important things. I've seen people claim things like, "I haven't had +any bugs in the past X months that could have been caught by the type system." +My response to that is, you're using the wrong type system. Trying to judge the +power of type safety based on the incredibly weak type system provided by Java +and similar languages misses out on a huge number of possibilities. I'll be +giving a bunch of examples on Thursday of invariants you can enforce in the +type system itself. A small example is using types to avoid cross-site +scripting (XSS) attacks. This is a bug that exists in a large number of +websites, and is difficult to track down. With strong typing, we can make it +almost disappear.

Coming back to our type safety sales pitch: I'll claim that in many cases, type +safe design is the best way to minimize bugs. However, we can't fool ourselves +into believing it's the only technique available. There are times when unit +tests, static analysis, and QA will give better results than type safety. (Just +look at the Yesod codebase, we have huge test suites in addition to our +extensive usage of type safety.) In some cases, trying to enforce an invariant +in the type system will make code more difficult to work with.

And that's where we need to start looking at a cost/benefit analysis. Type +safety is a great powerful tool, but it does have some costs. In most cases, +with proper usage and proper training, I think that type safety's benefits far +outweigh its costs. The additional setup costs for designing the type safe +system will not only drastically simplify maintenance of a project, but even +during the initial development phase, I believe that those costs amortize. In +all but the simplest of programs, I think type safety will pay for itself +before you make your first alpha release.

But that argument only applies to languages that make it easy and convenient to +write type-safe code. Haskell and a few other languages excel at this: +declaring new datatypes is concise, pattern matching is built in, the compiler +can catch things like unhandled cases, and so on. Other languages do not +provide such power in an easy-to-access fashion. And in those cases, the sell +for type safety is harder. I strongly believe that a reason we're seeing people +moving to dynamically typed languages is because the popular statically typed +languages have given static typing such a bad name.

I know that was quite wordy (which is why I won't be saying it on Thursday), +but let me try to sum up the major points:

  • Type safety is a great tool for minimizing bugs.

  • Type safety is one tool of many for that job.

  • There are distinct advantages for type safety, e.g., always run, full coverage.

  • Languages like Haskell make it cheaper to take full advantage of type safety.

  • Even in Haskell, type safety is not sufficient to handle all cases.

Enough for the introduction, let's discuss the actual techniques at the webinar +on Thursday.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/09/building-haskell-ide.html b/public/blog/2012/09/building-haskell-ide.html new file mode 100644 index 00000000..99e2160b --- /dev/null +++ b/public/blog/2012/09/building-haskell-ide.html @@ -0,0 +1,77 @@ + Building a Haskell IDE +

Building a Haskell IDE

September 12, 2012

GravatarBy Michael Snoyman

Our overarching directive at FP Complete is to promote the commercial adoption +of Haskell. We're approaching this by looking at what commercial needs are not +being met currently, and trying to fill in the gaps. One of these areas is an +IDE.

Some of you out there are probably thinking, "What gap? I don't even want an +IDE." I usually fit into that category myself. As everyone knows, Vim is the +only text editing software anyone ever needs, though of course Emacs is a +pretty good OS.</flame-war>

The Vim/Emacs mentality works great for many Haskellers. And it may even be +that the majority of current Haskellers don't use an IDE. But that's almost +certainly selection bias. Since +there aren't yet any fully-featured Haskell IDEs* that are easy to get started +with, Haskell attracts people who are comfortable without an IDE. Or new +Haskellers quickly learn to make do without one.

That may have seemed like a very long way to say "we're building an IDE," but +there's an important point I'm trying to stress. We're building an IDE for a +specific demographic: newcomers to Haskell who are more comfortable in an IDE +than in a text editor. This is a very important point to keep in mind. Some of +the features that newcomers will want may not be appealing at all to an +experienced Haskeller. And on the flip side, the advanced features some +Haskellers may desire would likely be overkill for newcomers. (That doesn't +mean we can't implement them, but it does change priorities.)

With all that said, let me finally state the purpose of this blog post: I want +to share with the community our initial design goals, and get community +feedback on what you think should be added or altered.

*Note: I am fully aware that there are other projects attempting to create +Haskell IDEs, such as EclipseFP and +Leksah. I'm not trying to downplay those projects, but I +believe as more details come out about what we're offering, the +apparent overlap between the projects will decrease.

Basic functionality

To start off with, we'll need the basics: create projects, edit files, and +build. We'll have some basic project templates (a Yesod application for one, +but there will be others). There will also be some built-in commands for common +activities (such as the yesod add-handler command).

Text editing itself needs to have all of the usual suspects: common +keybindings, syntax highlighting, search/replace.

Debugging/help

Like most IDEs, we'll be providing error messages in a separate pane. We're +also hoping to provide some "humanization" of error messages to make them less +intimidating (with the full error message still available for those who want to +see it). From each error message, we'll be providing some options, in +particular jumping to the relevant code and a "more information" link which +will go to an FP Complete Wiki page.

The UI isn't ironed out, but we'll provide something along the lines of +hovering over an identifier to get its type, along with links to where the type +is defined or online documentation. This very likely will go beyond simply +linking to the Haddocks: we'll likely have links to cookbooks explaining common +ways to use different types and functions.

Project management

A prime goal is to avoid the need for users to be editing cabal files manually. +(Actually, we have an even more general goal of completely removing cabal +dependency hell from users, but that will have to wait till another blog post.) +Most of what goes on in a cabal file is tedious, which is exactly the kind of +stuff we want to avoid. So the IDE will automatically handle:

  • Keeping track of the list of modules to build.
  • Package dependencies.
  • Various GHC flags (e.g., optimization level, depending on release vs debug builds).
  • Language pragmas.

That last one might be a bit more controversial, so let me explain. I think one +of the truly terrifying experiences for new users is to open up a file, and +have the first 15 lines be LANGUAGE pragmas. (I think the next terrifying +experience is when the following 40 lines are import statements.) Having to +remember to put the LANGUAGE pragmas in each file is always a pain. For my +current projects, I still take that approach, as it means the code is GHCi +friendly. But in our new IDE world, we'll be able to handle that kind of stuff +automatically.

As many of you know, the Yesod scaffolded site already encourages you to put +your language pragmas into the cabal file instead of the individual source +files. This works nicely because it's very unlikely that you'd want to use GHCi +directly (that's what yesod devel is for). In our new IDE, I think we'll have +the same situation.

Building

There should never be a need to drop down to the command line to build. Whether +you're debugging some code, profiling it, or building for production, it can +all be done through the IDE. Longer term, we're hoping to bundle quite a few +debugging and profiling tools to make the experience straight-forward.

This also ties in to deployment. We'll want to have easy deployment for web +applications. This will likely mean some integration with a build server as +well for production of binaries that are properly configured for your +deployment server. (In other words, something that builds appropriate keter +bundles.)

Source control

We'll need to have built-in source control management, likely backing with a +popular providing such as Github. Initially, support for committing, pushing, +pulling, and merging will be sufficient, though as time goes on we'll want to +improve what we're offering.

More advanced tools

There are lots of additional tools that we’ll want to be adding. Prime candidates include refactoring, debugging, and profiling. We’ll try to leverage existing tools (such as GHC’s built-in memory profiles) whenever possible, but a fair amount of this will be new endeavors.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/09/caching-fd.html b/public/blog/2012/09/caching-fd.html new file mode 100644 index 00000000..6404ee0e --- /dev/null +++ b/public/blog/2012/09/caching-fd.html @@ -0,0 +1,123 @@ + Caching file descriptors +

Caching file descriptors

September 21, 2012

GravatarBy Kazu Yamamoto

This is the third article for the series of "Improving the performance of Warp". +As I wrote in "Sending header and body at once", I will explain how to avoid the open()/close() system +calls in this article.

simple-sendfile basis

I designed the simple-sendfile to use as few system calls as possible, +since "system calls are evil for network programming in Haskell". +To make the story simple, I will only talk about the sendfile system call +on Linux.

The type of the sendfile function in Network.Sendfile is as follows:

sendfile :: Socket -> FilePath -> FileRange -> IO () -> IO ()

FileRange has two constructors: EntireFile and PartOfFile.

Let's consider the case where we send the entire file +by specifying EntireFile to the sendfile function. +Unfortunately, the function has to call the stat() system call +to know the size of the file because the sendfile() system call on Linux +requires the caller to specify how many bytes to be sent +(sendfile() on FreeBSD/MacOS has magic number '0' which indicates +the end of file).

If WAI applications know the file size, they can specify +PartOfFile to avoid the stat() system call. +It is easy for WAI applications to cache file information +such as size and modification time. +If cache timeout is fast enough (say 10 seconds), +the risk of cache inconsistency problem is not serious. +And because we can safely clean up the cache, +we don't have to worry about leakage.

open() and close()

If PartOfFile is specified, +the sendfile function calls open(), sendfile() repeatedly if necessary, and close(). +When I implemented the simple-sendfile package, +I noticed that open() and close() should also be eliminated. +For this, we should cache file descriptors.

Caching file descriptors works as follows: +If a client requests that a file be sent, a file descriptor +is opened. And if another client requests the same file shortly thereafter, +the file descriptor is reused. +At a later time, the file descriptor is closed +if no Haskell thread uses it.

Sound easy? Unfortunately, I had no idea on how to safely cache file descriptors. +It seems to me that it is difficult to ensure that +no Haskell thread uses a file descriptor when closing it.

A typical tactic for this case is reference counter. +But I was not sure that I could implement a robust mechanism +for a reference counter. What happens if a Haskell thread is +killed for unexpected reasons? +If we fail to decrement its reference counter, +the file descriptor leaks.

Andreas motivated me to consider this issue again +by pointing out that the performance bottleneck of Warp is +open()/close(). I thought this over for a month and +all the necessary pieces came together suddenly.

The timeout manager of Warp

I implemented a cache mechanism for file descriptor based on +Warp's timeout system. +So, let me explain how Warp's timeouts work first. +For security reasons, Warp kills a Haskell thread, +which communicates with a client, +if the client does not send a significant amount of data for a specified period (30 seconds by default). +I think that the heart of Warp's timeout system is the following two points:

  • Double IORefs
  • Safe swap and merge algorithm

Suppose that status of connections is described as Active and Inactive. +To clean up inactive connections, +a dedicated Haskell thread, called the timeout manager, repeatedly inspects the status of each connection. +If status is Active, the timeout manager turns it to Inactive. +If Inactive, the timeout manager kills its associated Haskell thread.

Each status is refereed by an IORef. +To update status through this IORef, +atomicity is not necessary because status is just overwritten. +In addition to the timeout manager, +each Haskell thread repeatedly turns its status to Active through its own IORef if its connection actively continues.

To check all statuses easily, +the timeout manager uses a list of the IORef to status. +A Haskell thread spawned for a new connection +tries to 'cons' its new IORef for an Active status to the list. +So, the list is a critical section and we need atomicity to keep +the list consistent.

A standard way to keep consistency in Haskell is MVar. +But as Michael Snoyman pointed out in "Warp: A Haskell Web Server", MVar (in threaded RTS) is slow. +This is because each MVar is protected with a home-brewed spin lock. +So, he used another IORef to refer the list and atomicModifyIORef +to manipulate it. +atomicModifyIORef is implemented via CAS (Compare-and-Swap), +which is much faster than spin locks.

The following is the outline of the safe swap and merge algorithm:

do xs <- atomicModifyIORef ref (\ys -> ([], ys)) -- swap with []
+   xs' <- manipulates_status xs
+   atomicModifyIORef ref (\ys -> (merge xs' ys, ()))

The timeout manager atomically swaps the list with an empty list. +Then it manipulates the list by turning status and/or removing +unnecessary status for killed Haskell threads. +During this process, new connections may be created and +their status are inserted with atomicModifyIORef by +corresponding Haskell threads. +Then, the timeout manager atomically merges +the pruned list and the new list.

The algorithm to cache file descriptors

Warp's timeout approach is safe to reuse as a cache mechanism for +file descriptors because it does not use reference counters. +However, we cannot simply reuse Warp's timeout code for some reasons:

Each Haskell thread has its own status. So, status is not shared. +But we would like to cache file descriptors to avoid open() and +close() by sharing. +So, we need to search a file descriptor for a requested file from +cached ones. Since this look-up should be fast, we should not use a list. +You may think Data.Map can be used. +Yes, its look-up is O(log N) but there are two reasons why we cannot use it:

  1. Data.Map is a finite map which cannot contain multiple values for a single key.
  2. Data.Map does not provide a fast pruning method.

Problem 1: because requests are received concurrently, +two or more file descriptors for the same file may be opened. +So, we need to store multiple file descriptors for a single file name. +We can solve this by re-implementing Data.Map to +hold a non-empty list. +This is technically called a "multimap".

Problem 2: Data.Map is based on a binary search tree called "weight +balanced tree". To the best of my best knowledge, there is no way to prune the tree +directly. You may also think that we can convert the tree to a list (toList), +then prune it, and convert the list back to a new tree (fromList). +The cost of the first two operations is O(N) but +that of the last one is O(N log N) unfortunately.

One day, I remembered Exercise 3.9 of "Purely Functional Data Structure" - +to implement fromOrdList which constructs +a red-black tree from an ordered list in O(N). +My friends and I have a study meeting on this book every month. +To solve this problem, one guy found a paper by Ralf Hinze, +"Constructing Red-Black Trees". +If you want to know its concrete algorithm, +please read this paper.

Since red-black trees are binary search trees, +we can implement multimap by combining it and non-empty lists. +Fortunately, the list created with toList is sorted. +So, we can use fromOrdList to convert the sorted list to a new +red-black tree. +Now we have a multimap whose look-up is O(log N) and +pruning is O(N).

The cache mechanism has already been merged into the master branch of +Warp, and is awaiting release.

UPDATE: Discussion here is based on my ignorance. Please read also comments.

New functions in simple-sendfile

I explained the sendfile function and +the sendfileWithHeader function in +this article and the previous one, respectively:

sendfile :: Socket -> FilePath -> FileRange -> IO () -> IO ()
+sendfileWithHeader :: Socket -> FilePath -> FileRange -> IO () -> [ByteString] -> IO ()

To avoid the open()/close() system call, I added two more functions +to the simple-sendfile package:

sendfileFd :: Socket -> Fd -> FileRange -> IO () -> IO ()
+sendfileFdWithHeader :: Socket -> Fd -> FileRange -> IO () -> [ByteString] -> IO ()

Of course, the master branch of Warp uses the last one.

That's all. Thank you for reading this long article.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/09/header-body.html b/public/blog/2012/09/header-body.html new file mode 100644 index 00000000..30f409d4 --- /dev/null +++ b/public/blog/2012/09/header-body.html @@ -0,0 +1,53 @@ + Sending header and body at once +

Sending header and body at once

September 19, 2012

GravatarBy Kazu Yamamoto

This article explains one technique to improve the performace of +the simple-sendfile package on which Warp relies. +Readers are supposed to read "Improving the performance of Warp".

Before Andreas suggested me that the open()/close() system calls are +a performance bottleneck, I found another bottleneck in the simple-sendfile +package. Before talking how to avoid open()/close(), I would like to +start with how to fix the latter bottleneck. I believe that this is +a good introduction to understanding the architecture of Warp.

Warp is an HTTP engine for WAI (Web Application Interface). It runs WAI +applications over HTTP. The type of WAI applications is as follows:

type Application = Request -> ResourceT IO Response

That is, an application takes Request and returns +Response. As you can guess, Warp first receives an HTTP request +from a client and parses it to Request. Then, Warp gives the Request +to an application and takes a Response from it. Finally, Warp builds +an HTTP response based on Response and sends it back to the +client.

As I pointed out in "Mighttpd – a High Performance Web Server in Haskell", "system calls are evil for network programming in Haskell". So, Warp is defined to use as few system calls as possible.

In the HTTP request parser, Warp uses Network.Socket.ByteString.recv which issues +the recv() system call. If a client uses HTTP pipelining, +multiple HTTP requests can be received with one recv().

In the HTTP response builder, the situation is complicated because +Response has three constructors:

ResponseBuilder Status ResponseHeaders Builder
+ResponseSource Status ResponseHeaders (Source (ResourceT IO) (Flush Builder))
+ResponseFile Status ResponseHeaders FilePath (Maybe FilePart)

For ResponseBuilder and ResponseSource, +Network.Socket.ByteString.sendAll- based on the send() system call- +is used to send both an HTTP response header and body with a fixed buffer.

For ResponseFile, the old Warp used +Network.Socket.ByteString.sendMany to send an HTTP response header +and Network.Sendfile.sendfile to send a file as an HTTP response body. +They are based on the writev() and sendfile() system call, respectively. +The combination of writev() and sendfile() had a problem.

When I measured the performance of Warp, +I always did it with high concurrency. That is, I +always make multiple connections at the same time. It gave me a good +result. However, when I set the number of concurrency to 1, I found +Warp is really really slow on Linux and FreeBSD.

I realized that this is because Warp uses +the combination of writev() and sendfile(). +In this case, an HTTP header and body are sent in separate TCP packets. +To send them in a single TCP packet (when possible), +I implemented a new function called +Network.Sendfile.sendfileWithHeader, +which is available in simple-sendfile v0.2.4 or later.

On Linux, it uses the send() system call with the MSG_MORE flag to store +a header and the sendfile() system call to send both the stored header and +a file. On FreeBSD, it uses the sendfile() system call with the header +arguments to send both a header and a file. This trick ensures that +both a header and a body is sent in a single TCP packet (if the file is small enough).

This makes Warp at least 100 times faster when measured with httperf.

I will explain how to avoid the open()/close() system calls +used in Network.Sendfile.sendfileWithHeader in the next article.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/09/header-composer.html b/public/blog/2012/09/header-composer.html new file mode 100644 index 00000000..7b606a7a --- /dev/null +++ b/public/blog/2012/09/header-composer.html @@ -0,0 +1,86 @@ + Composing HTTP response headers +

Composing HTTP response headers

September 28, 2012

GravatarBy Kazu Yamamoto

This is 4th article for the series of "Improving the performance of Warp". +Readers are supposed to read the following articles:

  1. Improving the performance of Warp
  2. Sending header and body at once
  3. Caching file descriptors

Note that new versions of Warp have already been released, +which contain the cache mechanism of file descriptors +described in the third article.

The old composer for HTTP response header

Andreas pointed out that the code to compose HTTP response header +may be another performance bottleneck. +So, I tackled this problem. +Let me explain how the old composer for HTTP response header +worked. Recall that the Response data type has +three constructors:

ResponseBuilder Status ResponseHeaders Builder
+ResponseSource Status ResponseHeaders (Source (ResourceT IO) (Flush Builder))
+ResponseFile Status ResponseHeaders FilePath (Maybe FilePart)

As you can see, each constructor includes both Status and ResponseHeaders, which are defined in the http-types package. The former is defined as follows:

data Status = Status {
+    statusCode :: Int
+  , statusMessage :: ByteString
+  }

The following are the definitions of the latter:

type ResponseHeaders = [Header]
+type Header = (HeaderName, ByteString)
+type HeaderName = CI ByteString

That is, ResponseHeaders is a list of pairs of case-insensitive ByteString (for field keys) and ByteString (for field values).

The old composer for HTTP response header creates a Builder of the blaze-builder package by appending the Bytestrings in the Status and the ResponseHeaders. Each append operation runs in O(1). +To simpify, I will only talk about the ResponseFile case on Linux. The Builder is converted to a list of packed ByteStrings and sent with the writev() system call. And then a file (HTTP response body) is sent with the sendfile() systam call.

As I described in Sending header and body at once, this code was changed. The list of packed ByteStrings is stored to the socket buffer by using send() with the MSG_MORE flag and then a file is sent with sendfile(). At this point, I received Andreas's comments.

The new composer for HTTP response header

In many cases, the performance of the blaze builder is sufficient. +But I suspected that it is not fast enough for +high performance servers. +So, I tried two methods.

The first method: create a list of ByteStrings without packing +and send the list with writev(). Since the header and its body +should be squeezed into a single TCP packet (if possible), +I set the TCP_CORK socket option before calling writev() +and reset it after calling sendfile(). +But this method is no better than the old composer. +I guess that copying many small chunks from the user space to the kernel +space does not work efficiently.

The second method: calculate the necessary length of the HTTP header +from Response and allocate a buffer for a new ByteString +according to the length. +Then copy Status and ResponseHeaders with low level +functions. Here is the most fundamental copy function:

copy :: Ptr Word8 -> ByteString -> IO (Ptr Word8)
+copy !ptr !(PS fp o l) = withForeignPtr fp $ \p -> do
+    memcpy ptr (p `plusPtr` o) (fromIntegral l)
+    return $! ptr `plusPtr` l

This copies a sequence of Word8s from the ByteString in the second argument +to the Ptr Word8 (extracted from ByteString) in the first argument. +This new packed ByteString is sent by using send() with the MSG_MORE flag and a file is sent with sendfile(). This improved the performance. So, I adopted this method.

Readers may notice that calculating length of an HTTP header is redundant. +If we design better WAI having APIs to manipulate header fields, +we can omit the calculation. +I will discuss the possible design change for WAI in a future article.

Division

The current code to copy Status is as follows:

copyStatus :: Ptr Word8 -> H.HttpVersion -> H.Status -> IO (Ptr Word8)
+copyStatus !ptr !httpversion !status = do
+    ptr1 <- copy ptr httpVer
+    writeWord8OffPtr ptr1 0 (zero + fromIntegral r2)
+    writeWord8OffPtr ptr1 1 (zero + fromIntegral r1)
+    writeWord8OffPtr ptr1 2 (zero + fromIntegral r0)
+    writeWord8OffPtr ptr1 3 spc
+    ptr2 <- copy (ptr1 `plusPtr` 4) (H.statusMessage status)
+    copyCRLF ptr2
+  where
+    httpVer
+      | httpversion == H.HttpVersion 1 1 = httpVer11
+      | otherwise = httpVer10
+    (q0,r0) = H.statusCode status `divMod` 10
+    (q1,r1) = q0 `divMod` 10
+    r2 = q1 `mod` 10

Since statusCode is Int, we need to convert it to three Word8s. +As you can see, the convention involves +two divMod operations and one mod operation. +Unfortunately, due to bugs of GHC, such division operations are slow. +Unnecessary guard is reported in ticket 5161. This has been fixed in GHC 7.4.1. +Ticket 5598 says +that divMod is complied +into unnecessary two divisions. +This has been fixed in GHC 7.6.1.

Even if you are using GHC 7.6.1 or later, this kind of conversion is slow and +unnecessary. We can prepare ByteStrings corresponding to values of Int beforehand. +There are two ways to implement this.

Method A: prepare an array of ByteString whose index is status code. +This does not change the definition of Status +but it appeared slower than Method B.

Method B: change the definition of Status to have one more +ByteString (e.g. Status 200 "200" "OK"). This is faster than method A +but breaks backword compatibilities. +To my experience, +http-conduit, wai-app-file-cgi and (of course) warp have to be fixed. +Aristid Breitkreuz, the author of http-types, agreed +to adopt method B but has not yet released a new version.

That's all about what I have done to the composer for HTTP response header.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/09/improving-warp.html b/public/blog/2012/09/improving-warp.html new file mode 100644 index 00000000..6659bb7b --- /dev/null +++ b/public/blog/2012/09/improving-warp.html @@ -0,0 +1,47 @@ + Improving the performance of Warp +

Improving the performance of Warp

September 13, 2012

GravatarBy Kazu Yamamoto

For the last month, I have been improving the performance of Warp and +got a good result. I would like to explain why I did it and what I +have actually done in several blog articles. In the articles, I will +only talk about GHC's threaded RTS.

In 2009, I started to implement my original Web server, Mighttpd +(called Mighty) using GHC 6. Since I also implemented a mail related +server before, I knew the limitation of GHC 6's IO manager:

  1. It is implemented using the select() system call. So, the IO manager cannot handle TCP connections over 1,024.
  2. Since the number of IO manager is one, it cannot utilize multi-core.

To solve these two problems, I hit upon an idea to use the pre-fork +technique. This is, before accepting new TCP connections, some +number of child processes are created by the fork() system call. +The child processes shares its listening port and OS selects +one of the child processes for a new TCP connection.

As many know, Bryan O’Sullivan and Johan Tibell improved the IO +manager by using modern system calls: the epoll family on Linux and +the kqueue family on BSD/Mac. The new IO manager was integrated into +GHC 7.

GHC 7 solved the problem 1 but problem 2 still remains. It seems to me +that servers complied with GHC 7 may not get great performance out of +multi-core even if we specify the +RTS -Nx option. To my experience, +the prefork technique is still effective and creating N child +processes is a good workaround where N is the number of cores.

I decided to switch my HTTP engine to Warp and I re-implemented +Mighttpd as an HTTP logic on the top of Warp. It is now called +Mighttpd 2. It still uses the prefork technique for the reason above.

Since some people started to use Mighttpd 2 in the real world, I +enhanced it to support dynamic re-configuration and graceful +shutdown. To implement these features, I needed to write some +boilerplate for inter-process communication using UNIX signals. +It was really boring and I was convinced that making the IO manager +parallel is very important.

When I sent messages about the parallel IO manager to GHC HQ, Simon +Marlow kindly told me that Andreas Voellmy, who is a Ph D student +under Paul Hudak, has already implemented it on Linux. So, I joined +his activity and started to test his new IO manager with Mighttpd 2.

Andreas said to me that Mighttpd 2/Warp is not fast enough to place +burden on the parallel IO manager. He pointed out that issuing +open()/close() system calls and the HTTP response composer are +bottlenecks. So, I started to improve the performance of Warp. I will +explain what I have actually done in the coming articles.

Meanwhile, it would be nice to read +"The Architecture of the Mighttpd High-Speed Web Server" +to understand the benefits of network programming in Haskell.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/09/project-templates.html b/public/blog/2012/09/project-templates.html new file mode 100644 index 00000000..69fb4ae9 --- /dev/null +++ b/public/blog/2012/09/project-templates.html @@ -0,0 +1,121 @@ + Haskell Project Templates +

Haskell Project Templates

September 19, 2012

GravatarBy Michael Snoyman

In my last blog +post, I discussed +one aspect of my work with FP Complete: the goal of creating a Haskell IDE. +Since then, I've gotten lots of incredible feedback from the community, and in +particular have been in email discussion with some of the major players in the +Haskell IDE scene. I think it's safe to say that we all agree that there's +going to be a large amount of overlap in our efforts, and we will be +coordinating to try and minimize duplicated work as much as possible, while +still providing for the unique goals of each IDE project.

In response to all this, I've created a Wiki on +Github to keep track of our goals, +and a Haskell IDE Google Group +for discussion. I strongly recommend joining if you're interested in any more +sophisticated Haskell code editing tools.

We have a lot of topics to cover, and a single blog post won't be nearly enough +to even scratch the surface. For now, I'd like to focus on one specific +feature. I haven't chosen to start here because it's the most important +feature, but because I think it's a problem that we can solve relatively easily +and thoroughly.

Project Templates

Most (all?) IDEs provide the concept of a project template: instead of writing +all of the code for a project from scratch, you select a template, answer a few +questions, and a bunch of files are automatically generated. We already have +this in the Haskell world: Yesod provides the project scaffolded (via the +yesod init command), and I believe Snap provides something like this as well. +But these are just two examples. I'm sure we could easily come up with a dozen +other possible templates: a GTK+ application, a web services client, or a +console app.

Currently, there's no standard for how this should work in the Haskell world +or, to my knowledge, in the non-Haskell world either. (If there is, please let +me know, I'd like to be able to build on existing work.) The Yesod scaffolding (and +Snap's I believe) are both generated via specialized command line tools. I'm +sure each IDE would be fully capable of building wrappers for for two tools, +but that quickly becomes an existential complexity issue. It also makes it much +more difficult for someone to start providing a new scaffolding. I know we +suffer from this already in the Yesod world, where innovation is definitely +stifled by having the One True Blessed Scaffolding.

So here are my goals for the ideal templating system:

  • A single file to represent a template. This can be some kind of archive (ZIP +file, tarball, etc), I don't really care, but single file systems simplify +things greatly.

  • Provide a Haskell library for both generating and consuming these +templates. We can have a command line tool as a wrapper around the library, +but the library should be the primary means of interacting. (You'll see this as +a pattern as I talk more about the IDE world.)

  • Build on top of commonly used formats as much as possible. The reasoning +here is that, even though we'll be providing a canonical Haskell library, +not all IDEs are written in Haskell (yet). If someone is writing an IDE in +Python and wants to provide Haskell support, we should make it as easy as +possible.

    • By the way, it's worth pointing out that, as described, there's nothing +Haskell-centric about my proposal here. I've been going in the +direction of creating language-agnostic tools and formats as much as possible +(e.g., keter, which can host web +apps written in any language).
  • I'm guessing that the most common way that people will want to actually +provide a template is as a Git(hub)/Darcs repo. It would be great if we +could provide a web service that takes a repo and automatically generates a +template file. Then users of an IDE could theoretically just type in a repo URL +to some text box and automatically get the most recent code available.

  • Similarly, we should provide a simple command-line tool that takes a folder +and generates a template file.

A semi-concrete proposal

As many of you know, I normally prefer to discuss actual working code/ideas +than to discuss theoretical ideas. In this case, however, I think it's worth +fleshing out the idea a bit before jumping in and implementing something. So +I'm going to lay out my proposal here, and ask for everyone's input and +recommendations before we start implementation. I recommend the discussion be +targetted at the Haskell IDE Google +Group as much as possible.

For file format: let's use JSON. I'm not worried about file size: these project +template files will likely be transferred over HTTP most of the time, and +compression can be performed at that level. As for binary files, we'll +base64-encode the contents.

The JSON file needs to have three sections:

  1. Metadata describing the project template itself. This would be the name of +the template, a description, author, homepage, and maybe a version. +(Version could be automatically generated as the date it was created.) This is +all pretty boring.

  2. Data that needs to be collected from the user. In the Yesod scaffolding, we +ask for the user's name, the project name, and the database backend to use. +The first two are (mostly) free-form text, while the third is an enumeration. I +think we'll need to support a few basic datatypes:

    • Text, with a regex for validation.
    • Booleans
    • Enumerations

    We can also allow default values. So to model the Yesod scaffolding, +perhaps something like this:

    {"user-fields":
    +    [ {"name":"user-name","type":"text","validation":".+","description":"Your name"}
    +    , {"name":"project-name","type":"text","validation":"[\w_]+","description":"Name of your project"}
    +    , {"name":"database-backend","type":"enumerator","choices":
    +        [{"display":"MySQL","value":"mysql"},{"display":"MongoDB","value":"mongodb"}],
    +        "description":"Name of your project"}
    +    ]}
  3. The files that will be generated. We need to take into account some issues:

    1. Some files will be generated conditionally based on the input from the user.
    2. Some of the files will be named based on the user input (e.g., the name of the cabal file).
    3. The actual contents of the file will depend on the user input (e.g., the contents of the cabal file).
    4. We want to support both textual and binary files. Binary files need not have any conditional aspect to them.

    For the first issue, we'll need to have a basic expression language. I +think equality, inequality, and, or, parantheses and variables should be +sufficient. So to say that the file config/postgres.yml should only be +generated if the database backend is postgresql, we could have something like:

    {"filename":"config/postgres.yml",
    + "contents":"We'll discuss this in a moment...",
    + "condition":"database-backend == 'postgresql'"
    +}

    For the conditional file naming, how about something like this:

    {"filename":[{"variable":"project-name"},{"content":".cabal"}],
    + "contents":"..."
    +}

    In order to solve the third point, we'll use a combination of what we've established for points 1 and 2:

    {"filename":[{"variable":"project-name"},{"content":".cabal"}],
    + "contents":
    +    [ {"content":"name: "},
    +      {"variable":"project-name"},
    +      {"content":"...build-depends:..."},
    +      {"content":"\n     , postgresql-simple >= 0.3 && < 0.4","condition":"database-backend == 'postgresql'"}
    +    ]
    +}

    The last one is easiest to solve: each file can have a field encoding which is either "text" or "base64".

    {"filename":"some-image.png",
    + "contents":"DEADBEEF",
    + "encoding":"base64"
    +}

Once we have the file format figured out, the library is relatively simple. Let's describe a simple consumption API:

data CodeTemplate
+instance FromJSON CodeTemplate
+
+data UserInputType = UIText (Maybe Regex) | UIEnumeration [(Text, Text)] | UIBool
+data UserInput = UserInput
+    { uiType :: UserInputType
+    , uiName :: Text
+    , uiDescription :: Text
+    }
+
+userInputs :: CodeTemplate -> [UserInput]
+generateFiles :: CodeTemplate -> Map Text Text -> Map FilePath LByteString

Setting up a generation API for dealing with completely static files should be +simple. It will be a bit more involved to deal with conditionals, but with +properly defined ADTs it shouldn't be too bad.

Next steps

I think the most important next step is to determine what use cases my proposal +doesn't cover. The file creating code specifically doesn't allow many common +text generation techniques, like looping, as I simply see no use case for it, +but perhaps I'm mistaken. I'm also curious to hear what other ideas people have +for project templates.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/09/small-status-update.html b/public/blog/2012/09/small-status-update.html new file mode 100644 index 00000000..5a7c33ba --- /dev/null +++ b/public/blog/2012/09/small-status-update.html @@ -0,0 +1,65 @@ + Small status update +

Small status update

September 7, 2012

GravatarBy Michael Snoyman

It's been a while since my last blog post, so I just wanted to give a small +community update touching on various things. I personally have been busy with +some changes in my personal +life, and therefore I +haven't had as much time to work on code as I would like. But the community has +been incredibly active during my hiatus.

  • Perhaps most exciting for many Yesoders: Felipe has released +esqueleto, an ESDL for generating +SQL queries. It sits on top of Persistent, and makes it easy to create +type-safe, complex SQL queries. I haven't used it for anything in production +yet (it was, after all, just released), but the API is a work of art, and the +examples are incredibly compelling.

  • Kazu has been putting through a bunch of refactorings and performance +enhancements on Warp. I won't steal his thunder on that front, I think +he'll be sharing some of that work himself.

  • Felipe has optimized our session handling code even more than he has +previously. Thanks to Lorenzo Bolla and Greg for reporting. You can see the +discussion in the Github +issue.

  • Luite and Falco Hirschenberger have been moving the new GHC API-based +yesod devel forward. Combined with the file watching code from Mark's +GSoC project, this will hopefully give us a much more responsive dev +environment.

Apologies for being less responsive than usual over the past few weeks, and +thanks to the community for picking up the slack and providing such quality +support on both the mailing list and Stack Overflow. I'm hoping to be back to +normal by the middle of next week... with a fair amount of stuff to catch up on +as well.

GHC 7.6

For those who haven't heard, GHC 7.6 has been +released. +A big congratulations to the whole GHC team. For Yesod users on Mac in +particular, this is a very important release: Luite's patch for issue +#7040 has been included, +meaning that yesod devel no longer segfaults. (If you're using 7.4, the +recommended workaround is to use 32-bit GHC.)

I'd like to remind everyone right now of our GHC support policy. We only +officially support Yesod on the most recent version of the Haskell Platform, +meaning that for the moment GHC 7.6 is not a supported target. I know for a +fact (based on emails I've received) that many of our support libraries do not +currently compile on 7.6.

Over the next few weeks, we'll hopefully be able to get all of this worked out. +Most of these problems are very trivial: changing import lists, adding language +pragmas, etc. This is a great opportunity for people looking to make some +initial commits to the project to dig their teeth in. As always, pull requests +are very much welcome.

FP Complete

I couldn't write a blog post without mentioning a bit of what's going on with +my new position at FP Complete. Most of what I want to talk about on that front +will deserve its own blog post at a later date, but there's one topic in +particular that I'd like to bring up with the Yesod community, at least to get +the seed planted.

One component that is currently lacking from the Yesod ecosystem, and the +Haskell ecosystem in general, is commercial add-ons. There is not currently any +standard, straightforward approach for writing, selling, purchasing, and using +commercial components, which hinders both the adoption of Haskell/Yesod as an +enterprise platform, and discourages some people from releasing high-quality +components for others to use.

One aspect of our offering at FP Complete will be to address this. I think this +will be a great opportunity for the existing Yesod community to spend more of +their time doing what they love: writing Haskell code. This project is still at +early planning stages, but we're very interested in getting community feedback. +If you would like to either write or use commercial components in some way, +please be in touch.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/announcing-http-reverse-proxy.html b/public/blog/2012/10/announcing-http-reverse-proxy.html new file mode 100644 index 00000000..88ea2641 --- /dev/null +++ b/public/blog/2012/10/announcing-http-reverse-proxy.html @@ -0,0 +1,82 @@ + Announcing: http-reverse-proxy, unix-process-conduit, and more +

Announcing: http-reverse-proxy, unix-process-conduit, and more

October 4, 2012

GravatarBy Michael Snoyman

I'm happy to announce the release of three new packages: +http-reverse-proxy, +unix-process-conduit, +and +network-conduit-tls. +Additionally, there is a new version of +network-conduit which +provides an improved interface.

Believe it or not, there's a reason that I'm announcing these four different +packages at the same time: put together, they provide a lot of the +functionality necessary to create a virtual hosting system working over both +HTTP and HTTPS. This work directly benefits the +keter deployment system. I'm still +testing these changes to keter, so the new version 0.3 has not yet been +released.

These new packages should be considered experimental. They are the basis of +some real life code at FP Complete, but it's early in the testing cycle, so I +cannot give any strong assurances that it will work perfectly. Nonetheless, +these packages provide some interesting features that could be useful to +others.

network-conduit and network-conduit-tls

network-conduit has been around for a while now. It provides a simple +conduit-based interface for creating TCP servers and clients. This 0.6 release +enhances the interface a bit:

  • You can register an action to be called after binding the listening socket. +This is useful for apps which bind to a restricted port and then setuid to a +non-privileged user.
  • The SockAddr is provided to both server and client applications.
  • By using settings types, it +should be easy to make additions to the API going forward without breaking +backwards compatibility.

The new addition here is network-conduit-tls, which allows you to create TCP +servers over an SSL connection. What's very nice is that it uses the same +interface for applications as the regular version, so you can write +applications that will run over either secure or insecure. This comes into play +with the next library.

Note: Currently network-conduit-tls doesn't provide a client interface. +There are some complications involved there, such as providing a certificate +approval mechanism. These aren't difficult problems necessarily +(http-conduit already does this, for example) but I decided not to include +the functionality right now. If people are interested in adding it, please +contact me.

http-reverse-proxy

This library provides two different ways of reverse proxying. The more +traditional approach leverages both WAI and http-conduit: it provides a WAI +application and makes requests to some backend host. In this scenario, the +request and response are fully parsed by the reverse proxy, which provides +opportunities for making modifications, but can also introduce a performance +overhead. Additionally, this approach can get in the way of tunneling non-HTTP +data, such as websockets.

The second approach is more light-weight. Only the initial HTTP headers are +parsed, in order to determine how to route your request. Then, all further +communications are simply passed to and from the backend server without any +further parsing. This means that, after the initial HTTP headers, the content +could be completely invalid HTTP... which in the case of websockets is actually +an advantage.

Both approaches allow you to have a very dynamic routing table: you must +provide an action which will be run on each connection to determine where to +route to. You could even go so far as to modify the routing table based on +incoming HTTP requests if you wanted to.

One final feature is the ability to convert a WAI application to a raw +network-conduit-style application. This is done by using some low-level +functions provided by Warp to parse and render the requests and responses. This +makes it possible to provide a single server which can either locally serve +responses, or pass on to a different server, depending on the request headers.

unix-process-conduit

The main motivation behind the creation of this library was actually not for +the conduit interface. It was mainly to be able to have better control of +termination of processes. To quote System.Process.terminateProcess's +documentation:

This function should not be used under normal circumstances - no guarantees +are given regarding how cleanly the process is terminated.

For my use cases, I needed guarantees that a process was really going to die. +We actually ran into this problem historically with yesod devel, and used +some complicated techniques with flag files to work around it. If we can come +up with a Windows counterpart to unix-process-conduit, we'll be able to avoid +that kludge.

But once I was writing a library for processes, I decided to go ahead and model +the input and output streams via conduits. The forkExecuteFile function takes +a Maybe (Source IO ByteString) parameter for stdin. If it's Nothing, then +the subprocess keeps the same standard input as the parent. Otherwise, stdin is +taken from the given Source. If all you want to do is provide no input to the +child, you can use Just $ return ().

Likewise, for stdout and stderr the parameter type is Maybe (Sink ByteString +IO ()). To simply ignore all output, you can use Just $ +Data.Conduit.List.sinkNull.


As I mentioned, all of the new libraries are still to be considered +experimental. If you have ideas for API improvements or find any bugs, please +let know.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/avoid-syscall.html b/public/blog/2012/10/avoid-syscall.html new file mode 100644 index 00000000..ce3ba941 --- /dev/null +++ b/public/blog/2012/10/avoid-syscall.html @@ -0,0 +1,63 @@ + Avoiding system calls +

Avoiding system calls

October 1, 2012

GravatarBy Kazu Yamamoto

This is fifth article for the series of "Improving the performance of Warp". +Readers are supposed to read the following articles:

  1. Improving the performance of Warp
  2. Sending header and body at once
  3. Caching file descriptors
  4. Composing HTTP response headers

In this article, I will explain how to avoid the fcntl() system call +and the gettimeofday() vsyscall.

Avoiding fcntl()

I sometimes compare Mighttpd and nginx with the results of strace +on Linux and/or ktrace on BSD variants. +One day, I noticed that nginx uses the accept4() system call, which I did not know about +at that moment. +(I used to be an expert of BSD variants but am a newbie to Linux.)

In the low level of GHC, file/socket operations are basically implemented as non-blocking. +If the network package is used, +a listening socket is created with the non-blocking flag set. +When a new connection is accepted from the listening socket, +it is necessary to set the corresponding socket as non-blocking, too. +The network package implements this by calling fcntl() twice: +one is to get the current flags and the other is to set +the flags with the non-blocking flag ORed.

On Linux, the non-block flag of a connected socket +is always unset even if its listening socket is non-blocking. +The accept4() system call is an extension version of accept() on Linux. +It can set the non-blocking flag when accepting. +So, if we use accept4(), we can avoid two unnecessary fcntl()s. +I modified the network package to use accept4 on Linux +and it is included in version 2.3.1.0 or later.

On BSD variants +the non-block flag of a connected socket is inherited +from its listening socket. +So, we can also avoid two unnecessary fcntl()s +on BSD variants. +But I have not implemented this.

Avoiding gettimeofday()

Date strings are used in various ways. An end HTTP server +should return GMT date strings in header fields such as +Date:, Last-Modified:, etc:

Date: Mon, 01 Oct 2012 07:38:50 GMT

For logging, a local date string in the Apache style would be convenient:

01/Oct/2012:16:42:33 +0900

It is known that formatTime in the Data.Time.Format module is too +slow for high performance servers. +Years ago, I implemented faster format packages: http-date +for the former and unix-time for the latter. +Unfortunately, they are still slow for high performance servers. +And if an HTTP server accepts more than one request per second, +the server repeats the same formatting again and again. +So, formatted date strings should be cached.

The members of the web-devel mailing-list discussed these issues +with the following assumption:

  1. Formatting time to date string is a heavy job
  2. Getting the current time by gettimeofday() is a light job

The discussion resulted in the following algorithm:

  • When a formatted date is required, first issue getttimeofday(). Then compare it with a cached time.
  • If they are equal, return the cached formatted date.
  • Otherwise, format the new time to a new formatted date, cache them, and return the new formatted date.

It seems to me that the assumption 2 is not correct. +gettimeofday() was a system call in old Linux +while it is a vsyscall in new Linux. +To my experience, I cannot say that +calling gettimeofday() is a light job on both old and new Linux.

So, I implemented a new algorithm:

  • Designated Haskell thread issues gettimeofday() every second, formats the result time to a date, and caches it.
  • When a formatted date is required, the cached formatted date is simply returned.

Only end HTTP servers should return the Date: header field. +Since Warp can be used to implement proxies, +adding Date: is not warp's job. +Also, logging is not warp's job, either. +WAI applications should take care of +both adding Date: and logging.

Yesod and Mighttpd both use the fast-logger package for logging. +I modified it so that it provides both algorithms above. +Also, I changed Mighttpd to use the new algorithm +to generate Date:. +The packages with these modification are +already available from Hackage.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/future-work-warp.html b/public/blog/2012/10/future-work-warp.html new file mode 100644 index 00000000..f92326cc --- /dev/null +++ b/public/blog/2012/10/future-work-warp.html @@ -0,0 +1,116 @@ + Future work to improve the performance of Warp +

Future work to improve the performance of Warp

October 22, 2012

GravatarBy Kazu Yamamoto

This is the last article for the series of "Improving the performance of Warp". +Readers are supposed to read the following articles:

  1. Improving the performance of Warp
  2. Sending header and body at once
  3. Caching file descriptors
  4. Composing HTTP response headers
  5. Avoiding system calls
  6. Measuring the performance of Warp

I will explain possible items to improve the performance of Warp.

Conduit

I have special cabal program which automatically specifies the +-fprof-auto (aka -auto-all) and -fprof-cafs (aka -caf-all) flags +to libraries to be installed. +So, I can take profile of all top level functions. +Unfortunately, GHC profile has a limitation: +right profiling is possible if +a program runs in foreground and it does not spawn child processes.

Suppose that N is the number of workers in the configuration file of Mighty. +If N is larger than or equal to 2, Mighty creates N child processes +and the parent process just works to deliver signals. +However, if N is 1, Mighty does not creates one child process. +The executed process itself works for HTTP. +So, we can get the correct profile for Mighty if N is equal to 1.

Here is a profile of Mighty 2.8.2 against the httperf benchmark:

COST CENTRE                MODULE                                  %time %alloc
+
+socketConnection           Network.Wai.Handler.Warp.Run             11.8   28.4
+>>=.\                      Control.Monad.Trans.Resource              8.4    1.9
+>>=.\.(...)                Control.Monad.Trans.Resource              7.7    8.0
+sendloop                   Network.Sendfile.Linux                    6.7    0.2
+==                         Data.CaseInsensitive                      3.1    3.7
+parseFirst                 Network.Wai.Handler.Warp.Request          2.8    3.4
+sendMsgMore.\              Network.Sendfile.Linux                    2.8    0.2
+push.push'                 Network.Wai.Handler.Warp.Request          2.1    2.0
+connectResume.go           Data.Conduit.Internal                     2.1    1.1
+MAIN                       MAIN                                      1.6    0.7
+>>=                        Data.Conduit.Internal                     1.3    1.1
+control                    Control.Monad.Trans.Control               1.3    2.0
+||>                        Control.Exception.IOChoice.Lifted         1.2    1.0
++++.p                      Network.Wai.Application.Classic.Path      1.2    1.3
+foldCase                   Data.CaseInsensitive                      1.2    3.1
+composeHeader              Network.Wai.Handler.Warp.ResponseHeader   1.2    1.2

My observations are:

  • I think that socketConnection and sendloop is relating to recv() and sendfile(), respectively. Since these are IO system calls, it might be natural to consume much time. I will come back this issue later.
  • Conduit and ResourceT also consumes much time. Michael is now thinking how to avoid this overhead.
  • I believe (==) and foldCase are used by Data.List.lookup to look up HTTP headers. I think we can eliminate lookup completely if we have better WAI definition.

Better WAI definition

In the HTTP response composer of Warp, lookup is used to look up:

  • Connection:
  • Content-Length:
  • Server:

These header fields are added to ResponseHeader in Response by WAI applications. +If Response has dedicated fields for them, we can directly obtain its value. +And if there is API to add these special fields and other fields, +we can calculate the total length of HTTP response header incrementally. +So, we can eliminate the current method to calculate the length by +traversing ResponseHeader.

Memory allocation

When receiving and sending packets, buffers are allocated. +Andreas suggested these memory allocations may be the current bottleneck. +GHC RTS uses pthread_mutex_lock to obtain a large object (larger than +409 bytes in 64 bit machines).

I tried to measure how much memory allocation for HTTP response header +consume time. I copied the create function of ByteString to Warp and +surrounded mallocByteString with Debug.Trace.traceEventIO. +Then I complied Mighty with it and took eventlog. +The result eventlog is illustrated as follows:

Eventlog of Mighty

Brick red bars indicates the event created by traceEventIO. +The area surrounded by two bars is the time consumed by mallocByteString. +It is about 1/10 of an HTTP session. +I'm confident that the same thing happens when allocating receiving buffers.

Michael, Andreas and I are now discussing how to reduce this overhead.

Char8

ByteString is an array of Word8, non-negative 8bit digits. As many know, there are two sets of API for ByteString:

  1. Data.ByteString directly provides Word8 API
  2. Data.ByteString.Char8 provides API based on Char.

Since header field keys are case-insensitive, +we need to convert keys to lower (or upper) letters to identify. +To carry out this job, +Haskell programmers tends to use Data.ByteString.Char8 +with toLower (or toUpper) of Data.Char. +Let's consider what kind of steps are necessary:

Haskell's Char is Unicode (UTF-32 or UCS-4). +The functions of Data.ByteString.Char8 are using the w2c and c2w functions +to convert Word8 to Char and Char to Word8, respectively. +Since Word8 is held in 32/64 bit registers, +w2c and c2w do nothing in assembler level. +The functions of Data.ByteString.Char8 themselves have no performance penalty.

However, toLower (or toUpper) of Data.Char targets the entire +space of Unicode. For our purpose, only 8bit part should be treated. +So, I implemented the word8 libraries which provides +toLower (and other functions) for both Word8 and 8bit portion of Char. +The criterion benchmark shows the following results:

Data.Char.toLower --  mean: 26.95289 us, lb 26.84163 us, ub 27.09798 us, ci 0.950
+Data.Char8.toLower -- mean: 5.603473 us, lb 5.493357 us, ub 5.840760 us, ci 0.950

So, the dedicated implementation is 5 times faster than +the Unicode implementation. +I'm planning to change Warp to use the word8 library.

Pessimistic recv()

The read()/recv() related functions of GHC are optimistic. +That is, they try to read data assuming that data is already available. +Since Handle/Socket are set non-blocking, +these functions will get the EAGAIN error if +data is not available. +In this case, these functions ask the IO manager to notify +when data becomes available. +This is accomplished by threadWaitRead. +Then, context is given to another thread. +When data become available, +the waiting thread is waken up by IO manager. +These functions obtain data finally.

When I took strace of Mighty, I saw many the EAGAIN errors caused by recv(). +As a trial, I implement pessimistic recv(). +That is, we call threadWaitRead before recv() to +ensure that data is available.

Let's compare two methods:

Optimistic recv():

  • If data is available, only one recv() is called.
  • Otherwise, the epoll related system calls and recv() are issued as well as the first recv().

Pessimistic recv():

  • In any cases, the epoll related system calls and recv() are issued.

I'm not sure which is better now. +By way of experiment, pessimistic recv() is enabled in Warp by default.

New thundering herd

Thundering herd is an old but new problem. +If a process/OS-thread pool is used to implement a network server, +the processes/OS-thread typically share a listening socket. +They call accept() on the socket. +When a connection is created, old Linux and FreeBSD +wakes up all of them. And only one can accept it and +the others sleeps again. +Since this causes many context switches, +we face performance problem. +This is called thundering herd. +Recent Linux and FreeBSD wakes up only one process/OS-thread. +So, this problem became a thing of the past.

Recent network servers tend to use the epoll/kqueue family. +If worker processes share a listen socket and they +manipulate accept connections through the epoll/kqueue family, +thundering herd appears again. +This is because +the semantics of the epoll/kqueue family is to notify +all processes/OS-threads. +I wrote code to demonstrate new thundering herd. If you are interested in, please check it out. +nginx and Mighty are victims of new thundering herd.

Andreas's parallel IO manager is free from new thundering herd. +In this architecture, only one IO manager accepts new connections +through the epoll family. +And other IO managers handle established connections.

Final remark

I would like to express gratitude to Michael for working on Warp +together and correcting my language. +I thank Andreas for useful suggestions to improve the performance of Warp.

Lastly, thank you all for reading my articles. +I will come back here when there is significant progress.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/generic-monoid.html b/public/blog/2012/10/generic-monoid.html new file mode 100644 index 00000000..3424469f --- /dev/null +++ b/public/blog/2012/10/generic-monoid.html @@ -0,0 +1,101 @@ + Generics example: creating Monoid instances +

Generics example: creating Monoid instances

October 2, 2012

GravatarBy Michael Snoyman

I recently was working on a project which included a very large datatype for +holding configuration data. The configuration data was parsed from a file. One +trick was that each config file could reference another "parent" config file. +The desired semantics are that the settings in the "child" override those in +the parent. To make things a bit more concrete, consider something like:

data Config = Config
+    { userLanguage :: Text -- only one allowed
+    , translationFolder :: [FilePath] -- can be many
+    }

Obviously, I was dealing with many more fields. The trick for dealing with the +parent folders was simple; my algorithm looked like:

parseConfig :: FilePath -> IO Config
+parseConfig fp = do
+    doc <- readFile fp -- it's stored as XML, but that's irrelevant
+    let parentFPs = getParents doc
+    parents <- mapM parseConfig parentFPs
+    let config = getConfig doc
+    return $ mconcat $ config : parents

In other words, we just use a Monoid instance to put together the different +config files. To simplify the task of creating this Monoid instance, I made +sure to add appropriate Monoid wrappers to each field as necessary. For the +example above, I would add First to userLanguage, since we only wanted to +get the first one. For translationFolder, since we want to grab the folders +from the parents in addition to the child, we'd leave it as a list. Then, the +Monoid instance is just some boilerplate:

instance Monoid Config where
+    mempty = Config mempty mempty
+    Config a b `mappend` Config x y = Config (a `mappend` x) (b `mappend` y)

Of course, writing such an instance by hand quickly becomes tedious. What I +wanted was some way to generate that boilerplate automatically. And the +solution I found was GHC 7.4's new Generics implementation. The code I wrote is +heavily based on the GHC +documentation, +which happens to be a great coverage of the topic. The docs give the example of +serialization, which uses a unary function. Implementing Monoid involves a +nullary and a binary function, which makes it a good follow-up to the +serialization example.

(Note: full code available as a Github gist.)

The first step is to create a generic version of our Monoid typeclass:

class GMonoid f where
+    gmempty :: f a
+    gmappend :: f a -> f a -> f a

This looks very similar to our standard Monoid typeclass. One tweak is the +fact that the instance of the typeclass now takes an argument (a.k.a., it's of +kind * -> * instead of *). The reason is that the Generics datatypes all +have a phantom type variable. My understanding is that this type variable is +currently unused.

Once we have this typeclass, we need to create instances for the different +Generic datatypes. There are five datatypes available: U1, K1, M1, :+:, and :*: +(please see the linked documentation for an explanation). Thankfully, most of +these instances are incredibly straight-forward.

Our first instance is for U1, which represents a nullary constructor. In +non-generic world, it's easy to deal with this case. mempty would just be the +constructor, and mappending two identical nullary constructors should result +in the same constructor. The generic version is just as simple:

instance GMonoid U1 where
+    gmempty = U1
+    gmappend U1 U1 = U1

Next, let's consider product types, e.g. data Foo = Foo Bar Baz. mempty +would want to take advantage of the mempty provided for Bar and Baz. +mappend would like to mappend the fields in the left and right Foo. We +can express this almost identically in the generic version:

instance (GMonoid a, GMonoid b) => GMonoid (a :*: b) where
+    gmempty = gmempty :*: gmempty
+    gmappend (a :*: x) (b :*: y) = gmappend a b :*: gmappend x y

Sum types are a bit trickier. It's not immediately clear what the right thing +to do is. Consider the datatype data Foo = Foo1 Bar | Foo2 Baz. Should +mempty use the first or second constructor? As for mappend, if both input +values use the same constructor, the solution is relatively simple. But what +happens if we have something like mappend (Foo1 x) (Foo2 y)? There's no +obvious solution.

So I decided to just leave off the sum type instance. What's wonderful about +the generics implementation is that this means, at compile time, trying to use +the generics code will fail on any sum type.

Nonetheless, for completeness sake, I did put together an instance. Its +semantics are to arbitrarily choose the first constructor for mempty, and the +first argument to mappend if there's a constructor conflict. This looks like:

instance (GMonoid a, GMonoid b) => GMonoid (a :+: b) where
+    gmempty = L1 gmempty
+    gmappend (L1 x) (L1 y) = L1 (gmappend x y)
+    gmappend (R1 x) (R1 y) = R1 (gmappend x y)
+    gmappend x _ = x

Ultimately, we'll end up hitting non-generic values (the actual values +contained by our datatype). At that point, we want to switch over to standard +Monoid functions. Again, the generics implementation will prevent us from +using datatypes which are not instances of Monoid.

instance Monoid a => GMonoid (K1 i a) where
+    gmempty = K1 mempty
+    gmappend (K1 x) (K1 y) = K1 $ mappend x y

And finally, we need to deal with the M1 datatype, which is just a metadata +container:

instance GMonoid a => GMonoid (M1 i c a) where
+    gmempty = M1 gmempty
+    gmappend (M1 x) (M1 y) = M1 $ gmappend x y

Now that we've implemented all of our instances, how do we use this? The +Generic typeclass provides two methods: to and from, to convert a generic +representation to a value and vice-versa. So we just take advantage of those, +together with our generic gmempty and gmappend, to come up with default +mempty and mappend functions:

def_mempty :: (Generic a, GMonoid (Rep a)) => a
+def_mempty = to gmempty
+
+def_mappend :: (Generic a, GMonoid (Rep a)) => a -> a -> a
+def_mappend x y = to $ from x `gmappend` from y

If we had control of the Monoid typeclass ourselves, we could also use the +DefaultSignatures extension right now to bake this directly into the Monoid +typeclass. Then, any time we wrote instance Monoid Foo, it would use +def_mempty and def_mappend. However, in our case, we have to do it +manually:

instance Monoid Config where
+    mempty = def_mempty
+    mappend = def_mappend

Still much cleaner than having to write it all out manually.

If you're looking for a more sophisticated example of generics usage, check out +the ToJSON and FromJSON typeclasses in the aeson +package.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/haskell-and-ci.html b/public/blog/2012/10/haskell-and-ci.html new file mode 100644 index 00000000..46d33abf --- /dev/null +++ b/public/blog/2012/10/haskell-and-ci.html @@ -0,0 +1,98 @@ + Haskell and Continuous Integration +

Haskell and Continuous Integration

October 19, 2012

GravatarBy Arash Rouhani

Continuous integration is about keeping your code base in shape and to +never let it degenerate. This is done through setting up a CI service +that will instantly build your project and run the tests once the code +base changes. All projects, in particular those with more than one +developer, will benefit from continuous integration. But is it worth +the trouble in setting it up and maintaining it? If you answered no, +this post might change your mind.

Software alternatives

Jenkins CI and Travis CI are the two continuous integration servers +I've been playing around with. They both have their pros and cons and +hopefully after reading this blog post you'll be able to pick the one +that best fits your project.

I'll be keeping my advice general to Haskell projects using the typical +cabal setup. But since this is the Yesod blog I'll include a section +related to that too.

Travis CI

Travis is a free CI service for open source projects hosted on GitHub +and it integrates well with GitHub. Travis launched in 2011 but Haskell +support were not added until March 2012.

The build agents seem to be quite fast but the maximum build time is +limited. This can be an issue for Haskell packages that have heavy +dependencies like the yesod-platform. Furthermore, Travis is still quite +new and +outages +can happen or Travis might just decide to decrease the build times, the +current alloted build times seem to between 8 to 20 minutes depending on +which build step is running. Currently Travis only supports public +repositories hosted at GitHub.

Here is a list of great features of Travis:

  • Extremely easy setup for Haskell
  • Supports all your public repos and your organization's public repos!
  • When somebody does a pull request, it will build those patches and +it integrates well into the GitHub web interface. Read +more
  • Want build results to be announced in your favorite irc channel? +Travis has many cool notification setups. Check out all the ways you +can +configure +Travis!

Jenkins CI

Many Haskellers already use Jenkins for their CI needs. To get Jenkins +running you have to install it on your own server. Jenkins is a large +Java project. There is an incredible +amount of open source plugins for it, including both Git and darcs +Integration. Setting up Haskell projects is quite easy. I've maintained +the build server for Yesod for over a year now and I've seldom needed to update +the Jenkins +configuration of that +server.

Here is a list of great features of Jenkins:

  • You don't need to use GitHub nor Git and you don't need to clutter +your repo with a .travis.yml file
  • There are plugins for most things
  • Flexibility, you can add workers if you have many projects and you +feel that Travis is congested
  • Jenkins can configure to your needs. In the Yesod world we've set up +the yesod project to be a post build of wai, persistence and +shakepeare. That enables continuous integration across dependent +projects
  • Jobs can start in any environment. Travis always run from clean +environments. This isn't always desired. In general you only want to +build your project and not it's dependencies. Fast builds is a +good CI practice!
  • Jenkins is community +driven

Some aftermath

So for a long time there have not been any popular continuous +integration hosting sites like Travis, so people had to host the CI +service themselves. As for source control hosting, Git became widely +recognized around 2007 and +the GitHub source hosting site launched a year after. Continuous +integration is a software engineering term coined in 1999. Two popular +CI services, Hudson and Teamcity, seem to have been released roughly +around 2006 and now Travis have come along to simplify continuous +integration by hosting the actual build workers. The reason that hosted +CI for open source projects took longer to appear could be that more +expensive computer resources are required (computation instead of storage), also +the sandboxing technology and such might have made it more complicated. +In either case, Travis is free for us but it does cost money, therefor a +donating +option is available.

Setting up the build steps

Luckily, there is almost nothing you need to know when setting up your +Jenkins jobs or your Travis configuration. For Jenkins, just have a +"Execute shell" step where you enter the commands you use to build your +package, typically something like cabal configure && cabal install. +For Travis the default steps work for most projects, that is, you only +need to specify that you use Haskell.

CI for Yesod sites

A Yesod site package can use +the general Haskell setup. If you happen to use the +yesod-platform +meta package, you will need this +trick +in your build step to ensure that the meta package gets built before the +other dependencies.

Conclusion -- which CI software should I use?

If your project is open on GitHub and have few dependencies and get +a lot of pull requests, then Travis is ideal for you. However, for a +closed project not using Git in a company that already has a Jenkins +cluster, Jenkins fits better.

There are of course other CI services as well. Some with Haskell +support! For instance, somebody have created a cabal +plugin for +TeamCity.

What's next?

The only thing left now is for the Haskell community to slowly start to +adopt continuous integration in open source projects wherever possible. +I've started slow +myself. If you use GitHub, pick any of your Haskell projects, register +it for CI at Travis, add your +.travis.yml +file and add the build status +image at the top +of your README. It will be appreciated by the community!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/jqplot-widget.html b/public/blog/2012/10/jqplot-widget.html new file mode 100644 index 00000000..b4f6872e --- /dev/null +++ b/public/blog/2012/10/jqplot-widget.html @@ -0,0 +1,180 @@ + Yesod composability: a jqPlot widget +

Yesod composability: a jqPlot widget

October 4, 2012

GravatarBy Michael Snoyman

I think we're overdue on having a new example Yesod application. This time, I +want to demonstrate reusable components, specifically Widgets.

I saw a blog post about Yesod's +composability +recently. My takeaway from the post is twofold:

  1. It's not clear to some users how to create reusable components in Yesod.

  2. We're lagging behind systems like ASP.NET by having a smaller ecosystem of +reusable components readily available.

I think it's too easy to misinterpret these two points as saying the Yesod is +missing out on features to produce reusable components. As I mentioned in a +previous post, +there are three main approaches to creating reusable components in Yesod: +creating general purpose functions (not Yesod-specific), widgets, and subsites.

Of the three, I think the widgets are currently the area we should focus on the +most. They provide the ability to create collections of HTML, CSS, Javascript, +and external dependencies that can be placed in other pages. One way in which +we take advantage of this in the core Yesod packages is to allow form fields to +register Javascript components, such as a jQuery UI datepicker. In this post, +I'd like to give another concrete example, inspired by Steve's blog post: +charts.

Note: the code used in this post is available as a Github +gist. There might be many differences between +the version on the blog and the version in the gist.

Jqplot.hs

{-# LANGUAGE OverloadedStrings, TemplateHaskell, RecordWildCards #-}

We're going to provide a module that provides a pure-Haskell interface to +the jqplot library. To use it, you don't need to write any HTML, CSS, or +Javascript in your code, as we'll see in our example app.

The idea is that all of the low-level Javascript code goes in this module, +and apps just deal with the widget.

Note that, for simplicity, we're only implementing a tiny subset of jqPlot's +full functionality. A more full-fledged wrapper could be written, but would +obviously be more involved.

module Jqplot
+    ( YesodJqplot (..)
+    , YesodJquery (..)
+    , PlotSettings (..)
+    , plot
+    ) where
+
+import Yesod
+import Yesod.Form.Jquery (YesodJquery (..))
+import Data.Monoid ((<>))
+import Data.Text.Lazy.Builder (toLazyText)
+import Data.Aeson.Encode (fromValue)
+import Data.Aeson (ToJSON (toJSON))
+import Data.Text (Text)
+import qualified Data.Text.Lazy
+import Text.Julius (juliusFile)

This is just a minor utility function which renders values to JSON text. +jqPlot, like many Javascript libraries, allows its functions to take +arguments as JSON data. This function could in theory be provided by aeson.

encodeText :: ToJSON a => a -> Data.Text.Lazy.Text
+encodeText = toLazyText . fromValue . toJSON

We need to be able to find the jqPlot Javascript files. By putting the +location in a typeclass, users are able to provide whichever location they +want (e.g., on a local webserver) or use the default value, which in this +case uses the jsdelivr CDN.

Note that the YesodJquery typeclass, provided by the yesod-form package, works the same way.

class YesodJqplot master where
+    jqplotRoot :: master -> Text
+    jqplotRoot _ = "http://cdn.jsdelivr.net/jqplot/1.0.4/"

Now we begin our Haskell API. For a plot, we'll have three settings: the +label for the X and Y axes, and the datapoints to be plotted.

data PlotSettings = PlotSettings
+    { psXLabel :: Text
+    , psYLabel :: Text
+    , psData :: [(Double, Double)]
+    }

This function is the meat of our module. It takes the PlotSettings and +turns it into a Widget. Note the type signature here: we're using GWidget +with arbitrary subsite and master site, so that this widget will work with +many different applications. However, since we need to know the location of +the jQuery and jqPlot libraries, we have the relevant typeclasses in the +context.

plot :: (YesodJquery master, YesodJqplot master) => PlotSettings -> GWidget sub master ()
+plot PlotSettings {..} = do

Grab the master site...

    master <- lift getYesod
+    let root = jqplotRoot master

so that we can add dependencies to the widget. Note that the calling app +will automatically inherit these dependencies, and can remain completely +ignorant of what's going on inside this function. Yesod will also ensure +that each file is only included in the page once.

    addScriptEither $ urlJqueryJs master
+    addScriptRemote $ root <> "jquery.jqplot.min.js"
+    addScriptRemote $ root <> "plugins/jqplot.canvasTextRenderer.min.js"
+    addScriptRemote $ root <> "plugins/jqplot.canvasAxisLabelRenderer.min.js"
+    addStylesheetRemote $ root <> "jquery.jqplot.min.css"

We need to give a unique ID to a div tag where the chart will be placed. +We ask Yesod to provide a unique identifier and then both the Julius and +Hamlet templates are able to use it. This avoids two problems: typos +between the two files, and name collisions when using the same widget +twice in a page.

    divId <- lift newIdent

And as a standard best practice, we've placed the Julius and Hamlet in +separate files.

    toWidget $(juliusFile "jqplot.julius")
+    $(whamletFile "jqplot.hamlet")

jqplot.julius

Here's the important point to make: when you're creating a widget, you have +to write Javascript. In many ways, you can consider this like writing an FFI +binding: it's low level and not type safe, but once you've written it properly, +you can interact fully from within Haskell-land.

In our case, here's the Julius file that produces the relevant Javascript.

$(function(){
+  $.jqplot('#{divId}', [#{encodeText psData}], {
+      series:[{showMarker:false}],
+      axes:{
+        xaxis:{
+          label:#{encodeText psXLabel}
+        },
+        yaxis:{
+          label:#{encodeText psYLabel},
+          labelRenderer: $.jqplot.CanvasAxisLabelRenderer
+        }
+      }
+  });
+});

jqplot.hamlet

The HTML is incredibly simple: it just creates a dummy div tag with the correct id.

<div id=#{divId}>

That can also be written as:

<div ##{divId}>

But that version looks just a bit more confusing to me.

jqplot-example.hs

Finally, let's use this library!

{-# LANGUAGE OverloadedStrings, MultiParamTypeClasses, TemplateHaskell, QuasiQuotes, TypeFamilies #-}
+import Yesod
+import Jqplot

Let's kick off a basic Yesod app with three routes: a homepage and two +charts.

data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/chart1 Chart1R GET
+/chart2 Chart2R GET
+|]

Next we create typeclass instances. We can use the default methods for +YesodJquery and YesodJqplot and take advantage of the built-in CDNs, or +could override and use a local copy instead. For simplicity, we choose the +former.

instance Yesod App
+instance YesodJquery App
+instance YesodJqplot App

Basic handler, nothing special.

getHomeR :: Handler RepHtml
+getHomeR = defaultLayout $ do
+    setTitle "Homepage"
+    [whamlet|
+<p>Demonstration of a reusable widget.
+<p>
+    <a href=@{Chart1R}>First chart
+    |
+    <a href=@{Chart2R}>Second chart
+|]

In this chart, we create a graph of the x² function, without writing a +single line of HTML, CSS, or Javascript. In theory, as more widgets become +available in the Yesod ecosystem, this type of development could become more +standard. But for now, the following example is more normative.

getChart1R :: Handler RepHtml
+getChart1R = defaultLayout $ do
+    setTitle "Graph of x²"
+
+    plot PlotSettings
+            { psXLabel = "x"
+            , psYLabel = "x²"
+            , psData = map (\x -> (x, x * x)) $ [0, 0.1..10]
+            }

Very often, you'll want to take some precomposed widget and embed it +inside some other HTML. That's entirely possible with widget interpolation.

getChart2R :: Handler RepHtml
+getChart2R = defaultLayout $ do
+    setTitle "Made up data"
+    toWidget [lucius|
+.chart {
+    width: 500px;
+    height: 300px;
+}
+|]
+    [whamlet|
+<p>You can just as easily embed a reusable widget inside other source.
+<div .chart>^{madeUpData}
+<p>And it just works.
+|]
+  where
+    -- Yup, totally bogus data...
+    madeUpData = plot PlotSettings
+        { psXLabel = "Month"
+        , psYLabel = "Hackage uploads"
+        , psData =
+            [ (1, 100)
+            , (2, 105)
+            , (3, 115)
+            , (4, 137)
+            , (5, 168)
+            , (6, 188)
+            , (7, 204)
+            , (8, 252)
+            , (9, 256)
+            , (10, 236)
+            , (11, 202)
+            , (12, 208)
+            ]
+        }

And now we just run our app!

main :: IO ()
+main = warpDebug 3000 App

Conclusion

I think widgets are one of the best features in Yesod (I think I rank it at #2, +right behind type-safe URLs). I haven't seen any other framework provide this +kind of packaging of HTML, CSS, and Javascript together.

I think one of the reasons for this is that Haskell's type system makes the +implementation so straight-forward. The contents of a Widget form a Monoid, +and the Widget itself is essentially a WriterT transformer sitting on top +of the standard Handler monad. That means we can run arbitrary code from +inside our Widget, such as pulling out the last five blog entries for a +"recent activity" widget.

I'm very curious to see how the other Haskell web frameworks approach this +problem. If the approaches are similar enough, I think it would be worth +investigating the possibility of creating a more universal widget system, so +that widgets could be shared among various Haskell frameworks. If anyone's +interested in working on this with me, please be in touch.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/keter-updates.html b/public/blog/2012/10/keter-updates.html new file mode 100644 index 00000000..67e6097a --- /dev/null +++ b/public/blog/2012/10/keter-updates.html @@ -0,0 +1,49 @@ + Keter updates +

Keter updates

October 25, 2012

GravatarBy Michael Snoyman

It's been a while since I wrote a post about the status of Keter, and there +have been a number of important changes and feature additions in the past few +months. This post should fill in some gaps.

For those of you not familiar with it, Keter is a web application deployment +manager. You package up your executable, static resources, and a config file, +and Keter will:

  • Run and monitor your server process.
  • Assign a port number to your app.
  • Handle virtual hosting to route requests to the right app.
  • Start running new versions of your app as they are providing, and atomically +switch which app is proxied to.

The Keter README +file has full +instructions on getting up-and-running with Keter, and the code itself can be +installed from Hackage. To get up-and-running quickly on Ubuntu-based systems, +you can run:

wget -O - https://raw.github.com/snoyberg/keter/master/setup-keter.sh | bash -ex

Keter is designed to work with any web application, the only requirement is +that the app answer HTTP requests on the PORT environment variable provided to +it. It is completely agnostic to your choice of development language and +toolset.

As far as notable features added in the past few months:

  • Removed Nginx dependency. Keter itself provides its own reverse proxying +system via +http-reverse-proxy. It +uses that package's raw proxy system, which is able to get by with a +substantially reduced overhead versus a full-blown proxy server.

    Moving away from Nginx has simplified the deployment process, and according +to user reports improved response times. (Note: I have not performed any +significant benchmarking to back that up, the reports are anecdotal.)

  • Using Vincent Hanquez's tls +package and the +network-conduit-tls +wrapper, Keter now fully supports secure connections. In practice, you can +really only have one secure domain per site (due to the normal restriction of +needing a separate IP address per SSL host). The README file shows how to +configure this setup.

  • An application can listen on multiple hosts at the same time. It will still +maintain a single canonical host for setting the APPROOT environment +variable.

  • Static file hosting is built-in. Each application can define zero or more +host-folder mappings. Under the surface, this uses Warp and wai-app-static, +providing efficient file serving and caching features out of the box.

  • Hostname redirects are built in. The main use case for this is +automatically redirecting from mysite.com to www.mysite.com (or the +reverse, if you prefer that).

  • setuid support built-in. If you set a setuid value in the config file, all +processes will be run as that user and project files owned by that user.

Keter is being used in the wild in a number of places, including this site. The +Keter instance is actually hosting four different applications, all on an +Amazon EC2 micro instance, so the overhead is not very high. If you're looking +for a tool to simplify deployment of your web applications, try Keter.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/measuring-warp.html b/public/blog/2012/10/measuring-warp.html new file mode 100644 index 00000000..d6032db7 --- /dev/null +++ b/public/blog/2012/10/measuring-warp.html @@ -0,0 +1,16 @@ + Measuring the performance of Warp +

Measuring the performance of Warp

October 16, 2012

GravatarBy Kazu Yamamoto

This is sixth article for the series of "Improving the performance of Warp". +Readers are supposed to read the following articles:

  1. Improving the performance of Warp
  2. Sending header and body at once
  3. Caching file descriptors
  4. Composing HTTP response headers
  5. Avoiding system calls

In this article, I will finally show you the results of "ping-pong" benchmarks.

Measuring one worker

My benchmark environment is as follows:

  • One "12 cores" machine (Intel Xeon E5645, two sockets, 6 cores per 1 CPU, two QPI between two CPUs)
  • Linux version 3.2.0 (Ubuntu 12.04 LTS), which is running directly on the machine (i.e. without a hypervisor)

Target web servers are as follows:

  • mighty 2.7.1: Mighty without performance improvements described in this series
  • mighty 2.8.2: Mighty with performance improvements described in this series
  • nginx 1.2.4: the current stable version of nginx
  • acme-pong: the pong program included in the acme-http package

Mighty 2.8.1 includes all improvements which I descried so far except the http-types hack. "acme-pong" is not a practical web server. It is a reference implementation to determine the upper-bound on Haskell web-server performance.

I tested several benchmark tools in the past and my favorite one is httperf. I used httperf for the benchmark in this article:

httperf --hog --num-conns 1000 --num-calls 100 --rate 100000 --server 127.0.0.1 --port 8000 --uri /

This means that 1,000 HTTP connections are established and each connection sends 100 requests. To my experience, we can think that "rate 100,000" is the maximum request rate.

For all requests, the same index.html file is returned. I used nginx's index.html whose size is 151 bytes. As "127.0.0.1" suggests, I measured web servers locally. I should have measured from a remote machine but I don't have suitable environment at this moment.

httperf is a single process program using the select() system calls. So, if target web servers can utilize multi cores, it reaches its performance boundary. Andreas suggested weighttp to me as an alternative. It is based on the epoll system call family and can use multiple native threads. I used weighttp as follows:

weighttp -n 100000 -c 1000 -t 3 -k http://127.0.0.1:8000/

Like httperf's case, 1,000 HTTP connections are established and each connection sends 100 requests. Additionally, 3 native threads are spawn.

Since Linux has many control parameters, we need to configure the parameters carefully. You can find a good introduction about Linux parameter tuning in ApacheBench & HTTPerf.

I carefully configured both Mighty and nginx as follows:

  • using file descriptor cache
  • no logging
  • no rate limitation

Here is the result of benchmarks for one worker:

Fig1: throughput for one worker

Y-axis means throughput whose unit is requests per second. Of course, the larger, the better. What I can see from this results are:

  • Mighty 2.8.2 is better than Mighty 2.7.1
  • nginx has better throughput than Mighty 2.8.2. The result of weighttp and that of httperf are very different. I don't know why.
  • acme-pong suggests there would be more room that we can improve the performance of Mighty/Warp.

Measuring multiple workers

Since multi-cores is getting more popular, benchmarks for multiple workers would be interesting. Due to the performance limitation, we cannot use httperf for this purpose. Since my machine has 12 cores and weighttp uses three native threads, I measured web servers from one worker to eight workers(to my experience, three native thread is enough to measure 8 workers). Here is the result:

Fig2: throughput for multiple workers

X-axis is the number of workers and y-axis means throughput whose unit is requests per second. "mighty 2.8.2" (blue) increases workers using the prefork technique while "mighty 2.8.2 -Nx" (orange) increases capabilities with the "-RTS -Nx" option. My observations are:

  • nginx does not scale if the number of workers is larger than 5.
  • Mighty 2.8.2 with the prefork technique does scale at least for 8 workers
  • Increasing capabilities of GHC's threaded RTS cannot utilize multi cores if the number of capabilities is larger than 3.

Also, Andreas and I measured Mighty 2.8.2 with Andreas's parallel IO manager. We had very good results but I don't think it is the proper time to show the results. I hope we can open our results in the future.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/yesod-fay-js.html b/public/blog/2012/10/yesod-fay-js.html new file mode 100644 index 00000000..c9ce26c2 --- /dev/null +++ b/public/blog/2012/10/yesod-fay-js.html @@ -0,0 +1,198 @@ + Yesod, AngularJS and Fay +

Yesod, AngularJS and Fay

October 30, 2012

GravatarBy Michael Snoyman

It's been a while since I discussed Yesod's approach to client-side +programming. I haven't been quiet due to a lack of interest. On the contrary, +I've been playing around with a lot of different approaches, and discussing +things with a number of people as well. Additionally, there have been some very +exciting changes in the Haskell Javascript space.

Back when we discussed this in +April, I demonstrated a +combinator-based approach for generating Javascript from our Haskell code in a +type-safe manner. After playing around with this a bit, I decided it was too +inconvenient to be a focus of efforts. I've kept that code, together with some +other attempts I've made, in a yesod-js repo on +Github. If you're interested in the +process I went through to come to the ideas I'm stating in this blog post, you +might want to look there. Otherwise, the remainder of this post will be talking +about future plans exclusively.

So the big question is: what do we want from a client-side solution? For me, it +boils down to:

  1. A better language than Javascript. Ideally, I want the same level of static +type checking in my client-side code as my server-side code. Having a better +syntax than Javascript is a nice bonus, but not the main point for me.

  2. Tight server/client integration. I'd like automatic serialization of data +between server and client. Or let's take it a step further: it would be +great if it's almost transparent to the client-side code if we're calling a +function locally or remotely.

    By "almost transparent", I mean we should still keep asynchronous calls +asynchronous. In an ideal world, we could have a powerful multithreaded runtime +like GHC provides. But dealing with explicit callbacks is acceptable IMO.

  3. We need to solve the big problems in client-side development as well. In +other words: even if we could use GHC on the client side and compile any +arbitrary piece of Haskell, we'd still have problems to solve: the DOM API is +a mess, creating interactive UIs requires a lot of work, etc.

  4. We should not reinvent the wheel; we can use existing Javascript libraries +when they're available. This might sound obvious, but I don't think it is +(I'll give a concrete example later). There is a huge amount of effort going on +into making client-side development better, and by simply barricading ourselves +in our Haskell bubble, we'll be missing out on a lot of improvements out there.

    This is actually a more general Yesod philosophy: we should stick to +tried-and-true techniques as much as possible, and simply make them better by +adding in Haskell's strengths. An additional benefit of this is the reduced +learning curve for developers coming from another language. (Or, conversely, +the fact that you can reuse your Yesod skills when working with other +languages.)

AngularJS

We can solve all four of these issues with a two-pronged approach. Starting +with issues (3) and (4): we piggy-back on an existing, well-designed library in +the Javascript world to solve the big problems. After some research and a lot +of recommendations, I mocked up a demo using AngularJS. I have to say, I'm +quite impressed. I won't by any means claim to be an expert, and I'm certainly +not completely sold that it's the One True Path, but it does solve a lot of +problems in an elegant manner.

You can see my sample code on +Github. +For this demo, I wrote the Javascript code as simple Javascript. +One important trick I used here is providing a set of commands. For +example:

cmdGetPeople <- addCommand $ \() -> do
+    people' <- getYesod >>= liftIO . readIORef . ipeople
+    return $ map (\(pid, Person name _) -> PersonSummary pid name) $ Map.toList people'

Each command takes a single JSON value as input, and returns a single JSON +value as output. This is an important simplification over the standard approach +of passing separate parameters: we can more clearly define our API, as will +become important when coming to type safety. Calling this from Javascript is +simple:

function($scope, $http) {
+    $http.post("#{cmdGetPeople}", []).success(function(data) {
+        $scope.people = data;
+    });
+}

Note: If you don't have experience with AngularJS, some of the code in here may not make much sense. Don't worry too much about that for now, I'm hoping to give more information about Angular in future blog posts, as the Yesod solutions for interacting with it mature.

The addCtrl Template Haskell function automatically includes the people.hamlet and people.julius files. cmdGetPeople is an automatically generated unique textual identifier given for the command defined.

Note, however, that there is no type safety in this approach.

I would classify AngularJS as giving us some level of reactive programming. We +update variables, and the views update themselves automatically. It doesn't +have all the power of a proper FRP solution (like reactive-banana or netwires), +and it uses a bit of a hack- efficient dirty checking- to get it working. So +for a long time I was opposed to using Angular: why use a hacky, suboptimal +solution to a problem when we have a beautiful solution just waiting to be +refined?

And the answer is simple: Angular is ready to be used now. I still believe that +FRP will give us a better result in the end, and I hope the Haskell community +continues to develop FRP solutions, and bring them into the client-side realm. +But in the meanwhile, using Angular gives us a huge amount of the benefits +we're looking for, while staying with mainstream web development.

Fay

The problem with this demo is that we're still programming everything in +horrible, ugly, unsafe Javascript. I originally thought to overcome this with a +combinator approach, but as I mentioned I think combinators will be too awkward +overall.

After playing around with some alternatives, I tried out +Fay, and I'm very impressed. It doesn't provide quite +the full Haskell experience, but for most websites I think it's offering the +right trade-off. The generated code, while not idiomatic Javascript, is still +close enough to the source to be recognizable.

One huge selling point of Fay is its Foreign Function Interface (FFI). As an +example, in Fay the alert function can be called via:

alert :: String -> Fay ()
+alert = ffi "window.alert(%1)"

This simplicity makes it trivial to interact with existing libraries, such as +jQuery. And as a result, I think Fay can be used as a drop-in replacement for +arbitrary Javascript code. To test out that theory, I started converting a +small project I've been writing to use Fay.

(For those of you wondering: this site is just a tool I built for my wife to +sort our family photos. It's never seen the light of day before.)

You can view the commit in +question +to see exactly what I did. The goal was to replace one Julius file (incoming.julius) with a Fay +file (Incoming.hs). +Just to give a small test of the advantages of such a switch, compare this AJAX Javascript call:

$.ajax({
+    url: "@{AddPostR}",
+    data: {
+        date: $(this).parent().children("input[type=date]").val(),
+        slug: $(this).parent().children("input[type=text]").val()
+    },
+    dataType: "json",
+    success: function() {
+        window.location.reload();
+    },
+    error: function () {
+        alert("Some error occurred");
+    },
+    type: 'POST'
+});

With this Fay type-safe callback:

t <- eventSource e
+date <- parent t >>= childrenMatching "input[type=date]" >>= getVal
+slug <- parent t >>= childrenMatching "input[type=text]" >>= getVal
+call (AddPost date slug) $ const reload

Build Process

One problem people face is how to integrate Fay into your project's build +process. Fay files are valid Haskell, and should be compiled with GHC to +type-check them. Afterwards, they can be compiled by Fay into Javascript. +However, I've gotten spoiled by Julius, and wanted to have automatic code +reloading during development.

To address this, I created two Template Haskell functions. +fayFileProd +compiles with GHC, erroring out if the compile fails. Then, if that's +successful, I use the Fay library to compile to Javascript. If that succeeds, I +return a Javascript value, the same as Julius would have produced.

The other function is +fayFileReload. +Instead of compiling to Javascript at compile time, it performs the compilation +at runtime, thereby automatically reloading any changes. The Fay compiler +itself runs incredibly quickly, so there's no noticeable lag. And if you have +errors in your Fay file, they show up in the browser:

Fay error message

Then there's a helper function called fayFile which will use fayFileProd +during production builds and fayFileReload when using yesod devel. This won't +catch type errors during development builds, but for me that's an acceptable +trade-off: we get very fast response times, and our production builds are +guaranteed to be type-safe. If you really want, you can manually run ghc to +type-check your code.

I used +fayFile +just like I'd use widgetFile. No modifications to the build system necessary.

File structure

If you looked at the code above, you might have noticed some special folders. +What I've set up is to subfolders: fay-shared is for modules used by both +client- and server-side code (see the next section about this). The fay +folder is for client-side-only code. My plan is to modify the yesod devel +file checker to ignore file changes in the fay folder, thereby avoiding code +reload when you're just playing around with client-side code.

Commands

In the code I gave for Angular above, I mentioned the concept of commands. It +turns out that Chris came up with almost exactly the same approach for getting +Fay to interact with the server. I've adopted his approach almost verbatim in +this example.

In fay-shared, there's a single module which defines a Command +datatype. +This represents all of the commands that can be sent to the server, along with +the results of such calls. In the +Handler.Command +module, I handle these requests on the server. The handle function is +general-purpose, and could be put into a yesod-fay package. In +handleCommand, I've written handler functions for each command. The type +system ensures that the response we send back is the one the client is +expecting.

Calling a command from the +client +is even easier. For example:

call (AddPost date slug) $ const reload

What this says is "Call the AddPost command with date and slug parameters, and +when the call returns, I want you to ignore the response value and reload the +page."

If you compare the original +Javascript +to the new Fay +code, +they're basically the same structure. Both are using jQuery and registering +callbacks for events. Both are making AJAX calls to the server. But personally, +I much prefer the Fay code: it's easier to read (as a Haskeller), and the AJAX +calls are completely type-safe.

Why not ghcjs?

I played around with ghcjs. It's a truly amazing project: it allows you to +compile virtually any GHC-understood code to Javascript. However, there are a +few reasons I've decided to go with Fay for the moment:

  1. There's a difficult installation process for ghcjs, involving a modified +version of GHC. Over time, I believe that this will be simplified as patches +get merged upstream, but for many users it would be too difficult a bar to +overcome.

  2. One of Fay's great features is its dead-simple FFI. By contrast, ghcjs has +the traditional FFI, which makes more difficult to interact with Javascript +libraries.

  3. Currently, the generated code won't run on Internet Explorer. I know +there's work in the pipeline to address this, so longterm it shouldn't be a +problem.

So if you read between the lines here, what I'm really saying is, "ghcjs isn't +the right solution today, but it might be soon." It's a truly amazing effort, +and I'm looking forward to where it heads. I think there's a lot of room for +Fay and ghcjs to complement each other: Fay being a simpler tool for simple +tasks, and ghcjs being the "big guns" when you need more power.

And Elm?

Another solution in this space which is developing nicely is +Elm. It provides a client-side FRP solution, and with +elm-yesod there's already the +ability to integrate nicely with Yesod.

However, as much as Elm is influenced by Haskell, it's still a separate +language. This may not be a problem for many cases, and if you think Elm would +be the right fit, you should definitely give it a shot.

Next steps

I sketched out four problems I'm hoping to solve. As it stands, I think Fay +solves the first two, and Angular solves the second two. So the next obvious +step would be to integrate these two approaches. Initially, this looks pretty +simple: using Angular required writing some Javascript code. Now we'll just +write Fay code.

Fay is still a very young project, and therefore there are still some obstacles +to overcome. In the process of writing this code, I came up with the following +observations:

  • We really need to have some package management system for Fay. I'm not too +concerned with exactly what this would look like, but it's important to be +able to share common bindings like jQuery.

  • When sharing datatypes between the server and client, I'm forced to use +String instead of Text (there are likely a few other examples of this +too). It would be nice if Fay could automatically treat Text as a synonym for +String, while for the server-side code use Data.Text.Text.

  • Chris's example code has a getThis function for retrieving the this +object in Javascript. Unfortunately, due to how function calls work in Fay, +this is almost meaningless. In particular, this does not point to the +clicked DOM element when using a jQuery event handler. I worked around this by +pulling the information out of the event object itself, but it would be nice if +we could somehow recover this.

  • A very minor point: CompilerState returns a list of modules which have been +parsed. If it could also provide the FilePaths these were parsed from, then +I could register those files as dependencies in the Template Haskell calls.

But these are all relatively minor points. The fact that, without too much +difficulty, I could convert some live Javascript code into Haskell code is very +exciting. Maybe 2013 will be the year of Haskell on the browser :).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/yesod-pure.html b/public/blog/2012/10/yesod-pure.html new file mode 100644 index 00000000..b24c2c29 --- /dev/null +++ b/public/blog/2012/10/yesod-pure.html @@ -0,0 +1,250 @@ + yesod-pure: Use Yesod without Template Haskell +

yesod-pure: Use Yesod without Template Haskell

October 15, 2012

GravatarBy Michael Snoyman

Thank you very much to everyone who has participated so far in the Yesod User +Survey 2012, the +response was even stronger than I'd hoped for, and the feedback has been very +helpful.

Two of the questions on the survey had to do with our usage of Template Haskell +(TH) and Quasi Quotes (QQ) in Yesod. My goal was to determine whether their +usage was something that impacted people across the board, as opposed to a +specific segment of the Haskell population, and therefore whether it was worth +putting in the effort to create and maintain a non-TH package.

Much to my surprise, the feedback seemed to be spread completely evenly across +the board. A solid 30% of people across the board agreed that there was too +much TH and QQ in Yesod. So in this blog post, I'd like to announce and +describe a new, slightly experimental package called +yesod-pure that allows you to +use Yesod without any code generation.

(Note: The name yesod-pure is open for discussion, it's the first one I +came up with, but if people have suggestions, I'd love to hear them.)

Note: I'm trying a slightly new format for displaying this information. All of +the code is in a Github +gist, +and each step is a separate commit. I'd appreciate feedback on whether this +format is helpful or not.

(Side note: I put together a demonstration called +yesod-alternative +a while ago showing how Heist and acid-state could be used with Yesod. That +effort could in theory be merged with yesod-pure to some extent if there is +interest.)

Step 1: A normal Yesod app

Before we can just get rid of TH and QQ, we need to determine what we're +getting rid of. Yesod uses them in three different places:

  • Routing
  • Shakespearean templates
  • Persistent

For our current purposes, I'm going to completely ignore Persistent. It's not +directly part of Yesod-the-framework, and already has its TH components in a +completely separate package. We might add some support for Persistent in the +future.

As a motivating example, let's create a Yesod application the normal way. It's +a simple app: a homepage with a type-safe link, and a separate route for +Fibonacci numbers. You can see the +code.

In this example, we've used mkYesod and parseRoutes to deal with the +routing, whamlet for generating HTML, and lucius for generating CSS.

Step 2: No more Shakespeare

Let's take the low-hanging fruit: removing Shakespeare. This is actually +relatively easy. Hamlet is built on top of the +blaze-html library, so we can +just replace Hamlet with Blaze's combinators. For CSS, we're going to use plain +text fed into yesod-pure's addCSS function.

Full source code

@@ -3,7 +3,10 @@
+ {-# LANGUAGE QuasiQuotes           #-}
+ {-# LANGUAGE TemplateHaskell       #-}
+ {-# LANGUAGE TypeFamilies          #-}
+-import           Yesod
++import           Text.Blaze.Html             (toValue, (!))
++import qualified Text.Blaze.Html5            as H
++import qualified Text.Blaze.Html5.Attributes as HA
++import           Yesod.Pure
+ 
+ data App = App
+ 
+@@ -17,19 +20,21 @@ instance Yesod App
+ getHomeR :: Handler RepHtml
+ getHomeR = defaultLayout $ do
+     setTitle "Hello World!"
+-    toWidget [whamlet|
+-<p>Hello World
+-<a href=@{FibR 5}>Fifth fib
+-|]
+-    toWidget [lucius|p { color: red }|]
++    toWidget $ \render -> do
++        H.p "Hello World"
++        H.a ! HA.href (toValue $ render (FibR 5) []) $ "Fifth fib"
++    addCSS "p { color: red }"
+ 
+ getFibR :: Int -> Handler RepHtml
+ getFibR i = defaultLayout $ do
+     setTitle "Fibs"
+-    [whamlet|
+-<p>Fib for #{i}: #{fib i}
+-<a href=@{FibR $ i + 1}>Next fib
+-|]
++    toWidget $ \render -> do
++        H.p $ do
++            "Fib for "
++            toHtml i
++            ": "
++            toHtml $ fibs !! i
++        H.a ! HA.href (toValue $ render (FibR $ i + 1) []) $ "Next fib"
+ 
+ fib :: Int -> Int
+ fib i = fibs !! i

One trick to notice is toWidget $ \render -> do. render is a URL +rendering function provided by Yesod. We're able to use this to turn FibR 5 +into a textual representation. This is the very heart of type-safe URLs: +instead of splicing text together, we ask the system itself to generate a URL +from a value known to be correct.

Step 3: Rewrite the routing code

The next part is trickier. We need to write out routing code manually. Let's +spell out the different components of type-safe routing:

  • A data type representing all possible routes in the app.
  • A function that turns a type-safe route into a list of path segments (aka, renderer).
  • A function that tries to turn a list of path segments into a type-safe route (aka, parser).
  • A function that takes a type-safe route and dispatches to the appropriate handler code (aka, dispatcher).

In our previous code, all four pieces were being generated automatically by +mkYesod and parseRoutes. Now we'll write them all our separately.

Full source code

@@ -1,8 +1,7 @@
+ {-# LANGUAGE MultiParamTypeClasses #-}
+ {-# LANGUAGE OverloadedStrings     #-}
+-{-# LANGUAGE QuasiQuotes           #-}
+-{-# LANGUAGE TemplateHaskell       #-}
+ {-# LANGUAGE TypeFamilies          #-}
++import           Control.Applicative         ((<$>))
+ import           Text.Blaze.Html             (toValue, (!))
+ import qualified Text.Blaze.Html5            as H
+ import qualified Text.Blaze.Html5.Attributes as HA
+@@ -10,12 +9,28 @@ import           Yesod.Pure
+ 
+ data App = App
+ 
+-mkYesod "App" [parseRoutes|
+-/ HomeR GET
+-/fib/#Int FibR GET
+-|]
++instance RenderRoute App where
++    data Route App = HomeR
++                   | FibR Int
++        deriving Eq
++    renderRoute HomeR = ([], [])
++    renderRoute (FibR i) = (["fib", toPathPiece i], [])
++
++parseRoute :: RouteParse App
++parseRoute [] = Just HomeR
++parseRoute ["fib", i] = FibR <$> fromPathPiece i
++parseRoute _ = Nothing
++
++dispatchRoute :: RouteDispatch App
++dispatchRoute "GET" HomeR = handler getHomeR
++dispatchRoute "GET" (FibR i) = handler $ getFibR i
++dispatchRoute _ _ = Nothing
++
++instance YesodDispatch App App where
++    yesodDispatch = dispatch parseRoute dispatchRoute
+ 
+ instance Yesod App
++type Handler = GHandler App App
+ 
+ getHomeR :: Handler RepHtml
+ getHomeR = defaultLayout $ do

Notice that first change: we're no longer using the TemplateHaskell and +QuasiQuotes language extensions!

We've added a RenderRoute instance. The Route associated data type provides +all of the route constructors for our application, and renderRoute turns each +route into a list of path segments and a list of query string parameters. Every +Yesod app has this instance, it's just usually generated for you.

parseRoute is the inverse of renderRoute. In order to keep your +applicationn correct, you must ensure that these functions are always exact +inverses of each other. This is the biggest advantage we're losing by switching +away from TH. (The other big advantage we lose is brevity of code, but we'll +come back to that in step 4.)

Finally, we dispatch our application. We can see that we're only going to +respond to GET requests, though if you wanted to handle other requests it would +be trivial to add more clauses.

And we finally tie it all together with the YesodDispatch instance. This is +another instance which exists in every Yesod application, but isn't normally +visible. The dispatch function is provided by yesod-pure, and is just a +helper to simplify creating the yesodDispatch function. The latter is fairly +complicated, involving lots of parameters for dealing with subsites, 404 and +405 handlers, and so on. dispatch hides all those details from you.

Otherwise, our code remains unchanged. Type-safe URLs still work exactly as +before, and we run our code as previously. That should make sense: all we've +done is switched from an automated code generation to a manual process, but the +end result is almost identical. (The TH code is actually includes a few +performance enhnacements for the routing process, but that's not really +important for our purposes.)

Step 4: Lose the type safety

The previous step let you create Yesod applications as you normally would by +manually writing some code. That approach let us keep all of our type-safe +features from Yesod, at the cost of writing a fair amount of boilerplate. But +suppose we'd rather give up some type safety in exchange for simpler code. +That's an option too.

Having a type-safe URL datatype means that we need to have all four components +listed above: a datatype, parsing, rendering, and dispatching. But if we drop +the URL datatype, we can get away with only dealing with dispatch, thus +simplifying our code significantly. That's the purpose of the +Yesod.Pure.NoRoute module, which provides routing combinators to automate +dispatch.

Full source code

@@ -1,43 +1,24 @@
+ {-# LANGUAGE MultiParamTypeClasses #-}
+ {-# LANGUAGE OverloadedStrings     #-}
+ {-# LANGUAGE TypeFamilies          #-}
+-import           Control.Applicative         ((<$>))
++import           Control.Applicative         ((<|>))
+ import           Text.Blaze.Html             (toValue, (!))
+ import qualified Text.Blaze.Html5            as H
+ import qualified Text.Blaze.Html5.Attributes as HA
+-import           Yesod.Pure
+-
+-data App = App
+-
+-instance RenderRoute App where
+-    data Route App = HomeR
+-                   | FibR Int
+-        deriving Eq
+-    renderRoute HomeR = ([], [])
+-    renderRoute (FibR i) = (["fib", toPathPiece i], [])
+-
+-parseRoute :: RouteParse App
+-parseRoute [] = Just HomeR
+-parseRoute ["fib", i] = FibR <$> fromPathPiece i
+-parseRoute _ = Nothing
+-
+-dispatchRoute :: RouteDispatch App
+-dispatchRoute "GET" HomeR = handler getHomeR
+-dispatchRoute "GET" (FibR i) = handler $ getFibR i
+-dispatchRoute _ _ = Nothing
+-
+-instance YesodDispatch App App where
+-    yesodDispatch = dispatch parseRoute dispatchRoute
++import           Yesod.Pure.NoRoute
+ 
+ instance Yesod App
+ type Handler = GHandler App App
+ 
++fibR :: Int -> Route App
++fibR i = AppRoute ["fib", toPathPiece i]
++
+ getHomeR :: Handler RepHtml
+ getHomeR = defaultLayout $ do
+     setTitle "Hello World!"
+     toWidget $ \render -> do
+         H.p "Hello World"
+-        H.a ! HA.href (toValue $ render (FibR 5) []) $ "Fifth fib"
++        H.a ! HA.href (toValue $ render (fibR 5) []) $ "Fifth fib"
+     addCSS "p { color: red }"
+ 
+ getFibR :: Int -> Handler RepHtml
+@@ -49,7 +30,7 @@ getFibR i = defaultLayout $ do
+             toHtml i
+             ": "
+             toHtml $ fibs !! i
+-        H.a ! HA.href (toValue $ render (FibR $ i + 1) []) $ "Next fib"
++        H.a ! HA.href (toValue $ render (fibR $ i + 1) []) $ "Next fib"
+ 
+ fib :: Int -> Int
+ fib i = fibs !! i
+@@ -58,4 +39,6 @@ fibs :: [Int]
+ fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
+ 
+ main :: IO ()
+-main = warpDebug 3000 App
++main = warpDebug 3000 $ App $
++    method "GET" (serve getHomeR)
++    <|> static "fib" (dynamic $ \i -> method "GET" $ serve $ getFibR i)

We've replaced most of our initial code with some dispatch code we've placed in +main. I've used the Alternative interface to these combinators (there's +also a Monoid interface, and an more experimental Monad interface). They +should be mostly self-explanatory, but let me explain:

  • method ensures that the request had the given method.
  • serve uses the given Handler to respond, if there are no path segments left to be processed. So our first line will only respond to requests to the root of our app.
  • static ensures that the next path segment is the given piece of text.
  • dynamic will attempt to read the following path segment using fromPathPiece.
  • multi (not featured here) will read all the remaining path segments with fromPathMultiPiece. This could be combined with serve to overcome the restriction of only serving when there are no remaining path segments.

This route definition should be identical to the ones we've used previously, +look through it carefully to be sure you have the feel. This is very much a +first stab at creating combinators for routing, if anyone has some +recommendations, please let me know (or sent a pull request!).

The final thing to point out is fibR. I lied a bit when I said we were +getting rid of type-safe URLs. There's still a datatype for all of our routes, +but now it's just a list of texts. fibR attempts to recapture some of the +safety of having dedicated constructors like FibR. If your app uses such +wrapper functions exclusively, then you can minimize the potential for invalid +URLs to just those wrapper functions.

Step 5 (there is no step 5)

Another approach I didn't cover here is keeping the Template Haskell but +dropping the QuasiQuoted syntax. You can see an example of that in this +gist.

Another alternative would be to use a system like Boomerang, which still uses +Template Haskell but grants you more control. I'm not an expert in Boomerang at +all, but if someone would like to contribute an example of how to get this app +to work with it, I'd be very interested.

Conclusion

This package is just an initial release, and should still be considered experimental. I'm interested in hearing feedback on how it works. With it, we now have three ways of creating Yesod applications:

  • The standard TH/QQ combination. We get brevity of code, guarantees that rendering and parsing are inverse of each other, and full type safety.
  • The Yesod.Pure approach: code is longer and we lose guarantees that the parsing and rendering is correct, but we retain full type safety.
  • Yesod.Pure.NoRoute: give up on some type safety in exchange for shorter code.

All three approaches allow you to access the full power of the Yesod ecosystem, +produce and consume widgets, deal with JSON data automatically, etc.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/10/yesod-user-survey-2012.html b/public/blog/2012/10/yesod-user-survey-2012.html new file mode 100644 index 00000000..6ce8fcc5 --- /dev/null +++ b/public/blog/2012/10/yesod-user-survey-2012.html @@ -0,0 +1,19 @@ + Yesod User Survey 2012 +

Yesod User Survey 2012

October 10, 2012

GravatarBy Michael Snoyman

It's been a while since we had a good user survey, and it's about time to get +some structured feedback from the community. I'd like to ask anyone who's +interested in Yesod- or Haskell web development in general- to please take the +time to fill it out, your answers will be invaluable in figuring out directions +to pursue moving forward.

Please pass around the link to relevant locations: https://docs.google.com/spreadsheet/viewform?formkey=dFNINUgtbnJNSS1DRGw3bGk4YkVjZlE6MQ

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/11/changes-scaffolding-system.html b/public/blog/2012/11/changes-scaffolding-system.html new file mode 100644 index 00000000..1f63033a --- /dev/null +++ b/public/blog/2012/11/changes-scaffolding-system.html @@ -0,0 +1,63 @@ + Changes to scaffolding system +

Changes to scaffolding system

November 7, 2012

GravatarBy Michael Snoyman

I've just pushed some significant changes to Hackage for the yesod +executable. These changes fall into two categories: an improved yesod devel +experience and better scaffolding. The former is by far the cooler feature, +and will hopefully be covered in another blog post soon. This blog post covers +the second feature.

I have purposely not yet updated yesod-platform to include this new +executable, as I still consider it somewhat experimental. It would be great if +everyone could install this new version and test it out. Doing so should be as +simple as:

cabal update
+cabal install yesod-1.1.3

If you run into any problems, please report them.

Scaffolding

In the past few days, I've pushed a major overhaul to how the Yesod scaffolding +system works. The scaffolded site itself remains mostly unchanged, but the +manner in which the code is stored and generated is completely different.

Under the new system, there is a dedicated scaffolding repo on +Github. There is a separate branch +for each scaffolding option, e.g. +PostgreSQL versus +MongoDB. Throughout +the scaffolding source, the word PROJECTNAME (whether in the file name or file +contents) can be treated as a variable; it will automatically be replaced by +the actual project name specified by the user.

Since each version is its own repo, synchronizing changes is as simple as a +git merge. I consider the PostgreSQL version the master branch, and +merging changes from there to all other versions. Testing is also greatly +simplified: you can just build the code like a normal project, and when you're +done, commit your changes. There's no need to go through an extra "run the +scaffolder" step. Due to this simplification, it's now possible to reinstate a +feature we used to have: a scaffolded site without a +database.

Once we have a set of scaffoldings, we run a special tool based on +project-template (discussed below) to generate a single file containing our +entire scaffolded site. We get one of these files for each version of the +scaffolding (PostgreSQL, MongoDB, etc) and then compile them into the yesod +executable. The yesod executable then uses project-template itself to +unpack those files.

To me, the main advantages of this move are:

  1. It's much easier to test out new ideas: just fork the scaffolding repo.
  2. Likewise, making modifications to the scaffolding is just a pull request away.
  3. It's much easier to maintain drastically different scaffoldings.
  4. We've removed a bunch of hairy code from the yesod executable.
  5. Users can now specify URLs containing scaffolded sites, lowering the barrier to entry for experimentation.

To prove that last point, I've created a test branch for including Fay support +in the scaffolding, and placed the scaffolding file at

https://raw.github.com/gist/4030486/dfe10c7c109c842f9eddd6a2811bfee4f305debe/postgres-fay.hsfiles.

If you run yesod init, take the url option, and provide that address, you +can start to play with yesod-fay right now.

Project Template

As a first step towards the collaborative Haskell IDE project, I +stated I was going to +be working on a project-template library for providing a single format for +different IDEs and scaffolding tools to represent project templates. The +initial version is now ready, and is being used in our codebase at FP Complete. +I'm hoping other IDE projects are able to take advantage of it to.

I initially had lots of grandiose plans for conditional text, conditional +files, etc. As you may have guessed based on this blog post, after careful +consideration, I've decided that such a system is overkill. Instead, I've +elected to go for a very simple file format (see, for example, +postgres-fay.hsfiles +linked above).

This format is just a simple piece of text, with special START_FILE pragmas +to specify where a new file starts. Binary files can be included via +base64-encoding. And that's really it. What's cool here is that it opens the +door for uses beyond creating scaffoldings. For example, I could imagine a +runghc wrapper (or perhaps a feature added to GHC itself) that would +automatically unpack a file into multiple logical subfiles and then run them.

The library is available on +Hackage. Feedback +welcome!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/11/solving-cabal-hell.html b/public/blog/2012/11/solving-cabal-hell.html new file mode 100644 index 00000000..6756abd7 --- /dev/null +++ b/public/blog/2012/11/solving-cabal-hell.html @@ -0,0 +1,157 @@ + Solving Cabal Hell +

Solving Cabal Hell

November 11, 2012

GravatarBy Michael Snoyman

Identifying the problem

In the past few weeks, there has been a flurry of discussion around making +Cabal better. The main issue on the table is Cabal dependency hell. Most +discussions on the topic are based on the premise that if we just "fix Cabal" +then dependency hell will disappear. I want to make a completely different +claim: for the average user, Cabal is already working fairly reliably (though +I'm sure some improvements can be made).

Let's clarify who this "average user" is. I'm not talking about the developer +who's working on some new library to be released to Hackage. Anyone publishing +Haskell libraries is already outside the average range. For that user, powerful +sandboxing utilities are a highly desirable feature. +(By sandbox, I mean the ability to have totally isolated Haskell library builds.) +But for my theoretical +average user, sandboxing is just one extra thing he/she has to learn.

In my world, the average user is someone who has heard about this Haskell +thing, is interested trying out some library (I'll use Yesod as the example +since (1) I know a lot about it and (2) it's a common pain point in Cabal), and +wants to get going as quickly and easily as possible. Sandboxing achieves +neither of those goals: it's not quick, because it requires installing extra +tools and compiling libraries multiple times, and it's not easy because there's +a high cognitive overhead to understanding which tools to use and how to use +them.

In other words: the average developer is a beginner.

So let's disect a common case of dependency hell. (Real life example coming.) +Alice comes along and decides she's going to try Yesod. She installs the +Haskell platform, runs cabal update && cabal install yesod-platform, and gets +everything installed. No problems occur at all. She's developing along, and +decides to test out Fay. So she types in cabal install fay. Assuming this +occured before October 28, she'll run into a conflict: fay depends on +language-ecmascript, which depends on data-default version 0.4. But Yesod +is installed with data-default version 0.5. Instant Cabal Hell!

To clarify: the problem we're facing is entirely about version bounds. This +problem is triggered by Yesod having a version bound which includes +data-default 0.5, while language-ecmascript does not include version 0.5. +Cabal eagerly tries to install Yesod with the most recent version of all +relevant packages that fit the constraints of Yesod, but doesn't know that Fay +will be thrown into the mix later.

So who's at fault here? Let's list the culprits and their alibis:

  • Yesod, for using data-default version 0.5. Can't really blame a package +for using another package. Besides, Yesod will install with any version of data-default, +you would just need to give Cabal a specific constraint to override its default +of using the newest version.

  • language-ecmascript, for putting on restrictive upper bounds. This is a +hot topic for debate in the community: should we preemptively put on upper +version bounds, or not? But let's forget about the preemptive nature for a +moment. What if the authors knew with certainty that their package wouldn't +work with data-default version 0.5? Are we going to blame them for writing a +package that isn't perfectly forwards compatible with all future versions of +dependencies? Certainly not. And can we blame them for not immediately +releasing a new version of their library once data-default was updated? No, +that's not fair either.

    (I've been on the receiving end of such demands. As a package maintainer, +it's just an impossibly high bar to try and reach, and we can't expect it of +anyone.)

  • Cabal, for not just automatically reinstalling Yesod with data-default +version 0.4. Maybe... but that might break a whole bunch of other code. +Hermetic builds might help here, but I'm going to make a bit of a baseless +assertion here: there's no way we could ever create a tool that can efficiently +and correctly handle all such reinstallation cases.

  • Cabal, for installing Yesod with data-default 0.5 instead of 0.4. It +should have known that I'd want to try out Fay next. I think Yoda said it +best, with "Strong am I in the force, but not that strong."

It would seem we've run out of scapegoats... or have we?

Blame Hackage

The real problem is that Hackage is maintaining conflicting packages! How dare +it tell Cabal about Yesod and Fay if they can't coexist. Hackage could simply +reject packages which conflict with existing dependencies, and cull existing +packages which use outdated dependencies.

Hackage has an alibi as well: it's doing exactly the job it's supposed to be +doing! Hackage says nothing about stability of code. It's a place for +developers to upload code. It doesn't have rigorous requirements for entry, +which is a good thing. It encourages experimentation and lets users test +out new ideas easily.

So here's the thesis of my post: All of our tools are working correctly, but +we're using them for the wrong purpose.

Four levels of package stability

Let me describe four levels of stability in packages. The lines are not always +so clear-cut, and therefore it's easy to imagine in-between levels. +Nonetheless, I think this breakdown is useful.

  1. Packages that live in source control. There are no guarantees that the code is +usable in any way, much less that it interoperates with other packages. +There are no clearly defined version numbers either, essentially just +meaningless SHA hashes.

  2. Individual packages that have been released as functional, but not necessarily +guaranteed to play nice with others. The definition of "functional" is very +much up to debate. It could mean anything from "Hey, I came up with this idea +five minutes ago and the code compiles" to "We have a rigorous test suite." +Each package author has his/her own definition of "good to go."

  3. A set of packages that have been vetted as working nicely together. Minimally, +this would mean they all install; ideally, they would pass a set of +integration tests as well. This requires efforts of some trusted group of +people to perform this vetting.

  4. A subset of vetted, interoperable packages +that are recommended for developers to use. This would include +support and documentation.

Hackage is currently providing level 2 stability. Let me reiterate: this is +exactly what Hackage should be doing, and I don't want to change that at all. +The Haskell Platform lives at level 4, providing a small subset of known good +and working packages. But there's nothing sitting at level 3. As a result, the +job of the Haskell Platform team is much harder than it need to be, and users +looking for more power than the HP provides are thrown back into the level 2 +immediately.

My claim is that for the "average developer," stability levels 3 or 4 are far +more valuable. The remainder of this post is a description of how I believe +the community can achieve this goal.

Get a list of target packages

The goal of this project shouldn't be to encompass the entirety of Hackage. For +one, we would almost certainly fail. There are simply cases where no resolution +could ever be achieved (e.g., lens requires transformers 3.0 while expat-enumerator +cannot use transformers 3.0). But this project will require responsive +package maintainers (as we'll see in a bit). So arbitrary packages shouldn't +just be thrown into the mix.

Instead, a developer should have to apply for a package to be included in this +set of target packages. This developer would then be the contact person if any +problems arise. I'll volunteer today as the contact on the yesod package, for +example.

Try to find a compatible set of versions

Now we come back to the upper bounds issue. If we're lucky, all of the packages +included in the set of targets will work with the newest version of the +dependencies available on Hackage. But that may not always be the case. The +simplest response would be to ask maintainers to bump their dependencies. But +some complications will arise:

  1. What about transitive dependencies? In the example I started with, suppose +that Fay was a target package. But the restrictive dependency on +data-default came from language-ecmascript. In this case, I think it is the +responsibility of the Fay maintainer to pursue one of the following:

    • Get the language-ecmascript maintainers to release an updated version +of their package. In many cases, this will be trivial, and happily +accepted upstream.

    • Remove the dependency on language-ecmascript, possibly by forking the +package.

    • Remove Fay from the set of target packages until the situation can be +resolved.

  2. What about massively disruptive releases? One example was the +transformers 0.3 release, which still has some dependency hell remnants. +In that case, however, upgrading was usually a simple matter of adjusting a +cabal file. But a more significant example was the change (about a year ago) +from conduit 0.3 to 0.4. That change required significant code rewrites, and +therefore the transition needed to be handled smoothly.

    My recommendation would be that in both cases, the package curators give a +deadline by which all packages must switch to the newer version of the +dependency. The length of time given should depend on the complexity of the +upgrade.

  3. Suppose a new version of a package is released, but is not intended to be +widely used yet. To harp on conduit again, conduit 0.5 was released +significantly before the rest of the conduit/wai/Yesod ecosystem was +updated. In such a case, the developer releasing the package should be able to +blacklist the new version for a certain amount of time until he/she decides it +should be moved into live mode.

Compile and test

Once an acceptable set of packages has been achieved, they should all be +simultaneously compiled, and all of their test suites run. Ideally, this would +be done on multiple operating systems and versions of GHC. Also ideally, as +this project matures, it will begin to include a large set of integration +tests.

Snapshot

We now have a list of packages and versions which are guaranteed (to some +extent) to work together correctly. Take this information and build up a +00-index.tar file. In other words, create a fake Hackage repository (we'll see +why in the next step).

In a truly ideal world, we'd have the same kind of major/minor version +breakdown of this package set as with normal Haskell packages. The idea would +be that we'd create a snapshot and name it version 5.1. If we get some bugfixes +for a certain package, we can included that updated version and release a new +snapshot named version 5.1.1. But short-term, a simple date-based release +system would be sufficient.

Point new users to this database

New Haskellers should then be pointed at this modified Hackage database instead +of the official Hackage database. The upshot is that it will now be impossible +to enter dependency hell.

One downside to this approach is that it greatly limits which packages you can +use. You can only install packages from the small subset of Hackage. Ideally +we'd like to allow users to install some packages from "greater Hackage" +without introducing too much dependency hell.

A possible addition we could make to achieve this goal is to have an additional +"extras" repo available, which will include all of Hackage which is not part of +the database. The important point is that we would only include a single +version of the blessed packages. So if bytestring 0.10.0 is in our set of +packages, then no other version of bytestring would be in the extras repo. By +doing so, we make it impossible for the core packages to enter dependency hell, +though the extras packages in theory could.

Next steps

This is certainly an ambitious project, but I think it's easily within reach. I +follow a very similar procedure already for creating the +yesod-platform meta-package, and a lot of that code can be reused. The +coordination amongst different maintainers could be handled on Github with +branches and pull requests. And since this is just a thin layer on top of +Hackage, there's a low upfront cost.

Before we dive into this, I hope the community looks at this proposal seriously +to determine if it will solve our problems. If you think it won't, challenge +the proposal. We want to improve the state of Haskell for new developers as +much as possible.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/11/stable-vetted-hackage.html b/public/blog/2012/11/stable-vetted-hackage.html new file mode 100644 index 00000000..ce130253 --- /dev/null +++ b/public/blog/2012/11/stable-vetted-hackage.html @@ -0,0 +1,85 @@ + Stable, Vetted Hackage: Call for Participation +

Stable, Vetted Hackage: Call for Participation

November 30, 2012

GravatarBy Michael Snoyman

About two weeks ago, I wrote a blog post giving a proposal to solve Cabal +hell for end users. +Let me summarise some main points of my proposal:

  • Cabal isn't really the problem at this point (so Cabal hell is really a +misnomer; let's agree to start calling it version hell). The problem is +that Hackage itself is unstable.

  • Hackage should be unstable, that's its purpose.

  • Many end users simply need access to a subset of Hackage which is guaranteed +to play well together.

  • The Haskell Platform goes a step beyond that, by placing high quality +standards on its packages. This is a good move for the Platform, but doesn't +cover user needs enough.

  • In other words, we need a level between Hackage and the Haskell Platform.

I received a lot of very positive feedback, and have therefore started work on +a project to implement this idea. This project has the full support of FP +Complete, who is sponsoring my time to get this project rolling. I'll be +writing a more thorough blog post on FP Complete's involvement, but in sum it +is our goal to help the community create an even greater level of stability for +the Haskell ecosystem, and we believe that this project is one means of doing +so.

The code base I'm working on is available as a Github +repo. (Note: I've called it "Stackage" +== Stable Hackage for now, but I don't really like that name at all. If someone +can come up with a better name, please let me know.) The most important file is +Stackage.Config, +and in particular the stablePackages value. This is where we would specify +which packages should be included in the package set. If you look in that file, +you'll see that there are already a number of people who have signed up as +maintainers for various packages. I hope this number will increase as the +community gets more involved; more on this below.

There are really two aspects to using Stackage: building the repos, and using a +set of built repos. The idea is that we'll do the former as a community process +(maybe on a dedicated build server, or a series of servers with different +OSes), place the repos on some publicly available server, and then an end user +would just need to do:

cabal install stackage # just needs to be run once
+stackage update

From then on, the user would be guaranteed to never enter dependency hell when +installing our blessed packages. For our purposes, the blessed packages would +be the list of stable packages and all of their dependencies.

If you want to get started with trying out the code, you can try the following:

cabal update
+cabal install cabal-dev
+git clone https://github.com/fpco/stackage
+cd stackage
+git submodule update --init # get the Haskell Platform files
+runghc app/stackage.hs build # takes a *long* time
+runghc app/stackage.hs init # modifies your ~/.cabal/config file

I've only tested this on Linux for now. I'm fairly certain we'll run into +issues on other platforms, either due to differences in where Cabal stores +things or missing platform-specific dependencies. If people can give the code a +shot on Mac and Windows and file issues (or better: send pull requests), that +would be a great way to move forward.

A few other minor details:

  • This is built as a superset of the Haskell Platform, so it pegs the versions +of HP packages to the versions released in the platform itself.

  • There are actually two repos generated: stackage contains the blessed +packages only, with only one version of each, and stackage-extra contains +all packages which are neither distributed with GHC nor in the blessed list. +This means that you can install (in theory) any package on Hackage, though you +can still run into dependency hell with the -extra list.

So how can we move forward on this? There are a few important ways:

  • Add more packages. I've included Yesod and all its dependencies, and a +number of other developerss (Neil Mitchell, Alan Zimmerman, Jasper Van der Jeugt and +Antoine Latter) have added their packages as well. Among all of these packages +and their dependencies, we already cover a lot of the most commonly used +packages out there. But having wider coverage is even better. If you'd like +to add your code, just send a pull request. (For more details, see the +README.)

  • Start hosting prepared repositories somewhere, and then release stackage +onto Hackage. We clearly need to have some more testing done before this +can happen.

  • Set up automated, cross-platform testing. It would be wonderful if, before +each release, every package was compiled and tested on all the major +operating systems and versions of GHC.

  • Initially, the build procedure reported errors for a number of packages. +I've sent a lot of pull requests over the past few weeks to try and get +those corrected, and Stackage now builds cleanly. I'm hoping that by having +this kind of automated tool running regularly, we'll be able to spot problems +in packages quickly and alert the maintainers. To make the system great, I'm +hoping that maintainers will be able to help out by making necessary changes to +their packages.

  • It would be great to have Linux distribution maintainers on board with this +initiative. Having the same set of stable packages available on multiple +platforms would be great, and hopefully this project will allow us all to +pool resources. I've been in touch with maintainers for Debian, Fedora, and +Nix, and we're trying to coordinate how such a system would work. If there are +other distributions I missed that want to be part of this process, please be in +touch!

But I want to make one thing clear. For this project to succeed, it has to have +wide-spread community support. There's been a great response from those I've +contacted already, and I'm hoping this will continue after this blog post. The +more of the community gets involved in the process, the greater the benefits +for everyone.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/11/updates-julius-interpolation.html b/public/blog/2012/11/updates-julius-interpolation.html new file mode 100644 index 00000000..1c797698 --- /dev/null +++ b/public/blog/2012/11/updates-julius-interpolation.html @@ -0,0 +1,21 @@ + Updates to Julius Interpolation +

Updates to Julius Interpolation

November 16, 2012

GravatarBy Michael Snoyman

I originally wrote this as an email to the web-devel and Yesod mailing +lists, but decided that this topic was important enough to be mentioned +in a blog post as well. If you have questions, please direct them to the +mailing list.

Following some discussions on the issue both recently and many months ago, I've just released some updates to how Julius interpolation works. The basic idea is that we want to make interpolation of properly escaped JSON data the default, and the interpolation of unchecked Javascript code the exception to the rule. Concretely, take the following Julius template:

let greeting = "Say \"Hello\"" :: String
+ in [julius|alert("#{greeting}")|]

In Julius 1.0, greeting is interpolated directly into your Javascript template, resulting in the output:

alert("Say "Hello"")

Importantly, the double quotes are not escaped. Instead, we would like greeting to be treated as JSON, the double quotes within the text to be escaped, and the surrounding double quotes to be automatically added. I've just released versions 1.0.2 and 1.1 of shakespeare-js (the package providing Julius), and it is now valid to say:

let greeting = "Say \"Hello\"" :: String
+ in [julius|alert(#{toJSON greeting})|]

Resulting in:

alert("Say \"Hello\"")

What if you really want to spit out raw Javascript code? You can do so via the rawJS function:

let somecode = "alert(\"Hello\")" :: String
+ in [julius|#{rawJS somecode}|]

I've set up this release system to try and maintain backwards compatibility, while simultaneously making it a compile-time error when you misuse the library. For the former, I've released shakespeare-js 1.0.2, which keeps the same interpolation semantics as 1.0.1, but adds in the rawJS function. You can safely add rawJS to virtually any existing #{} interpolation in Julius without any change in behavior.

The difference with version 1.1 is that the old interpolations will no longer work. Under the surface, this is because I've removed the ToJavascript instances for String and Text. You can now only interpolate JSON values (by using toJSON) or raw Javascript (by using rawJS).

Down the road, it will likely make sense to add in ToJavascript instances for common types which first convert to JSON. However, doing so in this first release would mean that old code would silently change its semantics. Hopefully you will now be able to upgrade to 1.0.2 without any breakage, and in the near future upgrade to 1.1 and get error messages where you need to insert rawJS calls.

I am not yet updating the yesod-platform to use the new versions of these packages; I'd like to ask people to test things out and report any issues they run into so that we can make a properly stable yesod-platform release soon.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/11/warp-posa.html b/public/blog/2012/11/warp-posa.html new file mode 100644 index 00000000..21988aea --- /dev/null +++ b/public/blog/2012/11/warp-posa.html @@ -0,0 +1,544 @@ + Warp chapter for Performance of Open Source Applications +

Warp chapter for Performance of Open Source Applications

November 14, 2012

GravatarBy Michael Snoyman

Kazu and I (mostly Kazu) have been working on a chapter on Warp for the +upcoming "Performance of Open Source Applications," a semi-sequel to the book +"Architecture of Open Source Applications." You've already seen some of the +content on this blog in the past few months; below is our draft of the chapter. +Feedback is highly welcome.

The actual content for this chapter lives on +Github. Issues +and pull requests are a very welcome way to receive comments.

Warp

Authors: Kazu Yamamoto and Michael Snoyman

Warp is a high-performance library of HTTP server side in Haskell, +a purely functional programming language. +Both Yesod, a web application framework, and mighty, an HTTP server, +are implemented over Warp. +According to our throughput benchmark, +mighty provides performance on a par with nginx. +This article will explain +the architecture of Warp and how we improved its performance. +Warp can run on many platforms, +including Linux, BSD variants, MacOS, and Windows. +To simplify our explanation, however, we will only talk about Linux +for the remainder of this article.

Network programming in Haskell

Some people believe that +functional programming languages are slow or impractical. +However, to the best of our knowledge, +Haskell provides a nearly ideal approach for network programming. +This is because the Glasgow Haskell Compiler (GHC), +the flagship compiler for Haskell, provides +lightweight and robust user threads (sometimes called green threads). +In this section, we will briefly explain the history of +server side network programming.

Native threads

Traditional servers use a technique called thread programming. +In this architecture, each connection is handled +by a single process or native thread- sometimes called an OS thread.

This architecture can be further segmented based on the mechanism used for creating the processes or native threads. +When using a thread pool, multiple processes or native threads are created in advance. +An example of this is the prefork mode in Apache. +Otherwise, a process or native thread is spawned each time a connection is received. Fig (TBD:1.png) illustrates this.

Native threads

The advantage of this architecture is that it enables the developer to write clear code, +since the code is not divided into event handlers. +Also, because the kernel assigns processes or +native threads to available cores, +we can balance utilization of cores. +Its disadvantage is that a large number of +context switches between kernel and processes or native threads occur, +resulting in performance degredation.

Event driven

Recently, it has been said that event-driven programming is +required to implement high-performance servers. +In this architecture multiple connections are handled +by a single process (Fig (TBD:2.png)). +Lighttpd is an example of a web server using this architecture.

Event driven

Since there is no need to switch processes, +less context switches occur, and performance is thereby improved. +This is its chief advantage. +However, it has two shortcomings. +The first is the fact that, +since there is only a single process, +only one core can be utilized. +The second is that it requires asynchronous programming, +so code is fragmented into event handlers. +Asynchronous programming also prevents the conventional +use of exception handling (although there are no exceptions in C).

1 process per core

Many have hit upon the idea of creating +N event-driven processes to utilize N cores (Fig (TBD:3.png)). +Each process is called a worker. +A service port must be shared among workers. +Using the prefork technique- not to be confused with Apache's prefork mode- +port sharing can be achieved, after slight code modifications.

1 process per core

One web server that uses this architecture is nginx. +Node.js used the event-driven architecture in the past but +it also implemented this scheme recently. +The advantage of this architecture is +that it utilizes all cores and improves performance. +However, it does not resolve the issue of programs having poor clarity.

User threads

GHC's user threads can be used to solve the code clarity issue. +They are implemented over an event-driven IO manager in GHC's runtime system. +Standard libraries of Haskell use non-blocking system calls +so that they can cooperate with the IO manager. +GHC's user threads are lightweight; +modern computers can run 100,000 user threads smoothly. +They are robust; even asynchronous exceptions are caught +(this feature is used timeout described in Section (TBD:Warp's architecture) and in Section (TBD:Timers for connections)).

Some languages and libraries provided user threads in the past, +but they are not commonly used now because they are not lightweight +or are not robust. +But in Haskell, most computation is non-destructive. +This means that almost all functions are thread-safe. +GHC uses data allocation as a safe point to switch context of user threads. +Because of functional programming style, +new data are frequently created and it is known that +such data allocation is regulaly enough for context switching.

Use of lightweight threads makes +it possible to write clear code +like traditional thread programming +while keeping high-performance (Fig (TBD:4.png)).

User threads

As of this writing, mighty uses the prefork technique to fork processes +to utilize cores and Warp does not have this functionality. +The Haskell community is now developing a parallel IO manager. +A Haskell program with the parallel IO manager is +executed as a single process and +multiple IO managers run as native threads to utilize cores. +Each user thread is executed on any one of the cores. +If and when this code is merged into GHC, +Warp itself will be able to use this architecture +without any modifications.

Warp's architecture

Warp is an HTTP engine for the Web Application Interface (WAI). +It runs WAI applications over HTTP. +As we described above, both Yesod and mighty are +examples of WAI applications, as illustrated in Fig (TBD:wai.png).

Web Application Interface (WAI)

The type of WAI applications is as follows:

type Application = Request -> ResourceT IO Response

In Haskell, argument types of function are separated by right arrows and +the most right one is the type of return value. +So, we can interpret the definition +as a WAI application takes Request and returns Response.

After accepting a new HTTP connection, a dedicated user thread is spawned for the +connection. +It first receives an HTTP request from a client +and parses it to Request. +Then, Warp gives the Request to a WAI application and +receives a Response from it. +Finally, Warp builds an HTTP response based on the Response value +and sends it back to the client. +This is illustrated in Fig (TBD:warp.png).

The architecture of Warp

The user thread repeats this procedure +as necessary and terminates itself +when the connection is closed by the peer +or and invalid request is received. +It is also killed by the dedicated user thread for timeout +if a significant amount of data is not received for a certain period.

Performance of Warp

Before we explain how to improve the performance of Warp, +we would like to show the results of our benchmark. +We measured throughput of mighty 2.8.2 (with Warp 1.3.4.1) and nginx 1.2.4. +Our benchmark environment is as follows:

  • One "12 cores" machine (Intel Xeon E5645, two sockets, 6 cores per 1 CPU, two QPI between two CPUs)
  • Linux version 3.2.0 (Ubuntu 12.04 LTS), which is running directly on the machine (i.e. without a hypervisor)

We tested several benchmark tools in the past and +our favorite one was httperf. +Since it uses select() and is just a single process program, +it reaches its performance limits when we try to measure HTTP servers on +multi-cores. +So, we switched to weighttp, which +is based on libev (the epoll family) and can use +multiple native threads. +We used weighttp as follows:

weighttp -n 100000 -c 1000 -t 3 -k http://127.0.0.1:8000/

This means that 1,000 HTTP connections are established, with +each connection sending 100 requests. +3 native threads are spawned to carry out these jobs.

For all requests, the same index.html file is returned. +We used nginx's index.html, whose size is 151 bytes. +As "127.0.0.1" suggests, we measured web servers locally. +We should have measured from a remote machine, but +we do not have a suitable environment at the moment.

Since Linux has many control parameters, +we need to configure the parameters carefully. +You can find a good introduction to +Linux parameter tuning in ApacheBench & HTTPerf. +We carefully configured both mighty and nginx as follows:

  • Enabled file descriptor cache
  • Disabled logging
  • Disabled rate limitation

Since our machine has 12 cores and +weighttp uses three native threads, +we measured web servers from one worker to +eight workers (in our experience, +three native threads is enough to measure 8 workers). +Here is the result:

Performance of Warp and `nginx`

The x-axis is the number of workers and the y-axis gives throughput, +measured in requests per second.

Key ideas

There are four key ideas to implement high-performance servers in Haskell:

  1. Issuing as few system calls as possible
  2. Specialization and avoiding re-calculation
  3. Avoiding locks
  4. Using proper data structures

Issuing as few system calls as possible

If a system call is issued, +CPU time is given to the kernel and all user threads stop. +So, we need to use as few system calls as possible. +For an HTTP session to get a static file, +Warp calls recv(), send() and sendfile() only (Fig (TBD:warp.png)). +open(), stat() and close() can be omitted +thanks to cache mechanism described in Section (TBD:Timers for file descriptors).

We can use the strace command to see what system calls are actually used. +When we observed the behavior of nginx with strace, +we noticed that it used accept4(), which we did not know about at the time.

Using Haskell's standard network library, +a listening socket is created with the non-blocking flag set. +When a new connection is accepted from the listening socket, +it is necessary to set the corresponding socket as non-blocking as well. +The network library implements this by calling fcntl() twice: +one is to get the current flags and the other is to set +the flags with the non-blocking flag ORed.

On Linux, the non-block flag of a connected socket +is always unset even if its listening socket is non-blocking. +accept4() is an extension version of accept() on Linux. +It can set the non-blocking flag when accepting. +So, if we use accept4(), we can avoid two unnecessary fcntl()s. +Our patch to use accept4() on Linux has been already merged to +the network library.

Specialization and avoiding re-calculation

GHC provides a profiling mechanism, but it has a limitation: +correct profiling is only possible +if a program runs in the foreground and does not spawn child processes. +So, if we want to profile live activities of servers, +we need to take special care for profiling.

mighty has this mechanism. +Suppose that N is the number of workers +in the configuration file of mighty. +If N is greater than or equal to 2, mighty creates N child processes +and the parent process just works to deliver signals. +However, if N is 1, mighty does not create any child process. +Instead, the executed process itself serves HTTP. +Also, mighty stays in its terminal if debug mode is on.

When we profiled mighty, +we were surprised that the standard function to format date string +consumed the majority of CPU time. +As many know, an HTTP server should return GMT date strings +in header fields such as Date:, Last-Modified:, etc:

Date: Mon, 01 Oct 2012 07:38:50 GMT

So, we implemented a special formatter to generate GMT date strings. +Comparing the standard function and our specialized function with +criterion, a standard benchmark library of Haskell, +ours are much faster. +But if an HTTP server accepts more than one request per second, +the server repeats the same formatting again and again. +So, we also implemented a cache mechanism for date strings.

We will also explain this topic in Section (TBD:Writing the Parser) +and in Section (TBD:Composer for HTTP response header).

Avoiding locks

Unnecessary locks are evil for programming. +Our code sometime uses unnecessary locks imperceptibly +because, internally, the runtime systems or libraries use locks. +To implement high-performance servers, +we need to identify such locks and +avoid them if possible. +It is worth pointing out that +locks will become much more critical under +the parallel IO manager. +We will talk about how to identify and avoid locks +in Section (TBD:Timers for connections) and +Section (TBD:Memory allocation).

Using proper data structures

Haskell's standard data structure for strings is String, +which is a linked list of Unicode characters. +Since list programming is the heart of functional programming, +String is convenient for many purposes. +But for high-performance servers, the list structure is too slow +and Unicode is overly complex since the HTTP protocol is based on byte streams. +Instead, we use ByteString to express strings (or buffers). +A ByteString is an array of bytes with meta data. +Thanks to this meta data, +splicing without copying is possible. +This is described in Section (TBD:Writing the Parser) in detail.

Other examples of proper data structures are +Builder and double IORef. +They are explained in Section (TBD:Composer for HTTP response header) +and Section (TBD:Timers for connections), respectively.

HTTP request parser

Besides the many issues involved with efficient concurrency and I/O in a multicore environment, +Warp also needs to be certain that each core is performing its tasks efficiently. +In that regard, the most relevant component is the HTTP request processing. +The purpose is to take a stream of bytes coming from the incoming socket, +parse out the request line and individual headers, +and leave the request body to be processed by the application. +It must take this information and produce a data structure which the application +(whether a Yesod application, mighty, or something else) +will use to form its response.

The request body itself presents some interesting challenges. +Warp provides full support for pipelining and chunked request bodies. +As a result, Warp must "dechunk" any chunked request bodies before passing them to the application. +With pipelining, multiple requests can be transferred on a single connection. +Therefore, Warp must ensure that the application does not consume too many bytes, +as that would remove vital information from the next request. +It must also be sure to discard any data remaining from the request body; +otherwise, the remainder will be parsed as the beginning of the next request, +causing either an invalid request or a misunderstood request.

As an example, consider the following theoretical request from a client:

POST /some/path HTTP/1.1
+Transfer-Encoding: chunked
+Content-Type: application/x-www-form-urlencoded
+
+0008
+message=
+000a
+helloworld
+0000
+
+GET / HTTP/1.1

The HTTP parser must extract the /some/path pathname and the Content-Type header and pass these to the application. +When the application begins reading the request body, it must strip off the chunk headers (e.g., 0008 and 000a) +and instead provide the actual content, i.e. message=helloworld. +It must also ensure that no more bytes are consumed after the chunk terminator (0000) +so as not to interfere with the next pipelined request.

Writing the Parser

Haskell is known for its powerful parsing capabilities. +It has traditional parser generators as well as combinator libraries, such as Parsec and Attoparsec. +Parsec and Attoparsec's textual module work in a fully Unicode-aware manner. +However, HTTP headers are guaranteed to be ASCII, +so Unicode awareness is an overhead we need not incur.

Attoparsec also provides a binary interface for parsing, +which would let us bypass the Unicode overhead. +But as efficient as Attoparsec is, +it still introduces an overhead relative to a hand-rolled parser. +So for Warp, we have not used any parser libraries. +Instead, we perform all parsing manually.

This gives rise to another question: how do we represent the actual binary data? +The answer is a ByteString, which is +essentially three pieces of data: +a pointer to some piece of memory, +the offset from the beginning of that memory to the data in question, +and the size of our data.

The offset information may seem redundant. +We could instead insist that our memory pointer point to the beginning of our data. +However, by including the offset, we enable data sharing. +Multiple ByteStrings can all point to the same chunk of memory and use different parts of it (a.k.a., splicing). +There is no concern of data corruption, since ByteStrings- like most Haskell data- are immutable. +When the final pointer to a piece of memory is no longer used, then the memory buffer is deallocated.

This combination is perfect for our use case. +When a client sends a request over a socket, +Warp will read the data in relatively large chunks (currently 4096 bytes). +In most cases, this is large enough to encompass the entire request line +and all request headers. +Warp will then use its hand-rolled parser to break this large chunk into lines. +This can be done quite efficiently since:

  1. We need only scan the memory buffer for newline characters. +The bytestring library provides such helper functions, +which are implemented with lower-level C functions like memchr. +(It's actually a little more complicated than that due to multiline headers, +but the same basic approach still applies.)

  2. There is no need to allocate extra memory buffers to hold the data. +We just take splices from the original buffer. +See figure (TBD:bytestring.png) for a demonstration of splicing individual components from a larger chunk of data. +It's worth stressing this point: +we actually end up with a situation which is more efficient than idiomatic C. +In C, strings are null-terminated, so splicing requires +allocating a new memory buffer, +copying the data from the old buffer, +and appending the null character.

Splicing ByteStrings

Once the buffer has been broken into lines, +we perform a similar maneuver to turn the header lines into key/value pairs. +For the request line, we parse the requested path fairly deeply. +Suppose we have a request for:

GET /buenos/d%C3%ADas HTTP/1.1

We need to perform the following steps:

  1. Separate the request method, path, and version into individual pieces.

  2. Tokenize the path along forward slashes, ending up with ["buenos", "d%C3%ADas"].

  3. Percent-decode the individual pieces, ending up with ["buenos", "d\195\173as"].

  4. UTF8-decode each piece, finally arriving at Unicode-aware text: ["buenos", "días"].

There are a few performance gains we have in this process:

  1. As with newline checking, finding forward slashes is a very efficient operation.

  2. We use an efficient lookup table for turning the Hex characters into numerical values. +This code is a single memory lookup and involves no branching.

  3. UTF8-decoding is a highly optimized operation in the text package. +Likewise, the text package represents this data in an efficient, packed representation.

  4. Due to Haskell's laziness, this calculation will be performed on demand. +If the application in question does not need the textual version of the path, +none of these steps will be performed.

The final piece of parsing we perform is dechunking. +In many ways, dechunking is a simpler form of parsing. +We parse a single Hex number, and then read the stated number of bytes. +Those bytes are passed on verbatim- without any buffer copying- to the application.

Conduit

This article has mentioned a few times the concept of passing the request body to the application. +It has also hinted at the issue of the application passing a response back to the server, +and the server receiving data from and sending data to the socket. +A final related point not yet discussed is middleware, +which are components sitting between the server and application that somehow modify the request and/or response. +The definition of a middleware is:

type Middleware = Application -> Application

The intuition behind this is that a middleware will take some "internal" application, +preprocess the request, +pass it to the internal application to get a response, +and then postprocess the response. +For our purposes, a prime example would be a gzip middleware, +which automatically compresses response bodies.

Historically, a common approach in the Haskell world for representing such streams of data has been lazy I/O. +Lazy I/O represents a stream of values as a single, pure data structure. +As more data is requested from this structure, I/O actions will be performed to grab the data from its source. +Lazy I/O provides a huge level of composability. +For example, we can write a function to compress a lazy ByteString, +and then apply it to an existing response body easily. +However, there are two major downsides to lazy I/O:

  1. Non-deterministic resource finalization. +If a reference to such a lazy structure is maintained, +or if the structure isn't quickly evaluated to its completion, +the finalization may be delayed for a long time +(e.g., until a garbage collection sweep can ensure the data is no longer necessary). +In many cases, this non-determinism is acceptable. +In the case of a web server, file descriptors are a scarce resource, +and therefore we need to ensure that they are finalized as soon as possible.

  2. These pure structures present something of a lie. +They are promising that there will be more bytes available, +but in fact the I/O operations that will be performed to retrieve those bytes may fail. +This can lead to exceptions being thrown where they are not anticipated.

We could drop down to a lower level and deal directly with file descriptors instead. +However, this would hurt composability greatly. +How would we write a general-purpose GZIP middleware? +It also leaves open the question of buffering. +Inherent in the request parsing described above, +we have the need to store extra bytes for later steps. +For example, if we grab a chunk of data which includes both request headers +and some of the request body, +the request body must be buffered and then provided to the application.

To address this, the WAI protocol- and therefore Warp- is built on top of the conduit package. +This package provides an abstraction for streams of data. +It keeps much of the compsability of lazy I/O, +provides a buffering solution, +and guarantees deterministic resource handling. +Exceptions are also kept where they belong, +in the parts of your code which deal with I/O.

Warp represents the incoming stream of bytes from the client as a Source, +and writes data to be sent to the client to a Sink. +The Application is provided a Source with the request body, +and provides a response as a Source as well. +Middlewares are able to intercept the Sources for the request and response bodies +and apply transformations to them. +Figure (TBD:middleware.png) demonstrates how a middleware fits between Warp and an application. +The composability of the conduit package makes this an easy and efficient operation.

Middlewares

Conduit itself is a large topic, and therefore will not be covered in more depth. +Suffice it to say for now that conduit's usage in Warp is a contributing factor to its high performance.

Slowloris protection

We have one final concern: the slowloris attack. +This is a form of a Denial of Service (DOS) attack wherein each client sends very small amounts of information. +By doing so, the client is able to maintain a higher number of connections on the same hardware/bandwidth. +Since the web server has a constant overhead for each open connection regardless of bytes being transferred, +this can be an effective attack. +Therefore, Warp must detect when a connection is not sending enough data over the network and kill it.

We discuss the timeout manager in more detail below, +which is the true heart of slowloris protection. +When it comes to request processing, +our only requirement is to tease the timeout handler to let it know more data has been received from the client. +In Warp, this is all done at the conduit level. +As mentioned, the incoming data is represented as a Source. As part of that Source, +every time a new chunk of data is received, the timeout handler is teased. +Since teasing the handler is such a cheap operation (essentially just a memory write), +slowloris protection does not hinder the performance of individual connection handlers in a significant way.

HTTP response composer

This section describes the HTTP response composer of Warp. +A WAI Response has three constructors:

ResponseFile Status ResponseHeaders FilePath (Maybe FilePart)
+ResponseBuilder Status ResponseHeaders Builder
+ResponseSource Status ResponseHeaders (Source (ResourceT IO) (Flush Builder))

ResponseFile is used to send a static file while +ResponseBuilder and ResponseSource are for sending +dynamic contents created in memory. +Each constructor includes both Status and ResponseHeaders. +ResponseHeaders is defined as a list of key/value header pairs.

Composer for HTTP response header

The old composer built HTTP response header with a Builder, +a rope-like data structure. +First, it converted Status and each element of ResponseHeaders +into a Builder. Each conversion runs in O(1). +Then, it concatenates them by repeatedly appending one Builder to another. +Thanks to the properties of Builder, each append operation also runs in O(1). +Lastly, it packs an HTTP response header +by copying data from Builder to a buffer in O(N).

In many cases, the performance of Builder is sufficient. +But we experienced that it is not fast enough for +high-performance servers. +To eliminate the overhead of Builder, +we implemented a special composer for HTTP response headers +by directly using memcpy(), a highly tuned byte copy function in C.

Composer for HTTP response body

For ResponseBuilder and ResponseSource, +the Builder values provided by the application are packed into a list of ByteString. +A composed header is prepended to the list and +send() is used to send the list in a fixed buffer.

For ResponseFile, +Warp uses send() and sendfile() to send +an HTTP response header and body, respectively. +Fig (TBD:warp.png) illustrates this case. +Again, open(), stat(), close() and other system calls can be committed +thanks to the cache mechanism described in Section (TBD:Timers for file descriptors). +The following subsection describe another performace tuning +in the case of ResponseFile.

Sending header and body together

When we measured the performance of Warp to send static files, +we always did it with high concurrency. +That is, we always made multiple connections at the same time. +It gave us a good result. +However, when we set the number of concurrency to 1, +we found Warp to be really slow.

Observing the results of the tcpdump command, +we realized that this is because originally Warp used +the combination of writev() for header and sendfile() for body. +In this case, an HTTP header and body are sent in separate TCP packets (Fig (TBD:tcpdump.png)).

Packet sequence of old Warp

To send them in a single TCP packet (when possible), +new Warp switched from writev() to send(). +It uses send() with the MSG_MORE flag to store a header +and sendfile() to send both the stored header and a file. +This made the throughput at least 100 times faster +according to our throughput benchmark.

Clean-up with timers

This section explain how to implement connection timeout and +how to cache file descriptors.

Timers for connections

To prevent slowloris attacks, +communication with a client should be canceled +if the client does not send a significant amount of data +for a certain period. +Haskell provides a standard function called timeout +whose type is as follows:

Int -> IO a -> IO (Maybe a)

The first argument is the duration of the timeout, in microseconds. +The second argument is an action which handles input/output (IO). +This function returns a value of Maybe a in the IO context. +Maybe is defined as follows:

data Maybe a = Nothing | Just a

Nothing indicates an error (without reason information) and +Just encloses a successful value a. +So, timeout returns Nothing +if an action is not completed in a specified time. +Otherwise, a successful value is returned wrapped with Just. +timeout eloquently shows how great Haskell's composability is.

Unfortunately, +timeout spawns a user thread to handle timeout. +To implement high-performance servers, +we need to avoid the creation of a user thread for timeout +for each connection. +So, we implemented a timeout system which uses only +one user thread, called the timeout manager, to handle the timeouts of all connections. +Its heart is the following two points:

  • Double IORefs
  • Safe swap and merge algorithm

Suppose that status of connections is described as Active and Inactive. +To clean up inactive connections, +the timeout manager repeatedly inspects the status of each connection. +If status is Active, the timeout manager turns it to Inactive. +If Inactive, the timeout manager kills its associated user thread.

Each status is referred to by an IORef. +IORef is a reference whose value can be destructively updated. +To update status through this IORef, +atomicity is not necessary because status is just overwritten. +In addition to the timeout manager, +each user thread repeatedly turns its status to Active through its own IORef as its connection actively continues.

The timeout manager uses a list of the IORef to these statuses. +A user thread spawned for a new connection +tries to prepend its new IORef for an Active status to the list. +So, the list is a critical section and we need atomicity to keep +the list consistent.

A list of status. `A` and `I` indicates `Active` and `Inactive`, respectively

A standard way to keep consistency in Haskell is MVar. +But MVar is slow, +since each MVar is protected with a home-brewed spin lock. +Instead, we used another IORef to refer the list and atomicModifyIORef +to manipulate it. +atomicModifyIORef is a function to atomically update IORef's values. +It is fast since it is implemented via CAS (Compare-and-Swap), +which is much faster than spin locks.

The following is the outline of the safe swap and merge algorithm:

do xs <- atomicModifyIORef ref (\ys -> ([], ys)) -- swap with an empty list, []
+   xs' <- manipulates_status xs
+   atomicModifyIORef ref (\ys -> (merge xs' ys, ()))

The timeout manager atomically swaps the list with an empty list. +Then it manipulates the list by turning status and/or removing +unnecessary status for killed user threads. +During this process, new connections may be created and +their status are inserted via atomicModifyIORef by +their corresponding user threads. +Then, the timeout manager atomically merges +the pruned list and the new list.

Timers for file descriptors

Let's consider the case where Warp sends the entire file by sendfile(). +Unfortunately, we need to call stat() +to know the size of the file +because sendfile() on Linux requires the caller +to specify how many bytes to be sent +(sendfile() on FreeBSD/MacOS has a magic number 0 +which indicates the end of file).

If WAI applications know the file size, +Warp can avoid stat(). +It is easy for WAI applications to cache file information +such as size and modification time. +If cache timeout is fast enough (say 10 seconds), +the risk of cache inconsistency problem is not serious. +And because we can safely clean up the cache, +we don't have to worry about leakage.

Since sendfile() requires a file descriptor, +the naive sequence to send a file is +open(), sendfile() repeatedly if necessary, and close(). +In this subsection, we consider how to cache file descriptors +to avoid open() and close(). +Caching file descriptors should work as follows: +if a client requests that a file be sent, +a file descriptor is opened by open(). +And if another client requests the same file shortly thereafter, +the previously opened file descriptor is reused. +At a later time, the file descriptor is closed by close() +if no user thread uses it.

A typical tactic for this case is reference counting. +But we were not sure that we could implement a robust mechanism +for a reference counter. +What happens if a user thread is killed for unexpected reasons? +If we fail to decrement its reference counter, +the file descriptor leaks. +We noticed that the scheme of connection timeout is safe +to reuse as a cache mechanism for file descriptors +because it does not use reference counters. +However, we cannot simply reuse the timeout manager for serveral reasons.

Each user thread has its own status. So, status is not shared. +But we would like to cache file descriptors to avoid open() and +close() by sharing. +So, we need to search for a file descriptor for a requested file +from cached ones. +Since this look-up should be fast, we should not use a list. +Also, +because requests are received concurrently, +two or more file descriptors for the same file may be opened. +So, we need to store multiple file descriptors for a single file name. +This is technically called a multimap.

We implemented a multimap whose look-up is O(log N) and +pruning is O(N) with red-black trees +whose node contains a non-empty list. +Since a red-black tree is a binary search tree, +look-up is O(log N) where N is the number of nodes. +Also, we can translate it into an ordered list in O(log N). +In our implementation, +pruning nodes which contain a file descriptor to be closed is +also done during this step. +We adopted an algorithm to convert an ordered list to a red-black tree in O(N).

Future work

We have several ideas for improvement of Warp in the future, but +we will explain two here.

Memory allocation

When receiving and sending packets, buffers are allocated. +We think that these memory allocations may be the current bottleneck. +GHC runtime system uses pthread_mutex_lock +to obtain a large object (larger than 409 bytes in 64 bit machines).

We tried to measure how much memory allocation +for HTTP response header consume time. +For this purpose, GHC provides eventlog which +can records timestamps of each event. +We surrounded a memory allocation function +with the function to record a user event. +Then we complied mighty with it and took eventlog. +The result eventlog is illustrated as follows:

eventlog

Brick red bars indicates the event created by us. +So, the area surrounded by two bars is the time consumed by memory allocation. +It is about 1/10 of an HTTP session. +We are discussing how to implement memory allocation without locks.

New thundering herd

Thundering herd is an old but new problem. +Suppose that processes/native threads are pre-forked to share a listening socket. +They call accept() on the socket. +When a connection is created, old Linux and FreeBSD +wakes up all of them. +And only one can accept it and the others sleep again. +Since this causes many context switches, +we face a performance problem. +This is called thundering herd. +Recent Linux and FreeBSD wakes up only one process/native thread. +So, this problem became a thing of the past.

Recent network servers tend to use the epoll family. +If workers share a listening socket and +they manipulate accept connections through the epoll family, +thundering herd appears again. +This is because +the semantics of the epoll family is to notify +all processes/native threads. +nginx and mighty are victims of this new thundering herd.

The parallel IO manager is free from the new thundering herd problem. +In this architecture, +only one IO manager accepts new connections through the epoll family. +And other IO managers handle established connections.

Conclusion

Warp is a versatile web server library, +providing efficient HTTP communication for a wide range of use cases. +In order to achieve its high performance, +optimizations have been performed at many levels, +including network communications, thread management, and request parsing.

Haskell has proven to be an amazing language for writing such a codebase. +Features like immutability by default make it easier to write thread-safe code +and avoid extra buffer copying. +The multi-threaded runtime drastically simplifies the process of writing event-driven code. +And GHC's powerful optimizations mean that in many cases, +we can write high-level code and still reap the benefits of high performance. +Yet with all of this performance, our codebase is still relatively tiny +(under 1300 SLOC at time of writing). +If you are looking to write maintainable, efficient, concurrent code, +Haskell should be a strong consideration.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2012/12/simplified-persistent-types.html b/public/blog/2012/12/simplified-persistent-types.html new file mode 100644 index 00000000..03e0d433 --- /dev/null +++ b/public/blog/2012/12/simplified-persistent-types.html @@ -0,0 +1,80 @@ + Simplified Persistent Types +

Simplified Persistent Types

December 11, 2012

GravatarBy Michael Snoyman

tl;dr: There's an update to Persistent's type generation which greatly +simplifies error messages at the expense of losing some not-often-needed +generality. You can turn this generality on or off with a simple boolean flag, +so if you want the full power, it's available. For most users, I recommend +turning it off as described below. As an example of error message +simplification, compare:

  Actual type: GHandler
+                 sub0
+                 master0
+                 (Maybe
+                    (Entity (UserGeneric Database.Persist.GenericSql.Raw.SqlBackend)))
+               -> (Maybe
+                     (Entity (UserGeneric Database.Persist.GenericSql.Raw.SqlBackend))
+                   -> GHandler
+                        sub0
+                        master0
+                        (Maybe
+                           (Key (UserGeneric Database.Persist.GenericSql.Raw.SqlBackend))))
+               -> GHandler
+                    sub0
+                    master0
+                    (Maybe
+                       (Key (UserGeneric Database.Persist.GenericSql.Raw.SqlBackend)))

Using the new simplified types, this turns into:

  Actual type: GHandler sub0 master0 (Maybe (Entity User))
+               -> (Maybe (Entity User)
+                   -> GHandler sub0 master0 (Maybe (Key User)))
+               -> GHandler sub0 master0 (Maybe (Key User))

The problem: multi-backend datatypes

(If you just want to make your life easier and don't care about the details, +feel free to skip to the next section.)

Persistent works with multiple database backends. One issue that comes into play +for this is textual serialization of database keys. In the case of our SQL +backends, we have simple numerical keys. So any piece of text that entirely +consists of digits could be a valid key. On the other hand, MongoDB has a much +more specific key format.

The solution to this was to parameterize database keys on the backend they +applied to, in addition to the data type they apply to. In other words, we +have:

data Key backend entity

You can see more about this motivation in an email I wrote over a year +ago. Now +suppose you want to embed a reference to a different entity in a current one, +e.g.:

Person
+    name Text
+Car
+    owner PersonId
+    make Text
+    model Text

PersonId gets converted to Key backend Person... but how do we know what +backend is? Up until now, Persistent's solution to this is to create a +separate CarGeneric datatype which is parameterized on backend, and to +create a helper Car synonym that uses the backend specified in your +MkPersistSettings. In other words, you get:

data PersonGeneric backend = Person Text
+type Person = PersonGeneric SqlBackend
+data CarGeneric backend = Car (Key backend (PersonGeneric backend)) Text Text
+type Car = CarGeneric SqlBackend

This lets you keep some generic data types in case you want to store your data +in multiple backends (like PostgreSQL and MongoDB). However, it results in much +more complicated error messages. And given that this is such a corner use case, +this solution was- in retrospect- a bad tradeoff.

So now, if you specify mpsGeneric as False (or use sqlOnlySettings), the +generic datatypes won't be created. Instead, you'd just get:

data Person = Person Text
+data Car = Car (Key SqlBackend Person) Text Text

If you're only using a single database backend, this should introduce no downsides.

Start using the update

You basically need to make two +changes +to your site to use this new feature:

  1. Depend on persistent-template 1.1.1 or greater.

  2. In Model.hs, change sqlSettings to sqlOnlySettings (or the equivalent +for +Mongo).

Note that you'll need to be using persistent 1.1.

If you're a library author creating reusable components (e.g., an admin +interface), you should stick with the generic datatypes to avoid tying down to +a single backend. But for most standard applications, I highly recommend the +change.

Future work

I'm hoping this is just one step of many for simplifying error messages in the +Yesod ecosystem. We've done a lot of that in the past, but I have some new +ideas. +If you want to have a look at a concrete idea, you can look at an experimental +typeclass-based conduit +library, +based around an idea I saw from Chris Smith a while +ago.

I'm hoping to write separate blog posts about thoughts on conduit and classy-prelude, but I'll save that for another time.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/01/adding-css-js.html b/public/blog/2013/01/adding-css-js.html new file mode 100644 index 00000000..1bb906d9 --- /dev/null +++ b/public/blog/2013/01/adding-css-js.html @@ -0,0 +1,58 @@ + Cookbook: Adding CSS and Javascript +

Cookbook: Adding CSS and Javascript

January 30, 2013

GravatarBy Michael Snoyman

This is a cookbook recipe. Yesod has a strong and growing collection of +cookbook recipes available on our +Wiki. If you see any recipes +you think should be highlighted here, or would like to request some additions, +please bring it up on the mailing list or +the Google+ +community.

Let's take a very simple problem: including some CSS or Javascript on a page in +your Yesod site. There are actually many different ways to do this. +Fortunately, there are just a few recommended options, and the choice really +depends on what kind of use case you're optimizing for. Let's step through +these recommended approaches:

widgetFile

Probably the simplest thing to do is to use the widgetFile helper function +from the scaffolding. This typically looks something like:

getHomeR = defaultLayout $ do
+    setTitle "My Awesome Site"
+    $(widgetFile "home")

widgetFile will then look in your templates folder and find home.hamlet, +home.lucius, home.cassius, and home.julius. If it finds any of those, it +will include them in your page appropriately. When you're using yesod devel, +it will automatically reload any changes to the Lucius, Cassius and Julius +files.

Some important points:

  • For per-page styling, this works out very well. The scaffolding will place +the generated CSS and JS into an automatically generated external file, so +users won't have to redownload the contents. But it will also automatically +concatenate these contents with other CSS and JS snippets, minimizing the +number of connections.

  • Since defaultLayout also uses widgetFile, you can likewise edit +default-layout.lucius, default-layout.cassius and +default-layout.julius.

  • If you have a very large amount of CSS or JS that will be used on many pages, +this is not the right approach to use, since it will inflate the size of each +page's CSS and JS download. Instead, the contents should be placed in an +external file.

addScript(Remote)/addStylesheet(Remote)

If you have large files, such as jQuery or bootstrap.css, you don't want Yesod +to automatically concatenate their contents for each page. Instead, you want to +have a single static file that all pages can reference, and caching will ensure +that the file isn't downloaded multiple times.

The simpler variant is just referring to this file via URL. This works very +well for CDN-hosted files. So for example, to include jQuery, you can use:

addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"

If you want to serve the files yourself, the process is only a bit more +complicated. First, you'll need to create the file inside the static folder, +e.g. static/css/bootstrap.css. Then you'll need to reference its type-safe +URL. To save you from typos, Yesod automatically generates identifiers for all +files in your static folder, based on a simple renaming scheme. So to include +Bootstrap, you would use something like:

addStylesheet $ StaticR css_bootstrap_css

addScriptEither/addStylesheetEither

There's one other tweak to throw in. For some specific libraries (including +jQuery), Yesod provides a typeclass where you can specify the location of the +file to be used. This way, all jQuery-using widgets can share a single jQuery +instance. To keep things as general as possible, these libraries are specified +as an Either type, giving either a textual URL or a type-safe URL. In order +to add something like that, you need to use addScriptEither, e.g.:

master <- lift getYesod
+addScriptEither $ urlJqueryJs master

toWidget

If you don't want to have to create an external template file, you can use the +Shakesperean quasi-quoters. You just need to promote them to Widgets, e.g.:

toWidget [lucius| foo { bar: baz } |]
+toWidget [julius| alert("Hello World!"); |]

In general, however, I recommend using the external file approach.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/01/meaning-of-power.html b/public/blog/2013/01/meaning-of-power.html new file mode 100644 index 00000000..605b0da4 --- /dev/null +++ b/public/blog/2013/01/meaning-of-power.html @@ -0,0 +1,178 @@ + The Meaning of Power +

The Meaning of Power

January 23, 2013

GravatarBy Michael Snoyman

As I announced +recently, the Yesod +team has started work on the upcoming 1.2 branch. I'm going to be following up +with a few blog posts to discuss some design decisions we have to make. To try +and frame this discussion better, I want to describe a little bit of the design +philosophy I had in initially creating Yesod, and see if that can help us make +our decisions.

Definition of Power

Programmers often use a term in two very different- and often times +contradictory- ways. Consider the folowing two sentences:

  • This library lets me create anything I want, it has so much power.

  • I was able to use that library and create exactly what I needed in just a few +lines of code, it's such a powerful library.

Both statements refer to a positive aspect of some library, but the positive +aspects are completely different. The first refers to the flexibility of a +library. The second is saying that the library is optimized for your use case +(for brevity, let's call this featurefulness). And to a great extent, these +two concepts are at odds.

Let's take a simple example. Suppose you need to generate some HTML content. +One approach would be that my library provides you with the means to generate a +stream of bytes. This approach is incredibly flexible. You can generate any +character encoding you want. You can optimize your HTML by leaving off some +closing tags if you wanted. You can use single or double quotes for attributes +values. You can use decimal or hexadecimal numerical entities. And for that +matter, you could forget about HTML entirely, and just generate PNG or PDF +files if you wanted.

At the other end of the spectrum, I could provide you with an HTML templating +solution. In a few lines of code, you could have a fully valid HTML document. +You could specify at a high level the key-value attribute pairs for each +element, the contents of the element in textual form, and the library would +generate your HTML for you.

Which approach is more powerful? That's a meaningless question. Each approach +has an advantage over the other. They are each powerful in their own way, +whether flexibility or featurefulness. The question you should be asking is not +which is more powerful, but which one you should use.

Layers of Abstraction

Another way to frame this discussion is to recognize that we're just talking +about layers of abstraction. Under the surface, my theoretical HTML templating +library is just generating the bytes for you. So we need some way to determine +what our best abstraction level is. And to do that, we need to analyze our +goals.

Educating yourself

One goal which I've seen touted occasionally in the broader web framework +discussion is one of educating yourself. The argument goes that if you use a system that +does a lot of the work for you, then you don't learn anything. I frankly think +this is a meaningless argument, and not a helpful goal. All you've done in this +case is stated that you want to learn how to reimplement existing +functionality. If your goal is to understand how HTML is rendered, then forcing +yourself to use a raw-byte-generating library is a good idea. If your goal is +to create an HTML page, forcing yourself to learn the details of how rendering +is done is a needless distraction.

Flexible enough for the problem at hand

So what other goals are available? Clearly, we need to be able to address the +actual problem at hand. For example, if our task is to generate a PNG file, +then the HTML library is not suitable, and we'd have to use the lower level of +abstraction of dealing with bytes. (This is of course ignoring the possibility +that there might be some other libraries in existance that could help with +PNGs.) Said another way, we need to make sure that we haven't sacrificed too +much flexibility given our goals.

Simplicity

Another goal is to make the coding process as simple as possible. Simplicity +spells out in multiple ways: the code is shorter, easier to read, easier to +write, less buggy, more robust. I think it's an easy argument to make that an +HTML templating library will allow simpler code to be written than directly +producing a stream of bytes.

Learnability

I can probably express my entire byte-generating library as a function emit :: +ByteString -> IO (). That's incredibly simple to learn. By contrast, an HTML +library might have types to represent different kinds of nodes, attributes, +etc. This would imply that it is easier to learn the lower-level library.

While this may be true, it actually hides a lot. With the bytes library, you'll +need to learn the rules of HTML escaping, attribute quoting, nesting, etc. +So the breadth of the API does not always tell you how difficult it will be +to learn how to use something. If a task at hand requires a feature that a +lower level abstraction doesn't provide, you'll also need to learn how to +implement that yourself!

Performance

Often times (but not always), high levels of abstraction can introduce a +performance cost. Weighing this against the benefits of the higher level of +abstraction is really a personal decision. In some cases (say a desktop app +used by one person that will be run once a month), the benefit of additional +performance is probably negligible. In software that will be calculating large +scientific calculations and will run for months on end on a cluster of +supercomputers, eeking out a 2% performance increase could equal a massive cost +savings.

So the value of extra performance is up for debate, but in all cases +performance is a good thing to have.

Optimizing for the problem at hand

The main point I hope came across in the previous section is that we need to +design solutions for the problem at hand. Let's consider the question, +"What's better, C or Haskell?" I'm sure many readers would immediately say +Haskell, but that's not really a fair answer. Suppose that the problem at hand +is to add a tiny piece of functionality to a large C project. C is probably the +better tool in that case, as introducing a Haskell piece of code into this +codebase just wouldn't make sense. Similar reasons would be targeting a +platform without a Haskell compiler, or a memory constrained environment where +we cannot reasonably use garbage collection.

Bringing all of this back home: in the Yesod world, I've tried to identify a +number of different levels which users may want to have. Consider this +progression of levels of abstraction for creating a web application, starting +with the lowest:

  • Direct I/O on a socket.
  • network-conduit, allowing an abstraction over the streaming nature of the data.
  • WAI, abstracting over the HTTP protocol itself.
  • yesod-core, providing routing and common handler functions.
  • yesod, adding in Persistent and Shakespeare for data access and templating.
  • The Yesod scaffolding, tying it all together with a set of best practice folder conventions and helper functions.

The line can sometimes get a little bit blurred, but for the most part we have +a distinction between all of the different sets of goals a user might have. +Need to create a non-HTTP network application? network-conduit can provide what +you want. Need a basis for a full blown web app using a standard database and +templating solution? Use the scaffolding. And so on.

Once we've established what our goals are, we can then proceed to try to +optimize our solution based on the four points from the previous section:

  • We need to make sure that we have the flexibility to solve the entire +problem. If network-conduit only allowed textual data, this would not be +flexible enough, since many network protocols require binary data. On the flip +side, we don't need to provide full flexibility higher up. The scaffolded site +includes Persistent, it doesn't need to provide five other data storage +mechanisms as well. If you want to use a different data model, go down a layer +of abstraction. (Or in reality, use the scaffolding that doesn't include +Persistent, but you get the idea.)

    But there's another point to make as well. If we can still provide the +extra flexibility without hindering the user experience, we should. A prime +example of that is the fact that Yesod will allow you to deal directly with the +WAI protocol via waiRequest and sendWaiResponse. So really, our objectives +are:

    • We must provide enough flexibility for the problem at hand.
    • We should make it possible to get extra flexibility if it doesn't destroy our abstraction.
    • We could make this extra flexibility easy to get at as well.
  • Once we've identified what features we're going to have, we should make it as +easy as possible to access them. Let's take routing as an example. At the level +of Yesod, we've created a system where routes are non-overlapping, represented +as individual pieces of a requested path, and are fully decoded to text. We +don't take the hostname or the query string into consideration for routing by +default. And our routing syntax is possibly the simplest expression of these +constraints.

    What if you need to be able to route based on hostname? There are approaches +available, but they are not as simple. At a certain point, trying to get all of +this extra flexibility into the system doesn't make sense, at which point it's +more logical to drop a layer of abstraction to WAI. But this brings up another +issue: if I need more flexibility in one area (e.g., routing), should I really +have to give up simplicity in another one (e.g., templating)?

    To address that, Yesod is designed to be modular. The Shakespearean template +system can be used with non-Yesod application as well, so you can have an +incredibly flexible routing system together with a simple HTML templating +solution.

    Could we augment Yesod's routing system to- by default- route based on the +hostname? Certainly. But it would complicate our solution. We'd be getting +extra functionality which we don't need for our stated problem domain, and +giving up on simplicity. And that's not a good tradeoff.

  • Learnability comes down to two points. One is that there should be a subset of +the API which addresses the most common needs. This overlaps very much with the +goals of simplicity. You can create a lot of Yesod applications by just +sticking to defaultLayout and widgetFile.

    The second aspect is documentation. Functionality needs to be well documented +in how it works. But that's not sufficient. It needs to be easy for people to +find what they're looking for. One aspect is highlighting the most common API +subset in the documentation. Another is giving lots of examples, such as our +cookbook. In many cases, people don't need to know all the details and every +way to achieve a goal, but seeing a single best practice example is enough.

  • There's not much to say about performance. Make your code as efficient as +possible without losing the necessary flexibility and simplicity.

Concrete examples

To take this out of the abstract, I want to give some more concrete examples of +how these design goals have played themselves out in some of the libraries in +the Yesod ecosystem.

conduit

The first question is what our use cases are. I was lucky in this case, since +we already had a large body of streaming code to use as a basis for determining +what features we needed. Note an important point here: when creating a library, +it's IMO completely meaningless to simply design a solution. You have to have +real cases in mind to guide the design, or you end up with libraries that seem +simple and elegant but don't solve the actual problems.

Based on what I'd implemented previously, I knew that we'd need the ability to +create data producers, consumers, and transformers. We'd want to be able to +escape the Inversion of Control that most streaming packages had. Buffering +(a.k.a., leftovers) was a requirement for the majority of use cases. As a +result, all of these features are first-class citizens of conduit.

There are other features that were left out because they were less commonly +used. enumerator's isolate function, for example, is more powerful than +conduit's. But getting that extra flexibility would greatly harm conduit's +simplicity, and since that flexibility wasn't necessary, it was left out. On +the other hand, we absolutely needed to have guaranteed and prompt resource +finalization. Even though that complicated conduit, it had to be included.

Is conduit as efficient as directly reading and writing to file handles? No, +it introduces some overhead. Is it as easy to learn as a basic I/O API would +be? No, it has a larger API and more concepts to learn. However, it makes +resulting code simpler to read and write, and less bug prone. So the tradeoff +is (IMO) worth it.

authenticate/yesod-auth

Let's say we want to implement OpenID login on a website. At a high level, I'd +simply like a library where I say "allow OpenID access" and a button magically +appears on the login page. But unfortunately there's a lot more to it than +that:

  • I need to provide such a login page and insert the correct HTML.
  • I need to create an OpenID completion route.
  • I'll have to store user credentials in a database (or equivalent) somehow.

There are lots of different approaches to each of these. So in this case, I +decided to attack the problem by starting with the most general solution and +building more and more specialized solutions. The authenticate library itself +knows nothing about any web frameworks, and must be passed in GET and POST +parameters manually. It handles communications with the OpenID providers, and +introduces a type-safe interaction layer.

On top of that is yesod-auth, which includes an OpenID plugin. This does +fulfill the goal of "just say OpenID and it appears on the login page": the +YesodAuth typeclass has a member for declaring all of the plugins, and plugins +can provide HTML to be injected into the login page. However, there's still +quite a bit of machinery to getting yesod-auth itself running: integration with +the database, providing routes to access authentication, etc.

Finally, we have the Yesod scaffolding, which provides sensible defaults for +all of these. When you run the scaffolding, you have a fully functional +authentication system, and can quite trivially add in new authentication +options like OpenID.

What we have here is a very clear delineation of simplicity versus flexibility. +There are many use cases for which the scaffolding's authentication system +would not be appropriate, but for which you'd still want to have OpenID logins. +If you're using Yesod, you can stick with yesod-auth and still get a level of +simplicity. And if you're not using Yesod at all, authenticate is still +available to facilitate your OpenID logins, though you'll have to do a bit more +work (or rely on a different higher-level wrapper if available).

Designing libraries in this fashion can take a bit more effort, but in my +opinion providing reusable tools for the broader community is worth it. Beyond +any advantages for the community in general, this also makes code easier to +test, and a wider group of users can help push bug fixes and improved +featuresets even faster.

Summary

  • Figure out the problem you're actually trying to solve.
  • Determine the flexibility necessary to solve that problem.
  • Based on those required features, design as simple a programming experience as possible.
  • If it's possible to add extra flexibility without comprimising simplicity, do it.
  • Optimize as much as possible based on these constraints.
  • Document, making it clear what the recommended approaches are.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/01/new-goal-yesod-1-2.html b/public/blog/2013/01/new-goal-yesod-1-2.html new file mode 100644 index 00000000..2dd8b20c --- /dev/null +++ b/public/blog/2013/01/new-goal-yesod-1-2.html @@ -0,0 +1,39 @@ + New goal: Yesod 1.2 +

New goal: Yesod 1.2

January 17, 2013

GravatarBy Michael Snoyman

Since the Yesod 1.0 release, we've been very careful to try and avoid breaking +changes wherever possible. But occasionally, we still have a few things we want +to improve, whether it be for a more consistent API or better performance. So +I'm announcing the start of a Yesod 1.2 development goal to open another window +of opportunity for such breaking changes.

The goal here is still to maintain backwards compatibility most of the time. +Hopefully the vast majority of code out there will continue working without any +change, or at the most a few minor and clearly documented cases. So don't +consider this an opportunity to make massive and fundamental changes to Yesod: +now is not the time for it.

To keep track of these issues, I've created a new milestone in our Github +issue tracker, as well +as a new yesod1.2 branch. +Some of the issues open are fairly major (like refactoring the module structure +of yesod-core), while others (like adding an IsContent instance for +ResumableSource) are minor.

Obviously tackling one of those major issues will be quite involved, but I'd +like to encourage people who have never worked on the Yesod codebase before to +take this opportunity to get involved. Some of the issues available should be +fairly easy to implement, there's no pressing timetable on getting them +implemented, and you can ask for assistance in creating an implementation. If +you see an issue you'd like to tackle, please add a comment indicating so on +the issue.

This is also another opportunity for users to throw out ideas for what they'd +like to see improved. Please go ahead and open up new issues, or discuss these +things on the mailing list.

As for current users: Yesod 1.1 will continue to be supported in terms of bug +fixes for at least a few months after Yesod 1.2 is released, and if there is +demand, that support can continue. The overriding goal in this endeavor is the +smoothest transition possible for the Yesod community.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/01/school-of-haskell.html b/public/blog/2013/01/school-of-haskell.html new file mode 100644 index 00000000..c373b96e --- /dev/null +++ b/public/blog/2013/01/school-of-haskell.html @@ -0,0 +1,18 @@ + The School of Haskell +

The School of Haskell

January 31, 2013

GravatarBy Michael Snoyman

I'm quite excited to be able to announce that FP Complete is launching a new +product, the School of Haskell. We're now accepting beta signups. You can find +out much more in Bartosz's release +announcement.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/01/so-many-preludes.html b/public/blog/2013/01/so-many-preludes.html new file mode 100644 index 00000000..85d85f44 --- /dev/null +++ b/public/blog/2013/01/so-many-preludes.html @@ -0,0 +1,130 @@ + So many preludes! +

So many preludes!

January 2, 2013

GravatarBy Michael Snoyman

I'm happy to announce new versions of basic-prelude, classy-prelude, and +classy-prelude-conduit, as well as the addition of the new package +classy-prelude-yesod. +This is the first release of these packages which I consider stable enough for +general use, and I encourage people to have a look. You can also check out the +basic-prelude and +classy-prelude repos on Github.

Since it's been a while since I discussed classy-prelude, let's start with a +quick recap, beginning with the motivation: I think that the standard Prelude +is lacking in a few ways:

  1. It encourages some bad practices (e.g., the partial head function).
  2. It promotes certain datatypes (e.g., String) over better alternatives (e.g., Text).
  3. Some commonly used functions are not exported (e.g., mapMaybe).
  4. Since it sticks to concrete types in many cases (usually lists), it uses up the common namespace for function names (e.g., length), thereby requiring programmers to use qualified imports on a regular basis.

I think the first point stands on its own: there's a basic question we need to +ask ourselves about what we consider idiomatic Haskell code, and in my opinion, +partial functions is not a part of it. While that's an important point to +discuss, it's relatively straight-forward, so I won't be dwelling on it any +further.

The other three points rest around a central philosophy I have: programmers are +inherently lazy (in a good way), and will often choose the path of least +resistance. To demonstrate, consider the difference between using String and +Text for some simple concatenation:

-- String version
+name = firstName ++ " " ++ lastName
+
+-- Text version
+import qualified Data.Text as T
+name = firstName `T.append` " " `T.append` lastName

(Without OverloadedStrings, this would be even longer.) It's not that the +second version is really that much worse than the first, it's just slightly +less convenient. And due to that extra bit of work, Text gets used less +often. You can see the same thing with Map versus associated lists, Vector +and lists, and so on.

Note: As Herbert pointed out to me, with GHC 7.4 and up, you could just use +the <> operator provided by Data.Monoid instance of `T.append`. So +consider the case where you need to use some Prelude functions that require a +String.

-- String version
+main = putStrLn $ "Invalid name: " ++ name
+
+-- Text version
+import qualified Data.Text as T
+import qualified Data.Text.IO as TIO
+import Data.Monoid ((<>))
+main = TIO.putStrLn $ "Invalid name: " <> name

By comparison, in classy-prelude this becomes:

import ClassyPrelude
+main = putStrLn $ "Invalid name: " ++ name

If you think that my assessment so far doesn't warrant any changes to our +tooling, turn back now, this blog post isn't for you. If you are interested +in some kind of a solution to this issue, I have two options for you.

basic-prelude

Points 1-3 above can actually be solved pretty easily: just create a new +prelude module that has better defaults. BasicPrelude, provided by the +basic-prelude package, does +just this. It exports more helper functions, avoids partial functions, and +exports a bunch of the common datatypes, like ByteString and HashMap.

Another important premise of BasicPrelude is that it doesn't replace any +existing types or type classes. It reuses the exact same Monad typeclass that +is exported by Prelude. That means that code using BasicPrelude is +completely compatible with "normal" Haskell code. Another way to put it is that +BasicPrelude is a non-revolutionary approach to improving the Haskell +programming experience.

basic-prelude is actually split up into two modules: BasicPrelude and +CorePrelude. BasicPrelude simply re-exports everything provided by +CorePrelude, and then adds in some missing components. CorePrelude is +intended to be a foundation for the creation of other preludes. It was +originally part of classy-prelude, but was then separated out by Dan Burton, +who now maintains basic-prelude with me. CorePrelude tries to export +components that would be usable by all Prelude replacements. For now, our +simple barometer of this is "would both BasicPrelude and ClassyPrelude use +this?"

BasicPrelude sticks to monomorphic functions for the most part, with a strong +bias towards lists (just like standard Prelude). It doesn't really do much +that's controversial, and should be a great approach to try out for people +experimenting with an alternate prelude. And if you don't like something about +it, you can either file an issue, or just create your own fork. Due to the +design of basic-prelude, forking does not create incompatible code, so it's +not a high-impact move.

classy-prelude

classy-prelude is the more radical prelude. As mentioned, it builds on top of +CorePrelude, just like BasicPrelude does. The distinction, however, is that +instead of providing monomorphic, list-biased functions, it creates a slew of +typeclasses and provides polymorphic functions. Unlike many common typeclasses, +these typeclasses are not intended to be used in a polymorphic context +themselves, but rather to avoid the need to use qualified imports to +disambiguate names. In other words, we're using typeclasses for namespacing +purposes only.

(Despite this, we actually have a fairly thorough test suite covering the +behavioral laws of these type classes. So you could theoretically write +polymorphic code with classy-prelude, it's just not what I originally +intended.)

This namespacing approach was fairly uncommon (perhaps classy-prelude was the +first usage of it?) when I first started classy-prelude, and as a result I +was unsure how well it would turn out in practice. At this point, I've been +using classy-prelude for a number of my projects (both personal and at +work), and the approach is certainly viable. I +personally greatly prefer it to the non-classy approach, and will almost +certainly be using it for the foreseeable future- likely until we get a better +namespacing solution in GHC itself.

There are of course some downsides, some of which can be worked around:

  • Error messages become more confusing. I have no good solution to that right +now. I don't think the messages are too daunting for experienced Haskellers, +but I would not recommend classy-prelude to beginners.

  • In some cases it is impossible for the compiler to figure out which type +you mean. For example, the following monomorphic code is completely +unambiguous:

    import qualified Data.Map as Map
    +
    +foo :: Text -> Int
    +foo name =
    +    Map.lookup name people
    +  where
    +    people = Map.singleton "Michael" 28

    However, the equivalent classy code is problematic:

    foo :: Text -> Int
    +foo name =
    +    lookup name people
    +  where
    +    people = singleton "Michael" 28

    The problem is that both singleton and lookup are polymorphic, and are not +used in the result, so there's no way to know which container to use. +Fortunately, there's an easy workaround: the as* functions. In our case, we +just replace the last line with:

        people = asMap $ singleton "Michael" 28

    Overall, the code is still shorter. As an added bonus, you can now simply +switch asMap to asHashMap to swap which container structure you use.

  • In some cases, keeping the same name can go beyond the capabilities of the +type system. For example, when working on classy-prelude-yesod, +overloading insert for both its usage in Persistent and containers like Map +proved to be a bit problematic, specifically when not using the return value. +For example, the following code doesn't compile:

    _ <- runDB $ insert $ Person "Michael" 28

    The options in this case are either to not use the overloaded name and +instead use a separately named function (in this case, insertDB), or to use a +disambiguating helper function (voidKey) that fixes the types- similar to the asMap +function described above. Those two solutions look like:

    voidKey $ runDB $ insert $ Person "Michael" 28
    +runDB $ insertDB $ Person "Michael" 28

    I'm not sure yet how I feel about these two approaches, but it definitely +stresses the point that we're using the typeclass system in an extreme manner.

classy-prelude-conduit and classy-prelude-yesod

These two packages build on top of classy-prelude to provide even more common +functionality. The former provides conduit and xml-conduit functions, while +the latter adds on top of that yesod, persistent, http-conduit, and a few +other things. classy-prelude-yesod has not been as thoroughly exercised as +the other packages discussed here, so you're more likely to run into issues +with it.

Conclusion

These alternate preludes are not for everyone, but I think they offer a lot to +certain audiences. If you want to try out a smaller move, I'd recommend +BasicPrelude. If you want to be a bit more experimental, go for +classy-prelude. If you decide to drop either one at any point, it should not +be overly onerous to switch back to normal, monomorphic, qualified imports.

I'm definitely interested to hear people's experience with these packages. +There are still lots of improvements to be made, more common functionality to +be added, and documentation to be written. Now's a great time to get involved!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/02/ajax-with-scaffold.html b/public/blog/2013/02/ajax-with-scaffold.html new file mode 100644 index 00000000..af9558f2 --- /dev/null +++ b/public/blog/2013/02/ajax-with-scaffold.html @@ -0,0 +1,220 @@ + Using Ajax with a Scaffolded Site +

Using Ajax with a Scaffolded Site

February 5, 2013

GravatarBy Paul Rouse

Using Ajax with a Scaffolded Site

This is cookbook recipe, also available from +the Wiki.

A common scenario is that part of an application requires Ajax, while the rest +is based on dynamically generated HTML. The core functionality of Yesod +includes everything you need to implement this, but the scaffolding leans +more towards the HTML side, especially in its handling of login and its +reporting of errors. However, the default functions can easily be overridden +with more general ones which handle Ajax properly. This article explains how.

In what follows, I look at

  • Customising error handling to avoid sending HTML error pages in response +to an Ajax request.
  • Customising authentication to avoid or mitigate redirections when +responding to an Ajax request.
  • Testing using Yesod.Test.
  • An example handler and client-side code, to illustrate the choices on +which the rest of the discussion is based.

A caveat

To give concrete examples, I have had to make choices about the way Ajax +calls are done. To be specific, I use jQuery in about the simplest way +possible, which means that:

  • Data is sent to the server as a set of form fields, with one parameter +for each top-level attribute of the JavaScript object being sent.
  • The response can be text or JSON, as long as it is consistent with the +setting of the dataType in the Ajax call on the client side.
  • For an error response, with an HTTP error status, reading the body as +text is an easy option for the client.
  • The requests include a header X-Requested-With: XMLHttpRequest

You may want to make different choices, such as sending JSON data in all +requests and responses. The details will change, but the main points I am +talking about here will remain valid.

An example Yesod handler, and the corresponding client-side code, are given +in the appendix at the end to illustrate these choices, and generally +motivate the discussion.

I should also mention that issues +#478 and +#479 on GitHub discuss +related issues, but not really the detailed scenario I have chosen here.

Error Handler

The first problem to look at is error handling. The example code shown later +can raise a 404 error if the database entry is not found at all, and a +permissionDenied error if the user is trying to use someone else's +clipboard. Errors like these short-circuit the handler and return a result +using the errorHandler function of the Yesod instance.

The scaffolded site is set up to use the default error handler. This produces +a brief HTML message in a page formatted by defaultLayout, and is probably +not what we want in response to an Ajax request!

To change it, we need to implement the errorHandler method of the Yesod +typeclass, rather than using the default setting. It is normal Handler code, +and takes an ErrorResponse (see the documentation for Yesod.Handler), so we +need to write something like this in Foundation.hs:

import Network.HTTP.Types (mkStatus)
+import Network.Wai (Request(..))
+import Data.Text (append, pack, unwords)
+import Control.Monad (when)
+...
+instance Yesod App where
+    ...
+    errorHandler errorResponse = do
+        $(logWarn) (append "Error Response: "
+                           $ pack (show errorResponse))
+        req <- waiRequest
+        let reqwith = lookup "X-Requested-With" $ requestHeaders req
+            errorText NotFound = (404, "Not Found", "Sorry, not found")
+            errorText (InternalError msg) = (400, "Bad Request", msg)
+            errorText (InvalidArgs m) = (400, "Bad Request", unwords m)
+            errorText (PermissionDenied msg) = (403, "Forbidden", msg)
+            errorText (BadMethod _) = (405, "Method Not Allowed",
+                                            "Method not supported")
+        when (maybe False (== "XMLHttpRequest") reqwith) $ do
+            let (code, brief, full) = errorText errorResponse
+            sendResponseStatus
+                (mkStatus code brief)
+                $ RepPlain $ toContent $ append "Error: " full
+        defaultErrorHandler errorResponse

The critical part of this is recognising the Ajax call by the X-Requested-With +header. If you are using a different Ajax setup, you might need to look for +something different, of course, but this one works for jQuery.

In the Ajax case, we produce a plain text response, which is what I chose as +easiest in our example scenario - if you prefer JSON, then feel free to produce +it here. If the request is not Ajax, we call the defaultErrorHandler, +which is what would have been used if we had not done any of this.

If you have been watching, you will have noticed that I also sneaked in +some logging - it is entirely optional, but I think it is helpful.

Login

The other issue which arises when working with Ajax is the way redirection +occurs when a user needs to log in during an Ajax request.

The first thing that happens is a redirection to the login page. +This uses a 303 status code, and so cannot be trapped in JavaScript - the +browser itself automatically follows it. So we will be redirected to a +login page. More precisely, we arrive at the loginHandler of the YesodAuth +instance. Our problem, just as in the error handler, is that we do not want +it to generate an HTML login form if this happens when handling an Ajax request.

Therefore, in the YesodAuth instance, we implement the loginHandler, +rather than accepting the default, again in Foundation.hs.

instance YesodAuth App where
+    ....
+    loginHandler = do
+        tm <- getRouteToMaster
+        master <- getYesod
+        clearUltDest
+        req <- waiRequest
+        let reqwith = lookup "X-Requested-With" $ requestHeaders req
+        when (maybe False (== "XMLHttpRequest") reqwith) $ do
+            sendResponseStatus
+                (mkStatus 403 "Forbidden")
+                $ RepPlain $ toContent ("Login required" :: Text)
+        let title = renderMessage master ["en"] MsgSiteTitle
+        defaultLayout $ do
+            setTitleI title
+            mapM_ (flip apLogin tm) (authPlugins master)

As before, we recognise an Ajax request from the X-Requested-With header. +For Ajax we produce an error instead of a login page, leaving the client-side +code to decide how to tidy up and get the user to a place where they can log in.

There are a few subtleties here. Firstly, we use a 403 response in the Ajax +case. 401 would not be a good choice, since it would be an invitation to +do HTTP authentication, not session-based login. However 403 has the +complication that it also gets used for other permissionDenied errors +(see above). This is resolved by taking a little care to ensure that the +two cases are unambiguously distinguished by the start of the message, +so that the client code can handle them appropriately.

The next subtlety concerns the "ultimate destination" - see the +Sessions chapter of the Yesod Book. +The ultimate destination is set in a couple of places in Yesod, and only one +of them is controlled by the setting of redirectToReferer (in the +YesodAuth typeclass). In most situations we cannot avoid it being set. +The problem is that a successful login is followed by a redirection to the +ultimate destination, and in an Ajax situation, this can result in being +redirected, using GET, back to a resource which should be accessed with POST +or PUT, for example. To avoid this, we just clear the ultimate destination +in all cases - if you want to be more selective, remember that it is stored in +the session, so it survives between requests until it is cleared or used.

Finally, the call to defaultLayout generates the login form for the +non-Ajax case. It is essentially copied from the default login handler +declared in the YesodAuth typeclass in Yesod.Auth. I have moved few things +outside, and I have used a site title set in our messages file(s) rather +than the one from Yesod.Auth.Message. This is one place you can modify the +HTML generated, for example by wrapping each authentication widget in a +div with a recognisable id so that you can apply CSS styles.

Testing

It is a good idea to write some tests, and it is easy to do with Yesod.Test. +To construct an Ajax request (of the sort I have been dealing with), we need +to set up the parameters, and make an HTTP request which includes the correct +X-Requested-With header. Here is one reasonably general function for doing +it, which assumes that any data is provided as a Map whose keys mirror the +attributes of the JavaScript object used on the real client:

import qualified Data.Map as Map
+
+ajaxRequest :: StdMethod -> B.ByteString -> Map.Map Text Text
+               -> OneSpec conn ()
+ajaxRequest method url datacontent = do
+    let params = mapM_ (uncurry byName) $ Map.toList datacontent
+    doRequestHeaders (renderStdMethod method)
+                     url
+                     [("X-Requested-With", "XMLHttpRequest")]
+                     params

doRequestHeaders is a very recent addition to Yesod.Test. If you want to +use it at the moment, you will need to pick up the latest version from +GitHub - you can simply make a local copy of Test.hs and import that.

Yesod.Test gives us a way of getting at the raw response body. Any +decoding needs to be allowed for explcitly, which in our example means +remembering that the body is utf-8 encoded.

We write two separate top-level specs, so that we can control the +order - the ordering of the it specs within a single describe is +probably not what you expect.

testdata = "A unicode string\x2122"
+url = "/clipboard/..."   -- In a real example, get this from the server
+
+clipSpecs1 :: Specs
+clipSpecs1 = describe "The clipboard (part 1)" $ do
+    it "can be set" $ do
+      ajaxRequest PUT url $ Map.fromList [ ("clip", testdata) ]
+      statusIs 200
+
+clipSpecs2 :: Specs
+clipSpecs2 = describe "The clipboard (part 2)" $ do
+    it "can be read, producing the utf-8 encoding of what we stored" $ do
+      ajaxRequest GET url Map.empty
+      bodyEquals $ map (chr . fromIntegral)
+                       (B.unpack $ encodeUtf8 testdata)
+      statusIs 200

Actually that is not quite all: for our running example we need to log in in +each it spec. A recipe for packaging that is the subject of +another cookbook article.

Appendix: Example handler and client-side code

This simple handler, and matching JavaScript, motivates the assumptions made +in the discussion above, and also just might help someone get started!

In this example, users have to be logged in, and by the time we reach the +handler function, this has already been checked because of a suitable +definition of isAuthorized in the Yesod instance in Foundation.hs. +For more on authentication and authorization, see the +Yesod Book.

Each user has a persistent clipboard for cutting and pasting things, which for +our present purposes can be taken as unicode strings. The server simply stores +the data and returns it to the client when requested, so we need read and write +operations, which rather naturally map onto HTTP GET and PUT methods. They will +act on entries in a database table, defined like this in config/models:

ClipBoard
+    user UserId          -- Who owns this clipboard
+    data Text            -- The data
+    UniqueCbUser user

Elsewhere in the application, a single clipboard entry is set up for each user, +but for this example we can just assume that it exists.

The route is

/clipboard/#ClipBoardId ClipboardR GET PUT

and the handler code is

module Handler.Clipboard (
+    getClipboardR,
+    putClipboardR
+) where
+
+import Import
+import Yesod.Auth (requireAuthId)
+import Control.Monad (when)
+
+checkEntry :: ClipBoardId -> Handler ClipBoard
+checkEntry cbid = do
+    userid <- requireAuthId   -- Auth already checked, but we need userid
+    cb <- runDB $ get404 cbid
+    when (clipBoardUser cb /= userid) $
+        permissionDenied "Incorrect user - did you invent the URL?"
+    return cb
+
+getClipboardR :: ClipBoardId -> Handler RepPlain
+getClipboardR cbid = do
+    cb <- checkEntry cbid
+    return $ RepPlain $ toContent $ clipBoardData cb
+
+putClipboardR :: ClipBoardId -> Handler ()
+putClipboardR cbid = do
+    _ <- checkEntry cbid
+    d <- runInputPost $ ireq textField "clip"
+    runDB $ update cbid [ClipBoardData =. d]

There are a few things to notice here:

  • In the GET handler, the response is simply a utf-8 encoded string sent with +type text/plain (toContent does the utf-8 encoding for Text).
  • The PUT handler uses an "Input Form" (see the Forms chapter of the Yesod +Book). In this case there is a single +parameter to decode as Text, but if we had sent a more complex structure +on the JavaScript side, there would have been further form fields here.
  • Two errors can occur, both most likely caused by not following the official +URL. In a real application, the URL would have been provided by the server, +so these errors should not occur unless a user types a URL by hand.

Finally, glossing over the way the client gets hold of the URL, jQuery code +can send data like this:

$.ajax ({ type: "PUT",
+          url: "/clipboard/" + clipid,
+          data: { clip: clipdata },
+          dataType: "text",
+          success: function () { alert ("It worked") },
+          error:   function (jqxhr) {
+              alert ("error response: " + jqxhr.responseText);
+          }
+       });

and get it back again this way:

$.ajax ({ type: "GET",
+          url: "/clipboard/" + clipid,
+          dataType: "text",
+          success: function (data) {
+              alert ("The data was: " + data);
+          },
+          error:   function (jqxhr) {
+              alert ("error response: " + jqxhr.responseText);
+          }
+       });

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/02/announce-conduit-1-0-wai-1-4.html b/public/blog/2013/02/announce-conduit-1-0-wai-1-4.html new file mode 100644 index 00000000..15692f12 --- /dev/null +++ b/public/blog/2013/02/announce-conduit-1-0-wai-1-4.html @@ -0,0 +1,34 @@ + ANNOUNCE: conduit 1.0, wai 1.4, and more +

ANNOUNCE: conduit 1.0, wai 1.4, and more

February 14, 2013

GravatarBy Michael Snoyman

Anyone who was on #haskell today probably noticed an inordinately large +number of uploads from me today. Besides some dependencies getting major +version bumps, the motivators for this were two new releases:

  • conduit 1.0.0
  • wai 1.4.0

The former has been discussed quite a bit. For those unaware: this is a +mostly backwards-compatible update meant to make the library a bit more +accessible. If you're on the FP Complete School of Haskell, you can read the +online +introduction.

The second release was also very minor: it was the addition of a single field +to the Request datatype to track the size of the request body. You can see +the discussion on +web-devel. The +only point of contention was whether to use Maybe or a custom datatype. I +ultimately decided for the custom data type, for no particular strong reason.

What's really nice about these two updates is their lack of disruption: since +the API breakage is so small, most packages can work with either the old or new +version, and therefore upgrading is a simple matter.

Coming back to the upcoming Yesod 1.2: both of these releases are a good start +towards 1.2. I have some thoughts on improvements to some of the core +components of Yesod, and I'll hopefully be sharing those in the next few weeks. +In the meanwhile, we're still tracking some known feature requests on the +Github issue +tracker, +so if you want to get involved, pick an issue and implement it!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/02/authentication-for-testing.html b/public/blog/2013/02/authentication-for-testing.html new file mode 100644 index 00000000..bb757e1e --- /dev/null +++ b/public/blog/2013/02/authentication-for-testing.html @@ -0,0 +1,118 @@ + Authentication during Testing +

Authentication during Testing

February 6, 2013

GravatarBy Paul Rouse

Performing Authentication during Testing

This is cookbook recipe, also available from +the Wiki.

When testing a real application using Yesod.Test, each test case may need +to perform a login. This is not difficult, but as it involves several steps, +it is a little tedious to work through the details.

Here is some example code. It is designed around the user/password +authentication provided by Yesod.Auth.HashDB, but can probably be used +as a model for other authentication schemes.

The idea is to be able write test specs which go like this:

homeSpecs :: Specs
+homeSpecs = describe "The home page" $ do
+    it "requires login" $ do
+      needsLogin GET "/"
+
+    it "looks right" $ do
+      doLogin "testuser" "testpassword"
+      get_ "/"
+      statusIs 200
+      bodyContains "Welcome"

A few things may need changing for your particular usage:

  • The testRoot, which is probably the same as the approot for the +Testing environment in your settings.yml file
  • The "Login" string used to confirm we have reached the login page.

So with those caveats, here we are. Use it as you wish!

{-# LANGUAGE OverloadedStrings #-}
+module TestTools (
+    assertFailure,
+    urlPath,
+    needsLogin,
+    doLogin,
+    StdMethod(..),
+) where
+
+import TestImport
+import qualified Data.ByteString as B
+import Data.Text (Text, unpack, pack)
+import Data.Text.Encoding (encodeUtf8, decodeUtf8)
+import Network.URI (URI(uriPath), parseURI)
+import Network.HTTP.Types (StdMethod(..), renderStdMethod)
+
+-- Adjust as necessary to the url prefix in the Testing configuration
+testRoot :: B.ByteString
+testRoot = "https://test.host/"
+
+-- Force failure by swearing that black is white, and pigs can fly...
+assertFailure :: String -> OneSpec conn ()
+assertFailure msg = assertEqual msg True False
+
+-- Convert an absolute URL (eg extracted from responses) to just the path
+-- for use in test requests.
+urlPath :: Text -> Text
+urlPath = pack . maybe "" uriPath . parseURI . unpack
+
+-- Internal use only - actual urls are ascii, so exact encoding is
+-- irrelevant
+urlPathB :: B.ByteString -> B.ByteString
+urlPathB = encodeUtf8 . urlPath . decodeUtf8
+
+-- Stages in login process, used below
+firstRedirect :: StdMethod -> B.ByteString
+                 -> OneSpec conn (Maybe B.ByteString)
+firstRedirect method url = do
+    doRequest (renderStdMethod method) url $ return ()
+    extractLocation  -- We should get redirected to the login page
+
+assertLoginPage :: B.ByteString -> OneSpec conn ()
+assertLoginPage loc = do
+    assertEqual "correct login redirection location"
+                (testRoot `B.append` "/auth/login") loc
+    get_ $ urlPathB loc
+    statusIs 200
+    bodyContains "Login"
+
+submitLogin :: Text -> Text -> OneSpec conn (Maybe B.ByteString)
+submitLogin user pass = do
+    -- Ideally we would extract this url from the login form on the
+    -- current page
+    post (urlPathB testRoot `B.append` "/auth/page/hashdb/login") $ do
+        byName "username" user
+        byName "password" pass
+    extractLocation  -- Successful login should redirect to the home page
+
+extractLocation :: OneSpec conn (Maybe B.ByteString)
+extractLocation = do
+    statusIs 303
+    withResponse ( \ SResponse { simpleHeaders = h } ->
+                        return $ lookup "Location" h
+                 )
+
+-- Check that accessing the url with the given method requires login, and
+-- that it redirects us to what looks like the login page.
+--
+needsLogin :: StdMethod -> B.ByteString -> OneSpec conn ()
+needsLogin method url = do
+    mbloc <- firstRedirect method url
+    maybe (assertFailure "Should have location header")
+          assertLoginPage
+          mbloc
+
+-- Do a login (using hashdb auth).  This just attempts to go to the home
+-- url, and follows through the login process.  It should probably be the
+-- first thing in each "it" spec.
+--
+doLogin :: Text -> Text -> OneSpec conn ()
+doLogin user pass = do
+    mbloc <- firstRedirect GET $ urlPathB testRoot
+    maybe (assertFailure "Should have location header")
+          assertLoginPage
+          mbloc
+    mbloc2 <- submitLogin user pass
+    maybe (assertFailure "Should have second location header")
+          (assertEqual "Check after-login redirection" testRoot)
+          mbloc2

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/02/upcoming-conduit-1-0.html b/public/blog/2013/02/upcoming-conduit-1-0.html new file mode 100644 index 00000000..875b6d9b --- /dev/null +++ b/public/blog/2013/02/upcoming-conduit-1-0.html @@ -0,0 +1,232 @@ + Upcoming conduit 1.0 +

Upcoming conduit 1.0

February 14, 2013

GravatarBy Michael Snoyman

I've been contemplating, coding, discussing, and writing (unpublished) blog +posts about the next version of conduit- off and on- for a few months now. I'm +going to try to get all of the important points for discussion in this blog post, starting with the most important (and hopefully interesting) for +users and digressing further into background information.

Bad error message examples

I was planning on writing this blog post some time next week, but I realized +that this Google+ +post was +really something of a tease. But let me reiterate the same message I stated +over there:

I'm working with +Felipe Lessa and +Dan Burton on a new version of conduit (more details to follow soon hopefully). The main goal is a simplified user interface. One of the most difficult parts of that is making error messages more helpful.

Does anyone have examples of some conduit code that generates really unreadable error messages? We'd like to make sure the new code does better.

Motivation

The most important question we need to ask ourselves for this release is: why +are we doing it? That response is the guiding principle for all the work we're +doing, and will be the barometer for whether we're doing a good job or not.

The motivation for this release is simple: simplicity. There are a few pain +points in the library right now that have irked me (and others) since conduit +0.5 was released, and this is our chance to clean those up. Before explaining +those pain points, let me emphasize something: I did not say that we're +hoping to add some cool new features, increase performance, or cure cancer. +Those are all great things, but outside the scope of this release.

So the pain points in the interface that I'm aware of are:

  • conduit 0.5 introduced two interesting new features: optional leftovers and +upstream terminators. That balooned our core datatype (Pipe) to having six +type variables. Though users shouldn't have to deal with that type directly, +it's sometimes inevitable, especially with error messages. This is the biggest +pain point in the library that I'm aware of.

  • We have two competing interfaces for conduit: the original +Source/Sink/Conduit with the $$ family of operators, and the newer +pipes-inspired interface using runPipe and >+>. This duplication (though +warranted in the current setup due to optional leftovers and upstream +terminators) is confusing.

  • To try and mitigate this confusion, I created even more confusion. On top of +the core Source/Sink/Conduit datatypes, we also have general versions of +them. But we have to deal with a general version which has leftovers, and +one which doesn't have leftovers. And one which returns the upstream terminator, and one +that doesn't. This results in a total of 12 type synonyms. Said another +way:

    The number of type variables and type synonyms is too damn
+high.

  • The result is that it's difficult for users to know what the type signatures +of their code should be. "Is this a GLConduit, or a GInfConduit, or just a +Conduit?"

To put this in terms of +power, we currently +have too much flexibility in our library. We need to trade that in for some +simplicity.

An important constraint we have in this move is to avoid a major upheaval. +conduit has been a stable library for over half a year, and I don't want to +destroy that stability. So- as much as possible- this change should be +backwards compatible. This has played out very well in practice in my testing, +as I'll describe later.

The actual change

Note: You can see the current codebase on the producer branch of +Github.

I have to thank Felipe and Dan. I'd been playing around with around 6 different +ideas on how to solve this problem, and together we were finally able to sort +the wheat from the chaff. I believe the approach I'm going to describe is our +best option, and results in a very elegant library. We haven't completely +settled on this approach, but we're pretty close.

As you may have noticed from the problems listed above, optional leftovers and +upstream terminators cause a lot of complication. They happen to be great +features for reasoning about the internal workings of the library, but in +practice don't really help users out very much (I think I've only taken +advantage of upstream terminators in one really obscure piece of code). So the +first step is to get rid of those two features from the user-facing API.

We'll retain the Pipe datatype internally with all six type variables. However, +we'll introduce a new wrapper around it, ConduitM, which has only four type +variables: input from upstream, output to downstream, the underlying monad, and +the return value. Now the worst case scenario is that the user will see four +type variables in some error messages. While not ideal, it's definitely +manageable. We also considered a typeclass based approach (see below) which +theoretically would have given even better error messages, but the overall +comparison made this approach seem to be the winner.

It's now easy to define our three core synonyms in terms of this ConduitM datatype:

type Source m a = ConduitM () a m () -- no input values, no return value
+type Sink a m b = ConduitM a Void m b -- no output values
+type Conduit a m b = ConduitM a b m () -- no return value

This is great, but leaves us with the problem of creating general functions. +For example, suppose I want to write a Conduit such as the following:

sourceList :: [a] -> Source m a
+
+tripleOutput :: Conduit Int m Int
+tripleOutput = awaitForever $ \x -> sourceList [x, x, x]

This example won't type-check: since sourceList is a Source, its input type +is forced to be (). But tripleOutput has an input of type Int, so it +won't work. What we really want is to state that sourceList can work with +any kind of input, and then it can be used as either a Source or a +Conduit. On the consumption side, we have a similar dilemna: we want to state that +something consumes a stream of input and produces a return value, but can +output any value it wants.

This is a perfect use case for RankNTypes. We can state that something produces a stream of data without specifying its input with:

type Producer m a = forall i. ConduitM i a m ()

And similarly for consumption:

type Consumer a m b = forall o. ConduitM a o m b

Now we can give a type signature sourceList :: [a] -> Producer m a, and our +example type checks. And thus we have a full API based on one concrete type +(ConduitM) and five type synonyms: Source, Conduit, Sink, Producer and +Consumer. From a user perspective, you would almost always use Source, Conduit, +and Sink, unless you're creating functions which will be used in both a Source +and Conduit (or Sink and Conduit). The core conduit libraries would be set up +with the generic types when possible.

We're still actually debating the names for these last two synonyms. The other +option is to stick with the current nomenclature and call them GSource and +GSink. I'd be interested in the community's thoughts on this, I'm very much on +the fence right now.

Notice that we only need to create generalized versions of Sources and Sinks. +Conduits are already as generalized as they need to be, and thus we're not +discussing any form of GConduit type synonym.

Note: We have alternate approaches to the Producer/Consumer approach +spelled out below, specifically: explicit generalizing functions, typeclasses, +and using the ConduitM type directly. You can see their downsides listed below.

Measuring against our goals

So does this solution actually solve our stated goals? Let's see.

  • The user will never have to interact with the 6-variable Pipe datatype, +unless he/she wants to dig into the Internal module. Check.

  • The pipes interface needed to be present to allow users to deal with optional +leftovers and upstream terminators. Since those are no longer in the +user-facing API, we can relegate the pipes interface to the .Internal module as +well. Check.

  • We've replaced 12 type synonyms with just 5. Three of them are integral to +the library, so we've only added in 2 more, both of which have pretty clear +meanings. Mostly check.

  • We now have very clear guidelines on how user code should be written: use +Source/Sink/Conduit unless you really need something else, and then use +Producer/Consumer. Check.

Updating user code

I've made clear many times in the past that I have a strong bias towards +real-world code to back up any claims. For each and every approach mentioned in +this blog post, I've tried migrating the entire Yesod ecosystem over. Most of +the other approaches resulted in some kind of complication. In the case of +this approach, there were virtually no complications, except for code dealing +directly with the .Internal module. For everything else, the upgrade guide +basically goes:

  • If you're using the G*Conduit types, replace them with Conduit.
  • If you're using the G*Sink types, replace them with Consumer.
  • If you're using G*Source, replace it with Producer.

And yes, it really should be that simple. Since most user code should only be +dealing with Source/Conduit/Sink, no updating may be necessary. In fact, for +the majority of the Yesod ecosystem, I was able to get the libraries to +simultaneously compile with both conduit 0.5 and 1.0. So this should be the +lowest-impact conduit update to date!

One last point: I'm going to take this upgrade as an opportunity to finally +remove the long-deprecated Data.Conduit.Util.* modules, containing some long +outdated, harder-to-use, and less efficient helper functions. If you're still +using those modules, it's time to upgrade.

What didn't work

To help explain why we arrived at the solution above given some of its +limitations, I wanted to describe (in brief) some of the other approaches we +tried.

Just three types

In theory, we could just use the RankNTypes versions for Source and Sink as +well. Unfortunately, this results in quite a few unpleasant surprises:

  • One example is that normal function +composition +can fail due to the fact that type inference is brittle with RankNTypes. In one +place I had to replace id with (\x -> x).
  • Similarly, I needed to add a lot of type signatures all over the place, +something we shouldn't be forcing on users.
  • Error messages started to become pretty unreadable, referencing type +variables that the user never wrote.

To spell it out a bit further: having the RankNTypes values be returned as a +result of a function never caused any issues, but having a RankNTypes value as +an argument to the function caused lots of pain. So as much as I'd love to be +able to just stick to just three types for simplicity, it's a false +simplicity: the complexity has merely been moved elsewhere. I think having the +two additional type synonyms for use in special cases where they do not +cause problems is the appropriate trade-off.

Another failed attempt: typeclass

This is the approach I probably spent the most time on. It's pretty attractive: +error messages now mention things such as "Upstream m is not the same as Int", +which seems like a wonderful step forward. We can also have our conduit +functions work with arbitrary monad transformers on top of our +Source/Sink/Conduit types. And finally, Source/Sink/Conduit can all be newtype +wrappers, guaranteeing that error messages are always as concise as possible.

Unfortunately, the system fell apart:

  • We still run into the issue of generalizing code, so we must either resort +to ugly type +synonyms +or ugly type signature, e.g.:

    map :: MonadStream m
    +    => (Upstream m -> Downstream m)
    +    -> m ()
  • Once we generalize, the error messages are no longer pretty.

  • We can't use standard lift and liftIO, since we could be lifting +through an arbitrary number of layers. Instead, we ended up with +specialized liftStreamMonad and liftStreamIO.

Explicit generalizing calls

Most of the complication we run up against comes from generalizing functions. +Another possibility would be to just make generalizing functions to convert +Sources and Sinks into Conduits. Then Source/Sink/Conduit could be unique types +and error messages and type signatures are clear. However, this was a burden +that doesn't seem to make sense to put on users. It would be a major step +backwards in conduit usability from where we are now.

So the trade-off we have instead is two extra type synonyms, and error messages +may occasionally display the four-variable data type.

Just a single type

Forget the synonyms! We just have a single data type:

newtype Conduit input output m result

Then we'd have:

sourceFile :: FilePath -> Conduit i ByteString m ()
+map :: (a -> b) -> Conduit a b m ()

This is certainly workable, but subjectively we decided this was inferior to +the solution we ultimately came up with. I still think this is a good +contender, however, and can actually be achieved fully by just ignoring the +five type synonyms. I'd be interested what people think of this approach.

conduit's niche

I actually wrote most of the ideas for this section in its own blog +post, +but ultimately decided to just include a smaller section here at the end of +this post.

I get questions on a fairly regular basis about switching conduit for pipes or +io-streams (and in the past quite a few about comparing to enumerator). Those +packages are all in a similar solution space to conduit, but do not fully meet +its feature set. In some places, they provide functionality which we don't +require, and in others omit vital functionality.

The main purpose of this section is to spell out the design goals of conduit, +in particular through comparison with the other packages. Some particular +points about conduit:

  • conduit was not created in a vacuum. There was a large body of existing code +and features we wished to add to it, and based on those requirements we created +conduit. The pipes package in particular took a much more abstract approach in +design. I have no objections to that approach, but it does result in quite a +different set of trade-offs.

    For example, conduit has never claimed to follow Category laws, and in fact it +does not. pipes is quite strict in its adherence. One difference that came to +light recently was prompt finalization: in some cases, following the Category +laws results in delayed finalization. For conduit and its pragmatic approach, +this is unacceptable. For pipes, deviating from Category laws is unacceptable. +Both approaches are valid, but also mutually exclusive, and conduit is +unapologetic about its choices.

  • As is hopefully obvious from this blog post, conduit is focused on creating the +most user-friendly API possible for its feature set. To achieve that, we'll +bundle in the functionality that we support in a single set of operations. +Leftovers and finalizers are bundled into the core datatype, so that users do +not need to combine multiple concepts to get a working whole.

    Note that there have been some claims about other libraries being simpler than +conduit. I agree that the type variable situation was overly complicated, but +given that we're addressing that problem now, I believe conduit is the simplest +library to use for its problem domain. Like io-streams, it is based on three +primitives (await, yield, and leftover, which perfectly mirror their read, +write, and unRead). In addition, conduit provides a robust library of helper functions to deal +with common use cases.

  • Composability is a requirement. I disagree with the assertion that composable +code == Category instance: composable means code can easily be reused in a +logical way. enumerator provides this with its concept of Enumeratees, which +can be combined with both Enumerators and Iteratees, for example. pipes and +conduit make composability a first-class citizen.

    However, I think io-streams is not providing this adequately. When you create +a transformer in io-streams, it must be focused on either an InputStream or an +OutputStream, but cannot work on both.

  • connect-and-resume is absolutely vital in a number of complicated use cases, +such as combining a web server and client to create an HTTP proxy. It's a major +feature of conduit, and was actually the motivating case for creating conduit +in the first place. I believe io-streams could provide this same functionality, +but enumerator and pipes certainly don't.

  • We need to have exception safety. Whether you love them or hate them, +exceptions are a reality of programming, and we need to deal with them. I +consider ResourceT to be a very good solution to the problem. But contrary to +some claims, ResourceT is not a prerequisite of exception safety in conduit. +You can, for example, write:

    import Data.Conduit (($$))
    +import Data.Conduit.Binary (sourceHandle, sinkHandle)
    +import System.IO (withBinaryFile, IOMode (..))
    +
    +main =
    +    withBinaryFile "input.txt" ReadMode $ \input ->
    +    withBinaryFile "output.txt" WriteMode $ \output ->
    +    sourceHandle input $$ sinkHandle output

    However, ResourceT provides quite a bit of flexibility that the standard +bracket pattern does not allow, such as interleaved resource cleanup. That's +why instead of considering ResourceT a hack, I consider it a great solution to +the problem.

  • While conduit is designed primarily for I/O, it should support pure code as +well. This is great for testing, and for creating libraries like xml-conduit +which can- in a memory-efficient and resource-friendly manner- parse both +in-memory and I/O-based documents. pipes and enumerator allow for this, but +io-streams has a distinct I/O bias. (There's nothing wrong with targeting a +specific use case, but it does exclude others.)

  • We want support for transformer stacks. Monad transformers are a great way to +structure code and deal with complexity. I am absolutely of the opinion that by +getting rid of support for monad transformers, we would simply be moving +complexity from our library to the user. This is another case where only +io-streams lacks support. And frankly, I don't believe that supporting a +transformer stack adds any significant complexity to a library, so it seems to +me as a bad trade-off.

On the other hand, there are some features which other libraries have which we +don't. Some interesting things from other approaches:

  • enumerator is the only approach which- without some external control structure +like bracket or ResourceT- gives exception-safe resource handling for the data +producer. It's in fact a major part of the design philosophy, and I don't think +people give that enough credit. I think overall the complexity trade-off needed +to achieve this is too high, but there's no doubt that it's a feature that +others don't have.

  • pipes has been adding new features, like bidirectionality. I haven't seen solid +real-world use cases that would benefit greatly from it, but I am interested in +seeing how it progresses. Similarly, pipes recently added some support for +handling exceptions inside a pipeline. In my experience, it has always made +more sense to handle the exceptions outside the pipeline, but I'm interested to +see how this progresses. I think the dual nature of pragmaticism in conduit and +research and experimentation in pipes has already reaped great benefits, and +will continue to do so.

  • io-streams has introduced a replacement for the Handle system for lower-level +I/O. This is certainly something we can consider switching to in the future. +For the most part, conduit just uses the standard Haskell I/O infrastructure +(with exceptions to deal with file locking on Windows). I wouldn't be surprised +to see an io-streams-conduit package in the future.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/03/big-subsite-rewrite.html b/public/blog/2013/03/big-subsite-rewrite.html new file mode 100644 index 00000000..6b95d110 --- /dev/null +++ b/public/blog/2013/03/big-subsite-rewrite.html @@ -0,0 +1,100 @@ + Big Subsite Rewrite +

Big Subsite Rewrite

March 15, 2013

GravatarBy Michael Snoyman

In my previous blog +post, I +alluded to some more radical changes I'd been playing with for the Yesod 1.2 +release. Initially, I thought I wouldn't be including them for now, but after +some experimentation, I +think I'm going to go ahead and include this.

For a quick overview of what's happening, you can look at the updates I've made to the following codebases:

I've also been keeping the detailed +changelog +up-to-date with the coming changes. And if you just want to jump ahead and see +what a subsite looks like, go the the end of the blog +post.

Irritations

If it ain't broke, don't fix it. So let's ask the question: what's wrong with +Yesod 1.1, and in particular the subsite integration? The following are some +annoyances I've had with it for quite a while:

  • GHandler and GWidget both take type parameters for both the subsite and the master site, even if you aren't writing a subsite.
    • This added complication appears everywhere.
    • It's unclear whether users should write code that says GHandler sub App () or GHandler App App () (the latter having the convenient synonym Handler ()).
    • If you use the latter, your code is more readable, but may cause problems if you later decide to use a subsite.
    • Even if you use the prettier Handler wrapper, error messages will still mention GHandler App App.
  • Speaking of error messages, we had really ugly onces back in the old days of the GHandlerT monad transformers (anyone remember GHandlerT sub App (Control.Monad.Trans.Resource.ResourceT IO)?). To work around this, GHandler and GWidget are no longer monad transformers.
    • But we still want to be able to pretend that they're transformers, so we have a custom lift function.
    • We can't do useful things like restrict the actions a handler can do by changing its underlying monad.
  • We have this weird arbitrary encoding of master and subsite. What if a subsite wants to have a subsite? Well, it's possible, but it's quite confusing how that fits in with datatypes that only mention two site parameters.
  • Many functions (like getCurrentRoute) refer to the subsite instead of the master site. As a result, you need to play around with things like getRouteToMaster even if you're not writing a subsite.

And then of course the main issue everyone probably has with subsites: they're +too confusing to create! Actually, I think this is mostly a documentation +issue, with a little bit of Template Haskell woes thrown in. We'll get to that +later, but for now I want to talk about how we're going to change the core of +Yesod itself to allow for better subsites.

Back to transformers

Let's dive in with the new and improved approach:

newtype HandlerT site m a
+newtype WidgetT  site m a

That's basically the whole change I'm talking about. Instead of a subsite and +master site parameter, we have a single site parameter. And instead of +hard-coding a specific underlying monad, the monad is a parameter again. This +immediately solves all the custom lift nonsense. And clearly, if you're not +writing subsite code, you won't even have to think about subsites, because +there is no subsite!

So now, if I'm writing the application App, my handler functions look like:

myAppHandler :: HandlerT App IO RepHtml

And just like now, we'll have the convenient Handler type synonym, now defined as:

type Handler = HandlerT App IO

So for the majority of user code just using the Handler synonym, nothing has +to change.

But that just begs the question: how do you write code that lives in a +subsite? Easy: you stack the transformers. Suppose you're working in the Auth +subsite; a handler function would look something like this:

myAuthHandler :: HandlerT Auth (HandlerT App IO) RepHtml

Or more generally, you could allow myAuthHandler to work with any master +site:

myAuthHandler :: HandlerT Auth (HandlerT master IO) RepHtml

Or you could require that the master site implement some kind of interface via +typeclasses:

myAuthHandler :: YesodAuth master => HandlerT Auth (HandlerT master IO) RepHtml

There's no longer any confusion about "which site" a function lives in, since +HandlerT only knows about a single site. So getCurrentRoute just returns a +route, not a "subsite route" or a "master site route". getYesod returns the +site foundation type, not sub or master site.

But if you're writing a subsite, you might need to access information from the +parent. But that's now trivial: just use lift. For example:

myAuthHandler :: HandlerT Auth (HandlerT App IO) RepHtml
+myAuthHandler = do
+    auth <-      getYesod -- returns an Auth
+    app  <- lift getYesod -- returns an App

This change requires some code rewriting for upgrading, but after working on it +a while I believe that the trade-off is worth it.

Where's ResourceT?

If you're paying close attention, you might be wondering where ResourceT went. +The answer is that it's actually embedded inside of HandlerT. In fact, I just +added some code to +resourcet +to make this kind of usage more efficient. The reasons I went for this +embedding are:

  • Avoiding an extra transformer layer can mean increased performance.
  • Type signatures and error messages will stay simpler.

This means that, unlike Yesod 1.1, using lift inside a Handler will not +let you run a ResourceT IO action. Instead, you'll want to use +liftResourceT.

And what about WidgetT

I've followed almost exactly the same formulation for WidgetT. And if you're +not writing subsites, there's really only one other thing you need to know. +Instead of lifting handler actions into your WidgetT, most handler actions +live in typeclasses now, so they can be automatically used in a Widget. If you +do have a Handler function that you want to lift, you'll need to use +handlerToWidget. (That might get changed to liftHandlerT instead, but +that's a different discussion...)

For subsites, the situation is a little bit more tricky. To understand why, +consider the new type signature for the defaultLayout method:

defaultLayout :: WidgetT site IO () -> HandlerT site IO RepHtml

Now consider that you're writing a widget in a subsite that refers to a route +in the subsite. This might look something like:

[whamlet|<a href=@{LoginR}>Please login|]

Since LogingR is a route in the Auth subsite, the type of that widget is +WidgetT Auth IO (). But if we're working on the App site, defaultLayout +expects a WidgetT App IO (). How do we reconcile the two? I've come up with two approaches.

  1. Never created subsite widgets. Instead, whenever you want to embed a +subsite URL, you convert it to a master site URL. Then, you can apply +defaultLayout and lift its result: For example:

    toParent <- getRouteToParent
    +lift $ defaultLayout [whamlet|<a href=@{toParent LoginR}>Please login|]
  2. Provide a helper function to lift up a widget into the parent site. This looks something like:

    liftWidget :: WidgetT child IO a
    +           -> HandlerT child (HandlerT parent m) (WidgetT parent m a)

    Then our example would be:

    widget <- liftWidget [whamlet|<a href=@{LoginR}>Please login|]
    +lift $ defaultLayout widget

So far, I favor the first, as it's pretty close to what we do already. This +still doesn't feel as elegant as I'd like it to be. However, in its favor, this +approach doesn't really perform any magic: it's fairly obvious what each step +is doing. And for the most part, this will be boilerplate stuff that all +subsites use, so it should be something that can be learnt once and reused.

Putting it together

So how do you actually create a subsite? It's much like creating a normal site, with a few differences:

  1. Use mkYesodSubData to create your route datatype and rendering function.
  2. Create a YesodSubDispatch instance, using the mkYesodSubDispatch TH function to generate the dispatch function itself.
  3. Instead of a plain Handler, you'll have a Handler stack, something like: HandlerT SubSite (HandlerT master IO).

I've put together a subsite +demo which +demonstrates how to create a subsite. Here are some important excerpts from +that demo.

Creating routes is very similar to how you do so for a master site. Instead of +mkYesod, you use mkYesodSubData:

mkYesodSubData "Wiki" [parseRoutes|
+/ WikiHomeR GET
+/read/*Texts WikiReadR GET
+/edit/*Texts WikiEditR GET POST
+|]

To set up dispatch, you must create an instance of YesodSubDispatch, using the +mkYesodSubDispatch TH helper function to generate the actual code. You can put +whatever restrictions you want on the master site by changing the typeclass +constraints.

instance YesodWiki master => YesodSubDispatch Wiki (HandlerT master IO) where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesWiki)

As described above, you have to remember to lift some functions and convert +subsite routes to the parent site, e.g.:

toParent <- getRouteToParent
+lift $ defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{toParent $ WikiReadR page}>Read page
+    |]

There is definitely still some room for improvement here. We can probably find +some common patterns to be abstracted out, like "apply defaultLayout and +convert all the routes". But overall, this approach feels much cleaner to me +than what we have currently. And most importantly, it takes the complexity out +of the majority of apps entirely.

In my opinion, the only downside with this change is its breaking nature, but +hopefully most breakage can be mechanically fixed (e.g., replace GHandler Foo +Foo with HandlerT Foo IO). If people see other problems, or think I'm +understating the effect of the breakage, please bring it up.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/03/resourcet-overview.html b/public/blog/2013/03/resourcet-overview.html new file mode 100644 index 00000000..16075516 --- /dev/null +++ b/public/blog/2013/03/resourcet-overview.html @@ -0,0 +1,158 @@ + ResourceT Overview +

ResourceT Overview

March 7, 2013

GravatarBy Michael Snoyman

Continuing the pattern I started with conduit, I've written up a resourcet package overview on the School of Haskell. For convenience, here's the content of the tutorial. But for the best experience, I recommend reading the School of Haskell version, which includes active code.


Proper exception handling, especially in the presence of asynchronous +exceptions, is a non-trivial task. But such proper handling is absolutely vital +to any large scale application. Leaked file descriptors or database connections +will simply not be an option when writing a popular web application, or a high +concurrency data processing tool. So the question is, how do you deal with it?

The standard approach is the bracket pattern, which appears throughout much of +the standard libraries. withFile uses the bracket pattern to safely wrap up +openFile and closeFile, guaranteeing that the file handle will be closed no +matter what. This approach works well, and I highly recommend using it.

However, there's another approach available: the +resourcet package. If the +bracket pattern is so good, why do we need another one? The goal of this post +is to answer that question.

What is ResourceT

ResourceT is a monad transformer which creates a region of code where you can safely allocate resources. Let's write a simple example program: we'll ask the user for some input and pretend like it's a scarce resource that must be released. We'll then do something dangerous (potentially introducing a divide-by-zero error). We then want to immediately release our scarce resource and perform some long-running computation.

import Control.Monad.Trans.Resource
+import Control.Monad.IO.Class
+
+main = runResourceT $ do
+    (releaseKey, resource) <- allocate
+        (do
+            putStrLn "Enter some number"
+            readLn)
+        (\i -> putStrLn $ "Freeing scarce resource: " ++ show i)
+    doSomethingDangerous resource
+    liftIO $ putStrLn $ "Going to release resource immediately: " ++ show resource
+    release releaseKey
+    somethingElse
+    
+doSomethingDangerous i =
+    liftIO $ putStrLn $ "5 divided by " ++ show i ++ " is " ++ show (5 `div` i)
+    
+somethingElse = liftIO $ putStrLn
+    "This could take a long time, don't delay releasing the resource!"

Try entering a valid value, such as 3, and then enter 0. Notice that in both cases the "Freeing scarce resource" message it printed. And by using release before somethingElse, we guarantee that the resource is freed before running the potentially long process.

In this specific case, we could easily represent our code in terms of bracket with a little refactoring.

import Control.Exception (bracket)
+
+main = do
+    bracket
+        (do
+            putStrLn "Enter some number"
+            readLn)
+        (\i -> putStrLn $ "Freeing scarce resource: " ++ show i)
+        doSomethingDangerous
+    somethingElse
+doSomethingDangerous i =
+    putStrLn $ "5 divided by " ++ show i ++ " is " ++ show (5 `div` i)
+    
+somethingElse = putStrLn
+    "This could take a long time, don't delay releasing the resource!"

In fact, the bracket version is cleaner than the resourcet version. If so, why bother with resourcet at all? Let's build up to the more complicated cases.

bracket in terms of ResourceT

The first thing to demonstrate is that ResourceT is strictly more powerful than bracket, in the sense that:

  1. bracket can be implemented in terms of ResourceT.
  2. ResourceT cannot be implemented in terms of bracket.

The first one is pretty easy to demonstrate:

import Control.Monad.Trans.Resource
+import Control.Monad.Trans.Class
+
+bracket alloc free inside = runResourceT $ do
+    (_releaseKey, resource) <- allocate alloc free
+    lift $ inside resource
+    
+main = bracket
+    (putStrLn "Allocating" >> return 5)
+    (\i -> putStrLn $ "Freeing: " ++ show i)
+    (\i -> putStrLn $ "Using: " ++ show i)

Now let's analyze why the second statement is true.

What ResourceT adds

The bracket pattern is designed with nested resource allocations. For example, consider the following program which copies data from one file to another. We'll open up the source file using withFile, and then nest within it another withFile to open the destination file, and finally do the copying with both file handles.

{-# START_FILE main.hs #-}
+import System.IO
+import qualified Data.ByteString as S
+
+main = do
+    withFile "input.txt" ReadMode $ \input ->
+      withFile "output.txt" WriteMode $ \output -> do
+        bs <- S.hGetContents input
+        S.hPutStr output bs
+    S.readFile "output.txt" >>= S.putStr
+{-# START_FILE input.txt #-}
+This is the input file.

But now, let's tweak this a bit. Instead of reading from a single file, we want to read from two files and concatenate them. We could just have three nested withFile calls, but that would be inefficient: we'd have two Handles open for reading at once, even though we'll only ever need one. We could restructure our program a bit instead: put the withFile for the output file on the outside, and then have two calls to withFile for the input files on the inside.

But consider a more complicated example. Instead of just a single destination file, let's say we want to break up our input stream into chunks of, say, 50 bytes each, and write each chunk to successive output files. We now need to interleave allocations and freeings of both the source and destination files, and we cannot statically know exactly how the interleaving will look, since we don't know the size of the files at compile time.

This is the kind of situation that resourcet solves well (we'll demonstrate in the next section). As an extension of this, we can write library functions which allow user code to request arbitrary resource allocations, and we can guarantee that they will be cleaned up. A prime example of this is in WAI (Web Application Interface). The user application may wish to allocate some scarce resources (such as database statements) and use them in the generation of the response body. Using ResourceT, the web server can guarantee that these resources will be cleaned up.

Interleaving with conduit

Let's demonstrate the interleaving example described above. To simplify the code, we'll use the conduit package for the actual chunking implementation. Notice when you run the program that there are never more than two file handles open at the same time.

import           Control.Monad.IO.Class (liftIO)
+import           Data.Conduit           (addCleanup, runResourceT, ($$), (=$))
+import           Data.Conduit.Binary    (isolate, sinkFile, sourceFile)
+import           Data.Conduit.List      (peek)
+import           Data.Conduit.Zlib      (gzip)
+import           System.Directory       (createDirectoryIfMissing)
+
+-- show
+-- All of the files we'll read from
+infiles = map (\i -> "input/" ++ show i ++ ".bin") [1..10]
+
+-- Generate a filename to write to
+outfile i = "output/" ++ show i ++ ".gz"
+
+-- Monad instance of Source allows us to simply mapM_ to create a single Source
+-- for reading all of the files sequentially.
+source = mapM_ sourceFileTrace infiles
+
+-- The Sink is a bit more complicated: we keep reading 30kb chunks of data into
+-- new files. We then use peek to check if there is any data left in the
+-- stream. If there is, we continue the process.
+sink =
+    loop 1
+  where
+    loop i = do
+        isolate (30 * 1024) =$ sinkFileTrace (outfile i)
+        mx <- peek
+        case mx of
+            Nothing -> return ()
+            Just _ -> loop (i + 1)
+
+-- Putting it all together is trivial. ResourceT guarantees we have exception
+-- safety.
+transform = runResourceT $ source $$ gzip =$ sink
+-- /show
+
+-- Just some setup for running our test.
+main = do
+    createDirectoryIfMissing True "input"
+    createDirectoryIfMissing True "output"
+    mapM_ fillRandom infiles
+    transform
+
+fillRandom fp = runResourceT
+              $ sourceFile "/dev/urandom"
+             $$ isolate (50 * 1024)
+             =$ sinkFile fp
+
+
+-- Modified sourceFile and sinkFile that print when they are opening and
+-- closing file handles, to demonstrate interleaved allocation.
+sourceFileTrace fp = do
+    liftIO $ putStrLn $ "Opening: " ++ fp
+    addCleanup (const $ liftIO $ putStrLn $ "Closing: " ++ fp) (sourceFile fp)
+
+sinkFileTrace fp = do
+    liftIO $ putStrLn $ "Opening: " ++ fp
+    addCleanup (const $ liftIO $ putStrLn $ "Closing: " ++ fp) (sinkFile fp)

resourcet is not conduit

resourcet was originally created in the process of writing the conduit package. +As a result, many people have the impression that these two concepts are +intrinsically linked. In fact, this is not true: each can be used separately +from the other. The canonical demonstration of resourcet combined with conduit +is the file copy function:

import Data.Conduit
+import Data.Conduit.Binary
+
+fileCopy src dst = runResourceT $ sourceFile src $$ sinkFile dst
+
+main = do
+    writeFile "input.txt" "Hello"
+    fileCopy "input.txt" "output.txt"
+    readFile "output.txt" >>= putStrLn

However, since this function does not actually use any of ResourceT's added functionality, it can easily be implemented with the bracket pattern instead:

import Data.Conduit
+import Data.Conduit.Binary
+import System.IO
+
+fileCopy src dst = withFile src ReadMode $ \srcH ->
+                   withFile dst WriteMode $ \dstH ->
+                   sourceHandle srcH $$ sinkHandle dstH
+
+main = do
+    writeFile "input.txt" "Hello"
+    fileCopy "input.txt" "output.txt"
+    readFile "output.txt" >>= putStrLn

Likewise, resourcet can be freely used for more flexible resource management without touching conduit. In other words, these two libraries are completely orthogonal and, while they complement each other nicely, can certainly be used separately.

Conclusion

ResourceT provides you with a flexible means of allocating resources in an exception safe manner. Its main advantage over the simpler bracket pattern is that it allows interleaving of allocations, allowing for more complicated programs to be created efficiently. If your needs are simple, stick with bracket. If you have need of something more complex, resourcet may be your answer.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/03/simpler-streaming-responses.html b/public/blog/2013/03/simpler-streaming-responses.html new file mode 100644 index 00000000..f4c02ce9 --- /dev/null +++ b/public/blog/2013/03/simpler-streaming-responses.html @@ -0,0 +1,254 @@ + Simpler streaming responses +

Simpler streaming responses

March 27, 2013

GravatarBy Michael Snoyman

Yesod is built on top of WAI, which has always provided a means of creating +efficient, streaming responses. Throughout Yesod's development, this +functionality has always been present in one form or another. In Yesod 1.2, the +goal is to make it as simple as possible to leverage this functionality.

Let's kick off with a simple example, and then drill into the details:

{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies #-}
+import Yesod.Core
+import Data.Conduit
+import qualified Data.Conduit.Binary as CB
+import Control.Concurrent.Lifted (threadDelay)
+import Data.Monoid ((<>))
+import qualified Data.Text as T
+import Control.Monad (forM_)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+fibs :: [Int]
+fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
+
+getHomeR :: Handler TypedContent
+getHomeR = do
+    value <- lookupGetParam "x"
+    case value of
+        Just "file" -> respondSource typePlain $ do
+            sendChunkText "Going to read a file\n\n"
+            CB.sourceFile "streaming.hs" $= awaitForever sendChunkBS
+            sendChunkText "Finished reading the file\n"
+        Just "fibs" -> respondSource typePlain $ do
+            forM_ fibs $ \fib -> do
+                $logError $ "Got fib: " <> T.pack (show fib)
+                sendChunkText $ "Next fib is: " <> T.pack (show fib) <> "\n"
+                yield Flush
+                sendFlush
+                threadDelay 1000000
+        _ -> fmap toTypedContent $ defaultLayout $ do
+            setTitle "Streaming"
+            [whamlet|
+                <p>Notice how in the code above we perform selection before starting the stream.
+                <p>Anyway, choose one of the options below.
+                <ul>
+                    <li>
+                        <a href=?x=file>Read a file
+                    <li>
+                        <a href=?x=fibs>See the fibs
+            |]
+
+main = warp 3000 App

Start simple: a standard response

Consider the following handler:

getHomeR :: Handler Text
+getHomeR = return "Hello World!"

What exactly does Yesod do to make this into a response the client can see? The +important bit is the ToTypedContent typeclass. Every handler function has +toTypedContent applied to its result. So let's look at the relevant classes +and types.

type ContentType = ByteString
+data Content = ContentBuilder !Blaze.Builder !(Maybe Int)
+               -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush Blaze.Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+
+data TypedContent = TypedContent !ContentType !Content
+class ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+
+-- Relevant instance
+instance ToTypedContent Text where
+    toTypedContent t = TypedContent
+        "text/plain; charset=utf-8"
+        (\t -> ContentBuilder (Blaze.fromText t) Nothing)

So every response has to be convertible to a TypedContent, which is two +pieces of information: the value for the Content-Type response header, and +the body fo the response. In our case, we use the ContentBuilder constructor, +which lets us leverage blaze-builder.

Use the Source

ContentBuilder isn't our only option. We could serve a file with +ContentFile. ContentDontEvaluate is a modifier to deal with exceptions; +we'll discuss that a bit later. But for our streaming discussion, the most +interesting constructor is ContentSource. This uses a conduit Source for +creating streaming data. Let's try out a minimal example:

getHomeR :: Handler TypedContent
+getHomeR = return $ TypedContent "text/plain" $ ContentSource $ do
+    yield $ Chunk $ Blaze.fromText "Hello World!"

We can use the TypedContent and ContentSource constructors directly. The +result isn't really anything more impressive than what we had previously. Let's +improve that, by streaming two files consecutively:

getHomeR :: Handler TypedContent
+getHomeR = return $ TypedContent "text/plain" $ ContentSource $ do
+    mapOutput (Chunk . Blaze.fromByteString) $ sourceFile "file1.txt"
+    mapOutput (Chunk . Blaze.fromByteString) $ sourceFile "file2.txt"

We're guaranteed that our response will live in constant memory and will +properly free resources. We have to play with mapOutput, Chunk and +fromByteString to convert a stream of ByteStrings to a stream of flushable +Builders.

Make it prettier

Having to muck around with those lower-level details isn't fun. Let's bump it +up a level:

getHomeR :: Handler TypedContent
+getHomeR = respondSource "text/html" $ do
+    sendChunk ("Some Text" :: Text)
+    sendChunk ("Hello & Goodbye" :: Html)

respondSource wraps up the tedium of dealing with the constructors directly. +sendChunk will send a chunk of content to the user, and can take as an +argument most common textual types (String, strict/lazy Text, strict/lazy +ByteString, and Html). But this doesn't play very nicely with overloaded +strings, since you need to provide explicit annotations. So we also have simple +type-specified wrappers as well:

getHomeR :: Handler TypedContent
+getHomeR = respondSource "text/html" $ do
+    sendChunkText "Some Text"
+    sendChunkHtml "Hello & Goodbye"

We can also use sendFlush to flush the buffer to the client immediately. And +we have the ability to use all common conduit concepts to build up our +Source.

And one final but important point: the base monad for the Source is +Handler, so you can perform arbitrary Handler operations inside your +Source, such as looking up query string parameters.

Exceptions

Let's go back to non-streaming responses. Consider the following:

getHomeR :: Handler Html
+getHomeR =
+    return $ "Hello " <> name <> "!"
+  where
+    name = error "Oops, forgot to set the name"

We have an exception being sent from pure code. Let's see what Yesod does with this:

Pure exception

This is the result we want: the user receives a 500 status code to indicate +that there was an error on the server. But how does this work? The pure +exception should only be discovered after we already send our 200 status +code and response headers, right?

In fact, Yesod does some fancy footwork behind the scenes, and fully evaluates +pure response bodies before sending any data to the user, specifically to +ensure that the user gets proper response codes. And this is also the purpose +of the above-mentioned ContentDontEvaluate constructor: to give the user a +chance to override this behavior (e.g., for efficiency). For example, we can +modify our above code to read:

getHomeR :: Handler (DontFullyEvaluate Html)
+getHomeR =
+    return $ DontFullyEvaluate $ "Hello " <> name <> "!"
+  where
+    name = error "Oops, forgot to set the name"

When run like this, the client receives an empty response from the server +instead.

"All very interesting," you might be saying, "but what does this have to do +with streaming responses?" Quite a bit, actually, as the same reasoning +applies. When using streaming responses, there's no way for Yesod to fully +evaluate your response body before sending them to the client. So if you throw +an exception in your Source, the client will get a corrupted response. This +isn't to say you shouldn't use streaming responses, but you have to be careful.

Logic before streaming

Exceptions aren't the only issue. You can't modify the status code or response +headers at all once you're inside the Source. That means you can't perform +redirects, can't modify the session, or can't switch from a 200 OK response to +a 403 Forbidden response. The important point here is to perform your logic +before streaming.

getHomeR :: Handler TypedContent
+getHomeR = do
+    maybeFoo <- lookupGetParam "foo"
+    case maybeFoo of
+        Just "yesod" -> redirect ("http://www.yesodweb.com" :: Text)
+        _ -> return ()
+    respondSource "text/plain" $ do
+        sendChunkText "You didn't go to yesodweb.com"

We check our query string parameter and perform the redirect before calling +respondSource. Once we know that we're returning a normal response, we then +use respondSource to create the body.

Database

Making it easy to create streaming database responses was probably my original +motivation here. I was never happy with the current recommended +approach, so I'm happy to +offer something simpler. Basically, we follow the exact same approach as with +normal streaming responses, but use the respondSourceDB function instead of +respondSource. Take the following example, which just returns a list of +people from a database.

{-# LANGUAGE EmptyDataDecls    #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE GADTs             #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Monad.Logger    (runNoLoggingT)
+import           Data.Conduit            (awaitForever, runResourceT, ($=))
+import           Data.Text               (Text)
+import           Database.Persist.Sqlite (ConnectionPool, SqlPersist,
+                                          SqliteConf (..), runMigration,
+                                          runSqlPool)
+import           Database.Persist.Store  (createPoolConfig)
+import           Yesod.Core
+import           Yesod.Persist
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name Text
+|]
+
+data App = App
+    { appConfig :: SqliteConf
+    , appPool   :: ConnectionPool
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlPersist
+    runDB = defaultRunDB appConfig appPool
+instance YesodPersistRunner App where
+    getDBRunner = defaultGetDBRunner appPool
+
+getHomeR :: Handler TypedContent
+getHomeR =
+    respondSourceDB typePlain $ selectSource [] [Asc PersonName] $= awaitForever toBuilder
+  where
+    toBuilder (Entity _ (Person name)) = do
+        sendChunkText name
+        sendChunkText "\n"
+        sendFlush
+
+main :: IO ()
+main = do
+    let config = SqliteConf ":memory:" 1
+    pool <- createPoolConfig config
+    runNoLoggingT $ runResourceT $ flip runSqlPool pool $ do
+        runMigration migrateAll
+        deleteWhere ([] :: [Filter Person])
+        insert_ $ Person "Charlie"
+        insert_ $ Person "Alice"
+        insert_ $ Person "Bob"
+    warp 3000 App
+        { appConfig = config
+        , appPool = pool
+        }

Obviously for our specific case, loading up the three names into memory would +be acceptable. But for more complicated responses, some form of streaming is +essential. This approach works very well in concert with the new streaming API +for yesod-sitemap, allowing us to create a streaming XML response body from a +database. The following is some real-life code from the School of +Haskell:

getSitemapR :: Handler TypedContent
+getSitemapR = do
+    AppContent {..} <- getYesod >>= readIORef . appContent
+    sitemap $ runDBSource $ do
+        yield $ SitemapUrl HomeR Nothing (Just Daily) (Just 1.0)
+        mapM_ (yield . goPage) $ unpack acPageMap
+        mapM_ (yield . goPost) acPosts
+        yield $ SitemapUrl UsersR Nothing (Just Daily) (Just 0.6)
+        yield $ SitemapUrl RecentContentR Nothing (Just Daily) (Just 0.6)
+        selectSource [] [] $= CL.mapMaybeM (\(Entity _ Profile {..}) -> do
+            mus <- getBy $ UniqueUserSummary profileHandle
+            case mus of
+                Just (Entity _ us) | userSummaryTutcount us > 0 -> return $ Just $
+                    SitemapUrl (UserR profileHandle) Nothing (Just Weekly) (Just 0.5)
+                _ -> return Nothing
+                )
+        selectKeys [] [] $= CL.mapMaybeM (fmap (fmap goTutorial) . getCanonicalRoute)
+  where
+    goPage (pn, PageInfo {..}) = SitemapUrl (PageR pn) Nothing (Just Monthly) (Just 0.8)
+    goPost Post {..} =
+        SitemapUrl (BlogPostR y m s) (Just postDate) (Just Never) (Just 0.7)
+      where
+        PostKey y m s = postKey
+    goTutorial route = SitemapUrl route Nothing (Just Monthly) (Just 0.6)

As the number of users and tutorials grows considerably, we want to avoid +loading all of that information into memory. The above code runs in constant +space, dealing with each individual user, and then each individual tutorial. +Under the surface, each SitemapUrl value is converted into a stream of +xml-types +Events, +and xml-conduit converts that stream into a stream of ByteStrings.

We have to be careful that our database queries are guaranteed to succeed. If +we use functions like get404 inappropriately, we could generate incorrect +response bodies.

And yes, that means that the School of Haskell is currently running on Yesod 1.2.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/03/yesod-1-2-cleaner-internals.html b/public/blog/2013/03/yesod-1-2-cleaner-internals.html new file mode 100644 index 00000000..1c0b8605 --- /dev/null +++ b/public/blog/2013/03/yesod-1-2-cleaner-internals.html @@ -0,0 +1,144 @@ + Yesod 1.2's cleaner internals +

Yesod 1.2's cleaner internals

March 11, 2013

GravatarBy Michael Snoyman

The past 48 hours have been very exciting for me: I've done possibly the +largest refactoring of the Yesod +codebase to date. Most of it was simply restructuring the internal code and +non-user-facing APIs. But there are some user visible changes going on right +now, and I think now's a good time to document them. I also have some ideas for +even more radical changes, which I'll touch on at the end of this post.

Representations

The most important change I've just implemented was an overhaul of the +representation system. This may not even be a concept most Yesod users are +familiar with, but it happens to be one of the oldest features of Yesod. The +idea is to allow a single function to return a different representation of the +data (HTML, JSON, etc) depending on the client's Accept header.

Let's start by defining two datatypes which we'll use below. A ContentType +is simply a ByteString holding the raw mimetype. A Content is a bit more +complicated: it's a sum type which can be a blaze-builder Builder, a conduit +Source, or a few other things which can be easily converted into a response +body. There's a ToContent typeclass which provides a function toContent +which converts many common datatypes into a Content.

I don't want to dwell on the old approach to much, but it boiled down to every +handler function returning something that looked like this:

[ContentType] -> (ContentType, Content)

The input [ContentType] is a list of mime types that the client as requested, +ordered by preference. You can then write a function that will determine the +appropriate content based on what the user accepts. For example:

myResponse userContentTypes =
+    loop userContentsTypes
+  where
+    loop ("text/html":_) = ("text/html", htmlContent)
+    loop ("application/json":_) = ("application/json", jsonContent)
+    loop (_:rest) = loop rest
+    loop [] = ("text/html", htmlContent)

This works out fairly nicely, except for one detail: it requires you to perform +all I/O actions (such as database queries) before you know which representation +of the data you need to provide to the user. This unfortunately doesn't work +out too well in practice, and therefore representations have not been well +utilized in Yesod.

In Yesod 1.2, we're going to have a completely different approach. A handler +function will instead need to return an instance of the ToTypedContent +typeclass, which looks like this:

data TypedContent = TypedContent ContentType Content
+class ToTypedContent a where
+    toTypedContent :: a -> TypedContent

We define some sensible instances in yesod-core itself:

instance ToTypedContent Html where
+    toTypedContent html = TypedContent "text/html" (toContent html)
+instance ToTypedContent Data.Aeson.Value where
+    toTypedContent value = TypedContent "application/json" (toContent value)
+instance ToTypedContent Text where
+    toTypedContent text = TypedContent "text/plain" (toContent text)

With this change, it's now perfectly acceptable to create a JSON response with +something as simple as return $ toJSON myValue. But how do we deal with +multiple representations? For that, we have a pair of helper functions, which +are easiest to understand with a simple example.

getPersonR personid = do
+    person <- runDB $ get404 personid
+    selectRep $ do
+        provideRep $ defaultLayout $ do
+            setTitle "Some Person"
+            extraInfo <- getExtraInfo person
+            $(widgetFile "person")
+        provideRep $ toJSON person
+        provideRep $ (personName person :: Text)

selectRep will get the parsed contents of the HTTP Accept header and +determine which representation should be used. provideRep provides an +additional representation for selection. Notice how you don't even need to +state the mime type: it's all inferred automatically through the type system. +(If you need something more dynamic, provideRepType is available as well.)

In our example, we start off with an HTML representation. This representation +requires some extra data to be looked up. We can safely perform the +getExtraInfo call inside the provideRep call, and the overhead of that +extra call will not affect the JSON and plain text representations.

You can also see the code behind selectRep and provideRep itself.

I've already used this new API to clean up a few nagging issues (error messages +and authentication responses are now representation-aware). I'm hopeful that +this change makes it much more convenient for developers to create sites +catering to both a plain-HTML and rich client view.

Caching

Continuing on the trend of inferring information from the types, we also have a +new request-local caching mechanism. A prime use case for this is +authentication checking. In a typical Yesod application, you'll need to check +if a user is logged in in a number of different places: the authorization code, +the defaultLayout function, database functions, and the handler itself. +Having to do a database round trip for the same data multiple times is +inefficient; we should be able to cache that data somewhere.

Yesod has had request local storage for a while now, but it required generation +of a unique key. Possible approaches to this are using Template Haskell to +generate one, or Data.Unique and some unsafePerformIO. But both of those +approaches are just inconvenient enough that this feature went unused.

Luite, Felipe, Greg and I all discussed +this a while ago, and Felipe +mentioned using TypeRep (i.e., the Typeable typeclass) as a unique key. This +requires you to create a newtype wrapper for each piece of data you want +cached, but otherwise is unobtrusive. With that approach in hand, the entire +caching API becomes a single function:

cached :: Typeable a => Handler a -> Handler a

To implemented our cached maybeAuth function, we could do something like:

newtype CachedUser = CachedUser { unCachedUser :: Maybe User }
+    deriving Typeable
+
+cachedMaybeAuth = fmap unCachedUser $ cached $ fmap CachedUser maybeAuth

A variant of this has already been applied to the real maybeAuth in +Yesod.Auth, so no changes are required to your code, except ensuring that +your User type is an instance of Typeable.

The implementation +itself +is actually pretty simple.

Handler typeclasses

The Yesod.Handler module (which will probably become Yesod.Core.Handler soon) +has a number of functions that can be used when writing Handler code. This +includes looking up GET parameters, sending redirects, and modifying the +session. And through Yesod 1.1, all of these functions have lived in the +GHandler monad.

There are a few reasons why this is suboptimal:

  1. It doesn't allow us to lift into monad transformers automatically. A common +workflow is using defaultLayout or runDB to deal with widgets or +Persistent, and then needing to lift some operations from the GHandler monad. +It would be nice to have automatic lifting.

  2. GHandler, like IO, is a bit of a "sin bin." There's absolutely no control +over what a user may do there. By moving operations into typeclasses +instead, we can isolate non-mutating effects, mutating effects, short-circuit +effects, and IO actions, giving us more knowledge about what our code is doing +from the types themselves. I don't know how many people will be interested in +using the typeclasses in this way, but I have heard people express interest in +this previously.

The move to typeclasses also opens up the possibility for some more radical +changes in the future without massive disruption for users. I'll touch on some +of these thoughts at the end of this post.

YesodRequest/YesodResponse

This was actually the most invasive change to the codebase, but as it only +affects the internals I've left it toward the end of this post. In the WAI +world, we have a very simple model for an application: it takes a Request, +and returns a Response. The Yesod world seems to drastically complicate that +simple approach.

But in reality, Yesod also has the same simplistic approach available, it's +just always been buried under piles of code and strangely named functions/data +types. My refactoring makes this much clearer.

  • The Request datatype from WAI does a very minimal amount of parsing. However, in Yesod we require a bit more processing of the incoming request to be performed. We store this extra information in the YesodRequest datatype. This adds information like cookies and the user session.
  • Similarly, The Response datatype from WAI is very low-level. A Yesod app may want to return a much more high-level response in terms of status code, content type, content, and an updated session. To allow for this, the YesodResponse data type allows you to return either this higher-level response, or a low-level WAI response instead.
  • As a parallel to the Application type in WAI, Yesod provides the YesodApp synonym, which simply takes a YesodRequest and returns a YesodResponse.
  • We need to store some kind of environment information which is not request-specific, so that our handler functions have access to it. This includes logging functions, error handlers, and the foundation datatype. All of this goes into RunHandlerEnv.
  • There's also mutable data for each request, like the session and the cache. This goes in GHState.
  • We finally tie up the per-request immutable data, environment data, and mutable data into a single datatype: HandlerData. Note that we wrap up the mutable data in an IORef instead of using a State monad since we need to maintain a consistent state even in the presence of exceptions.
  • With all of that out of the way, our GHandler monad is much less mysterious: it's just a Reader providing access to the HandlerData.
  • runHandler will take GHandler and convert it into a YesodApp by feeding it a HandlerData and converting the output into a YesodResponse. defaultYesodRunner takes this a step further and creates a plain WAI Application.

If there are things that are unclear in the explanation above, please let me +know. I intend to include this text, or some version of it, in the code as a +high-level architectural view of yesod-core.

Dispatch is a different beast

I like to classify Yesod as a Model-View-Controller (MVC) framework. In this +approach, the model is handled by Persistent (or whatever replacement you use +for data storage) and the view is handled by Shakesepare (again, this can be +replaced). The last piece of the puzzle, Controller, is what Yesod itself- and +the yesod-core package in particular- deal with.

Within controller, we can also make a few smaller pieces. The HTTP layer itself +is handled by WAI, for example. The final two pieces handled by yesod-core are +dispatch and handlers. Other frameworks will combine these two concepts +together; I think there's huge value to be gained by keeping them as separate +components.

So far, I've been focused entirely on the handler aspect of the puzzle. +Dispatch has already somewhat moved out of yesod-core itself into the +yesod-routes package which, despite its name, is actually not Yesod-specific, +as can +be +seen by some of its +dependencies. My goal is to separate out even more of the dispatch +functionality from yesod-core itself, to open the door for others to use even +more of the functionality outside of Yesod, and to hopefully make a better +product for Yesod as well.

I haven't given this part of the process too much thought yet, but I'm +definitely playing around with the thought of merging in the ideas from +yesod-pure. I still believe +that code generation is the best bet for a robust dispatch system, but having +that code generation built on top of a more powerful, user accessible library +will make the generated code more transparent.

If anyone has thoughts on this part of the refactoring, please let me know!

Better subsite approach for the future?

I had thought I would discuss some radical ideas for ways to clean up the +subsite system in Yesod, but this post is already long enough. I'll come out +with a separate post in a few days. I'll give this leader though: this change +is significantly more breaking in nature than anything I've implemented so far, +so I'm hesitant to move ahead with it. I might hold it off for the 1.2 release, +and then have a Yesod 2.0 release in the not-too-distant future with a higher +level of breakage.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/03/yesod-dispatch-version-1-2.html b/public/blog/2013/03/yesod-dispatch-version-1-2.html new file mode 100644 index 00000000..618dbfcc --- /dev/null +++ b/public/blog/2013/03/yesod-dispatch-version-1-2.html @@ -0,0 +1,159 @@ + Yesod dispatch, version 1.2 +

Yesod dispatch, version 1.2

March 21, 2013

GravatarBy Michael Snoyman

In my +previous +two posts, I +discussed some significant changes coming to Yesod in the 1.2 release. Both of +these posts discussed the changes to handlers. Now I'd like to switch gears +and talk about the other half of yesod-core: dispatch.

As a tl;dr: in Yesod 1.1 and prior, the dispatch system was a fairly +complicated beast that was hidden from users behind Template Haskell +generators. None of those code generators are going away, and they'll continue +working just like they have until now. But the internals have been cleaned up +to such a point where they can be a user facing component without inducing +fear. And as a result, we're now in a position to provide alternate dispatch +systems. As an example:

{-# LANGUAGE OverloadedStrings #-}
+import Yesod.Core
+import Data.Aeson
+import Data.Monoid ((<>))
+import Data.Text (Text, pack)
+
+people :: [(Text, Int)]
+people = [("Alice", 25), ("Bob", 43), ("Charlie", 37)]
+
+main = warp 3000 $
+    onStatic "people" (dispatchTo getPeople) <>
+    onStatic "person" (withDynamic $ dispatchTo . getPerson)
+
+getPeople = return $ toJSON $ map fst people
+
+getPerson name =
+    case lookup name people of
+        Nothing -> notFound
+        Just age -> selectRep $ do
+            provideRep $ return $ object ["name" .= name, "age" .= age]
+            provideRep $ return $ name <> " is " <> pack (show age) <> " years old"

Let's get into some details of how it works, and why it's designed that way.

YesodDispatch

The core of any Yesod application is its foundation datatype. One of the +features it provides is the ability to perform some initialization before your +app starts running. Two prime examples of this are initializing a database +connection pool and setting up an HTTP connection manager. This foundation type +is then available to all of your handler functions, which can access this +initialized state.

But there's other initialization that Yesod itself must perform. In particular, +we need to load up the clientsession encryption key and create a Logger +value. There might also be other activities to perform in the future. But for +now, our application needs to have access to these three pieces of data +(foundation, session backend, and logger) in order to process requests. Let's +represent that with +YesodRunnerEnv.

So now we've got our environment; what do we do with it? Presumably we could +just return a handler. But that would actually be a bit inefficient for some +use cases. In particular, we have full support in Yesod for having subsites +that were not written in Yesod. yesod-static is a prime example of this: it's a +thin wrapper around the wai-app-static package. In fact, any WAI app can be +used as a Yesod subsite. Theoretically, this applies to +Scotty and even +Happstack.

Alright, back to the point at hand: having our dispatch return a handler would +mean that WAI subsites would have to go through a bunch of unnecessary +processing for loading session variables, converting GET parameters to Text, +etc. So dispatching should return the lowest common denominator: a WAI +application.

Putting these two pieces of information together, we know what a dispatch +function needs to look like. We stick the whole thing in a +typeclass +and get:

class Yesod site => YesodDispatch site where
+    yesodDispatch :: YesodRunnerEnv site -> W.Application

We'll come back to this typeclass in a little bit.

toWaiApp(Plain)

Let's move up the stack a bit. Assuming we have some type that implements +YesodDispatch, how do we run it? Yesod is built on WAI, so what we really +need is a WAI Application. That seems easy enough: just provide a +YesodRunnerEnv. And in fact, that's basically all we do. You can see the +implementation in +toWaiAppPlain. +This function will create a YesodRunnerEnv from your foundation type. It uses +methods from the Yesod typeclass to determine how to create this environment. +And then it applies a small middleware +wrapper +to clean up requested paths

Once you have a WAI Application, you can apply more middlewares if you want. +toWaiApp applies some commonly used middlewares to get a more featureful +application. Finally, you can pass the Application to any WAI handler. In +production, this will usually be Warp. But you can also use wai-test to perform +some local, non-network testing of your application. In fact, the Yesod +testsuite does this extensively, and the yesod-test package leverages this +functionality as well. Some basic yesod-test testing is included with the +scaffolding.

Creating a YesodDispatch

We've now covered what YesodDispatch does and how it's used. How do you +actually write an instance? Most users will never have to: the Template Haskell +provided by Yesod will generate it all for you based on the high-level route +syntax. I personally think this is the best approach to take for most +applications, but it doesn't satisfy everyone's needs. So one of the major +goals of the 1.2 rewrite is to open up the system to allow alternate +dispatching.

As both a proof-of-concept and a useful tool, I've included one such alternate +dispatch system in yesod-core. That's what powered the example given at the +beginning of this post. You can see the full +implementation.

Every site must have an associated route datatype. This is how type-safe URLs are implemented in general. However, in this light-weight dispatch system, we have no desire to create a meaningful route datatype. So instead, we have a simple wrapper around a list of path segments:

data Route LiteApp = LiteAppRoute [Text]

And we provide instances for the RenderRoute and ParseRoute typeclasses based +on this. We also need to have an instance of the Yesod typeclass. For now, we +simply use default values for all methods, but in theory could override some, +or provide the user with a means of overriding specific settings. But for now, +we've just taken the simplest approach.

So with that overhead out of the way, we can focus on the important point: the +dispatch itself. LiteApp is defined as:

newtype LiteApp = LiteApp
+    { unLiteApp :: Method -> [Text] -> Maybe (HandlerT LiteApp IO TypedContent)
+    }

The datatype is nothing more than a dispatch function itself! It takes a +request method and a list of path segments, and either returns Nothing (page +not found), or a handler to be used. We have a Monoid instance to combine these +together, and a number of primitive combinators for building up these values. +(See the source for more details.)

So the final piece is the YesodDispatch instance itself:

instance YesodDispatch LiteApp where
+    yesodDispatch yre req =
+        yesodRunner
+            (fromMaybe notFound $ f (requestMethod req) (pathInfo req))
+            yre
+            (Just $ LiteAppRoute $ pathInfo req)
+            req
+      where
+        LiteApp f = yreSite yre

The last line gets the LiteApp value itself from the YesodRunnerEnv and unwraps the newtype wrapper, giving us a core dispatch function. The code:

f (requestMethod req) (pathInfo req)

applies that function to the actual requested method and path. If Nothing is +returned, then we replace it with the notFound handler. Once we have a +handler function, we use the +yesodRunner +function to convert it into a WAI application. (The details of how that works +goes back into the realm of handlers, so I'll stop discussion there.)

And just like that, we have an alternate dispatch system for Yesod. You're able +to still leverage the Yesod infrastructure for things like form parsing, +short-circuit responses, etc. And as described above, our dispatch system isn't +really tied to Yesod handlers at all: you can use any WAI applications. So +routing and dispatch are really two orthogonal components in the Yesod world.

Subsites

Subsites are a bit different than normal apps. They need to know how to promote +their routes to the parent site's routes. As described in the previous post, +subsite handlers are just wrappers around the parent handlers. The subsite +knows how to unwrap that wrapping, but it also needs to know how to turn a +parent handler into a WAI Application. And we need to have all the same +environment as a standard dispatch.

So we're going to follow the same pattern from before. We have a YesodSubRunnerEnv and a YesodSubRunnerDispatch typeclass:

class YesodSubDispatch sub m where
+    yesodSubDispatch :: YesodSubRunnerEnv sub (HandlerSite m) m
+                     -> W.Application

Probably the simplest implementation is WaiSubsite, which just wraps an existing WAI Application:

instance YesodSubDispatch WaiSubsite master where
+    yesodSubDispatch YesodSubRunnerEnv {..} req =
+        app req
+      where
+        WaiSubsite app = ysreGetSub $ yreSite $ ysreParentEnv

Like normal sites, subsites can be created with Template Haskell as well, +though there's no requirement to use it, as demonstrated with WaiSubsite. You +can see a small subsite +demo +in the repo.

A peek at the TH

I don't want to dwell on the Template Haskell too much: I've discussed it in +the past, and frankly I don't think there's a lot of user benefit to seeing +what it's doing. At a very high level, the Template Haskell code will:

  • Create your associated route datatype.
  • Create RenderRoute and ParseRoute instances.
  • Create a YesodDispatch instance that calls out to your handler functions, dispatching with the efficient yesod-routes package.

The basics chapter of the book has a section on +routing which gives some +demonstration generated code. With Yesod 1.2 the code will be a bit simpler, +but in reality will look quite different to be able to leverage the efficient +data structures used by yesod-routes. In other words, that section can provide +you some insight, but isn't the full story. If you're really curious to see +what code gets generated, you can compile with -ddump-splices.

Upcoming dispatch features

So now I've laid out two dispatch systems: the TH-based system and LiteApp. +These will cover a large percentage of real world use cases. But I think +there's another use case to be handled better as well: RESTful web services. +Yesod works admirably for this already, but there are some improvements to be +made. We're currently discussing this at FP Complete, and will likely be using +it to power some of our future offerings. I don't have many details to share at +the moment, but will keep you updated on progress when something is available.

Next time

I have one more feature that I want to describe for Yesod 1.2: better streaming +data support. Yesod has always been built around a streaming response +mechanism, but has required some clunky code to get it to work. Yesod 1.2 +introduces a few helper functions that make the approach much more elegant. +Look for another post on this next week!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/03/yesod-platform-1-1-8.html b/public/blog/2013/03/yesod-platform-1-1-8.html new file mode 100644 index 00000000..7043d05a --- /dev/null +++ b/public/blog/2013/03/yesod-platform-1-1-8.html @@ -0,0 +1,20 @@ + ANNOUNCE: yesod-platform 1.1.8 +

ANNOUNCE: yesod-platform 1.1.8

March 4, 2013

GravatarBy Michael Snoyman

Heads-up everyone: I just release a new version of the yesod-platform to +Hackage. Notable updates in this release include:

  • Luite has made a number of improvements to yesod devel, such as using cabal-install's improved dependency solver.
  • A number of added API functions for yesod-test, thanks to Paul Rouse and Shane Kilkelly.
  • Update to conduit 1.0.0.
  • Some scaffolding improvements for a better logging experience.
  • The scaffolding tool now includes a PostgreSQL + Fay scaffolding option. This option is still considered experimental, but I encourage people to give it a shot. (I've been using Fay quite a bit for some work projects, and it's working out very well. Thanks Chris!)
  • As usual, a bunch of improvements to our underlying library set, too numerous to be listed.

As a reminder: yesod-platform provides a stable, tested set of packages which +are known to compile together and interact well. If you're looking for +stability and don't mind using libraries that are one to two months old, it's +highly recommended. If you want to be using more recent libraries, then just +install the yesod package directly instead.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/04/changes-to-online-book.html b/public/blog/2013/04/changes-to-online-book.html new file mode 100644 index 00000000..e4e5cfbc --- /dev/null +++ b/public/blog/2013/04/changes-to-online-book.html @@ -0,0 +1,36 @@ + Changes to the online book +

Changes to the online book

April 19, 2013

GravatarBy Michael Snoyman

In preparation of the 1.2 release of Yesod, I've made a long-overdue change to +this site: it now supports the display of +multiple +versions of the book. This should make it +easier for users still sticking with a previous version of the framework. The +plain /book URLs will always contain the most recent versions of the content, +while /book-1.1, /book-1.2, etc, will be used for version-specific content. The +two versions are currently identical, but I'll be making some changes over +the coming weeks to the 1.2 version of the book.

For those of you who have poked around at the book content +repo, I'm making one other +important change right now: migrating the book content from DITA to DocBook. My +motivations are twofold: there are more DocBook tools available than DITA in +the open source community, and O'Reilly uses DocBook in their publishing +process.

If anyone notices problems in the online book, please give me a heads-up.


Just because I can't get away without touting a Yesod 1.2 feature: multiple +versions of the book is a perfect motivating case for using a subsite, as the +exact same routing behavior can be reused for each version of the book. In this +case, I just had each separate subsite point to a different Git branch. If +you're curious to see this in practice, have a look at the commit introducing +the +subsites, +and in particular the subsite routing +file.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/04/mixin-support-in-lucius.html b/public/blog/2013/04/mixin-support-in-lucius.html new file mode 100644 index 00000000..15168663 --- /dev/null +++ b/public/blog/2013/04/mixin-support-in-lucius.html @@ -0,0 +1,55 @@ + Mixin support in Lucius +

Mixin support in Lucius

April 24, 2013

GravatarBy Michael Snoyman

I just released version 1.0.4 of shakespeare-css (the package providing the +Lucius CSS template system). This release contains one newly added feature: +mixins. This allows you to define reusable CSS components. A common use case +for this is to address vendor prefixes. So as a motivating example:

{-# LANGUAGE QuasiQuotes #-}
+import Text.Lucius
+import qualified Data.Text.Lazy.IO as TLIO
+
+transition :: String -> Mixin
+transition val =
+    [luciusMixin|
+        -webkit-transition: #{val};
+        -moz-transition: #{val};
+        -ms-transition: #{val};
+        -o-transition: #{val};
+        transition: #{val};
+    |]
+
+main :: IO ()
+main = TLIO.putStrLn $ renderCssUrl undefined
+        [lucius|
+            .some-class {
+                ^{transition "all 4s ease"}
+            }
+        |]

This produces the following CSS (whitespace added for readability):

.some-class {
+    -webkit-transition: all 4s ease;
+    -moz-transition:    all 4s ease;
+    -ms-transition:     all 4s ease;
+    -o-transition:      all 4s ease;
+    transition:         all 4s ease;
+}

There's not much more to it than that. You can begin using this new feature +immediately with your existing projects, simply bump the lower bound on +shakespeare-css.

Thanks to Blake Rain for motivating me to actually get this +written.

With the close of this issue, there are officially no more open issues in the +Yesod 1.2 milestone on +Github. In +other words, we're getting very close to release. Greg, Felipe and I are tying +up some last few tweaks to Persistent 1.2, and then we should be ready to go. +This is officially your last chance to provide any input on the 1.2 release.

If you're looking for a refresher on what changes are coming, please check out +the high-level changelog +and the detailed change +list.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/04/new-chapter.html b/public/blog/2013/04/new-chapter.html new file mode 100644 index 00000000..2a3b7746 --- /dev/null +++ b/public/blog/2013/04/new-chapter.html @@ -0,0 +1,22 @@ + New chapter: Understanding a Request +

New chapter: Understanding a Request

April 23, 2013

GravatarBy Michael Snoyman

Just a heads-up: I've written a new chapter for the upcoming 1.2 version of +the book. The purpose of this chapter is to +help explain how a request is processed in Yesod, start to finish. It is based +around the simplified design in Yesod 1.2, and incorporates much of the +information I put in the 1.2 blog posts.

Comments, questions and criticism are very welcome! You can view the chapter +on the site. If you'd +like to submit pull requests, you can see the source on +Github.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/04/persistent-1-2-out.html b/public/blog/2013/04/persistent-1-2-out.html new file mode 100644 index 00000000..d36126ed --- /dev/null +++ b/public/blog/2013/04/persistent-1-2-out.html @@ -0,0 +1,27 @@ + Persistent 1.2 is out, last call on Yesod 1.2 +

Persistent 1.2 is out, last call on Yesod 1.2

April 29, 2013

GravatarBy Michael Snoyman

I'm happy to announce the release of Persistent 1.2, along with all related +packages and backends. In other words, I'm announcing the release of:

  • persistent-1.2.0
  • persistent-mongoDB-1.2.0
  • persistent-mysql-1.2.0
  • persistent-postgresql-1.2.0
  • persistent-sqlite-1.2.0
  • persistent-template-1.2.0
  • pool-conduit-0.1.2

You can see a Persistent detailed change +list.

I'm also announcing the last call for input for the Yesod 1.2 release. The +Yesod team has decided to make this release on Thursday, May 2, unless there is +new input from the community. If you want to play with it before release, +please install from Github:

cabal update
+git clone https://github.com/yesodweb/yesod
+cd yesod
+cabal-meta install

If you run into any problems, or have any questions, please raise them on the +mailing list or the IRC channel.

As a reminder, you can see the upcoming changes for Yesod 1.2 in the +changelog +and the detailed change +list.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/04/shakespeare-typescript.html b/public/blog/2013/04/shakespeare-typescript.html new file mode 100644 index 00000000..6af2cc16 --- /dev/null +++ b/public/blog/2013/04/shakespeare-typescript.html @@ -0,0 +1,15 @@ + Shakespeare templating updates and TypeScript support +

Shakespeare templating updates and TypeScript support

April 22, 2013

GravatarBy Greg Weber

We are very excited about the upcoming Yesod 1.2 release, but we should mention other improvements in the Yesod ecosystem. There are some greate updates to shakespeare-js are available today.

  • Dramatically faster template reloads
  • Support for TypeScript on unix
  • Roy integration supports variable interpolation

Shakespeare is a family of compile time templates that is used for HTML, CSS, Javascript, and text. Javascript templates come from the shakespeare-js template. Variables holding JSON content can be inserted into the templates. The templates can be compiled by invoking external compilers: this has allowed us to support CoffeeScript templates for a long time now.

Faster template reloads

In production code, shakespeare compiles everything once to static strings awaiting some variable insertions. In developement mode, it is preferred to use the shakespeare functions that automatically reload their file contents such as coffeeFileReload. This means that on every web request the file will be re-parsed in case it changed. This works great, but when using an external service such as coffee-script, there is a noticeable lag to shell out to the compiler. So the reload functions now cache the file contents in memory and only reload them when the modification time of the file changes.

TypeScript support on unix

TypeScript is now supported on unix in the same way that CoffeeScript has been. We should be able to make it work on Windows if there is interest.

TypeScript is a JavaScript superset that is easy to start using. TypeScript has the backing of Microsoft, which may actually be hindering developer uptake in the non-Microsoft communities, but if you judge TypeScript by what it is, I think they have come up with something fantastic. The TypeScript superset strictly tracks the latest JavaScript standards and compiles down to an older version of Javascript runnable by all browsers.

What TypeScript has added which is not in the Javascript specs is types. By default TypeScript is dynamically typed like JavaScript and will run most Javascript code without a few small additions like declarations of global variables. But you can add in types wherever you want and gradually make your code more type-safe.

The biggest problem with TypeScript is the lack of type inference and presence of null. Coming from Haskell you might expect the type to be inferred when it is obvious to you that it could be, but it will often end up just being a dynamic type unless you write down a type annotation for every function. However, you still may be able to achieve some strong typing with less effort if you are leveraging existing type declaration files for the frameworks you are using, like these: https://github.com/borisyankov/DefinitelyTyped

Personally I have been moving all my CoffeeScript code to TypeScript. CoffeeScript turns Javascript into a pretty good dynamic language, but I need more than that to write reliable software (without spending an enormous effort on writing tests). I definitely miss the comprehensions in CoffeeScript, but I can use lodash.js in its place. It helps that some of the CoffeeScript features are making their way into the JavaScript standard and are available in TypeScript.

Knowledge of TypeScript, like CoffeeScript, is a skill that is portable outside of the Haskell community.

Roy variable interpretation

Roy templates now support variable interpolation.

Roy is a melding of functional programming to the existing Javascript semantics. It has never got a strong uptake due to the project lacking polish and documentation, but that keeps improving and I know there are some happy Haskell users of Roy now.

Javascript polyglot

Fay will compile a Haskell subset directly to Javascript, and is a popular option in the Haskell community. yesod-fay uses shakespeare-js and is also being kept up to date with the 1.2 changes.

Elm is another option that tackles both type-safety and GUI. The elm-yesod package uses shakespeare-js and integrates Elm as a widget.

Of course you can use whatever you want on the client side. Those mentioned here have existing packages for easy integration with Yesod. ghcjs is continually being improved and is another option for compiling Haskell (and the only option for Haskell using more than the Fay subset), and there are examples available for usage with Yesod.

There is nothing stopping you from mixing and matching the different available approaches: TypeScript for code that is leveraging javascript frameworks, Fay for sharing server-side code, and Roy/Fay/Elm for strongly typed client-side code.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/05/yesod-1-2-released.html b/public/blog/2013/05/yesod-1-2-released.html new file mode 100644 index 00000000..42453af5 --- /dev/null +++ b/public/blog/2013/05/yesod-1-2-released.html @@ -0,0 +1,19 @@ + Yesod 1.2 released +

Yesod 1.2 released

May 2, 2013

GravatarBy Greg Weber

The Yesod team is pleased to announce the release of Yesod 1.2. You can get it with:

cabal install yesod-platform yesod-bin

The yesod binary is now a separate package which helps manage dependencies, but it does mean you need to remember to install 2 separate packages.

Yesod 1.1 was released in August. Shortly after, Michael started working for FP Complete. A lot has happened since then!

Yesod ecosystem

Yesod 1.2

Representation system

Previously discussed in the post: a better representation system, cleaner internals, and the request local cache. Providing different representation types (JSON or HTML) used to be cumbersome at times, but now it is simple using selectRep.

getResource :: Handler TypedContent
+getResource = do
+  selectRep $ do
+      provideRep $ [hamlet|<div>|]
+      provideRep $ object ["result" .= "ok"]

Request local type-based caching

See the previous mentioned blog post, but you just need to create a newtype wrapper around some data and then you can cache it with the cached function.

Subsite overhaul

Subsites are now just a transformer over a master site

Flexible routing

routing dispatch is more flexible

Better streaming API

streaming has been simplified

Asset pipeline

Yesod has always made it easy to combine dynamic css and javascript since before Rails started using the term asset pipeline. What was missing was the same ease for static assets. Combining static assets is very important for optimal performance by reducing the total number of network requests. You can now easily combine CSS and Javascript with the combineScripts and combineStylesheets helpers. Here is the scaffolding change and you can also look at the haddock documentation.

Better testing

yesod-test was completely overhauled, making it easier to use and providing cleaner integration with hspec.. It is easy in Haskell to just lean against the type system for most things and skip testing, particularly if it is something that is hard to test with QuickCheck. But yesod-test (and wai-test) are there to prevent bugs that the type system cannot.

Even more

  • More efficient session handling.
  • yesod-auth email plugin now supports logging in via username in addition to email address.
  • probably more stuff we forgot to mention

Conclusion & more info

FPComplete's development of the School of Haskell has been great for the Haskell community to keep spreading knowledge. It has also been running with the changes for 1.2 for quite a while which should contribute to making 1.2 a more stable release.

The high-level changelog has been discussed in high-level here. Detailed changes are here

The book documentation for 1.2 has been started, but still needs more work to get fully up to date.

Most of the changes to upgrade your site to 1.2 should be fairly mechanical. I started a wiki page for the upgrade. If you have any issues, please note them there or on the mail list.

We hope you enjoy using Yesod 1.2

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/06/first-11-chapters.html b/public/blog/2013/06/first-11-chapters.html new file mode 100644 index 00000000..5047adde --- /dev/null +++ b/public/blog/2013/06/first-11-chapters.html @@ -0,0 +1,26 @@ + First 11 chapters are Yesod 1.2-compliant +

First 11 chapters are Yesod 1.2-compliant

June 23, 2013

GravatarBy Michael Snoyman

It's been a while since I've written a post. Besides being very busy at +work, a lot of my time +has gone into getting the Yesod book up-to-date with Yesod version 1.2. And +today, I'm happy to report that the first 11 chapters, comprising the "basics" +section of the book, have been converted.

You can view the new content at the 1.2 +URL. Once the entire book is converted, I'll +switch the URLs, but will continue hosting the 1.1 version of the book at its +own URL. I intend to continue this pattern +for any future releases as well.

I intend to continue the conversion process, but I also intend to augment the +book. In particular, I have a number of new examples to add. My current plans +are:

  • JSON serving
  • Client-side development with Fay
  • An updated subsite example based on 1.2 features
  • Easy generation of streaming data
  • How to store some initialized data in the foundation
  • Getting configuration from environment variables
  • Writing your own Template Haskell code

If anyone has suggestions for changes to this list, let me know.

As for the newly converted content: comments and pull requests are, as always, +welcome!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/07/catching-all-exceptions.html b/public/blog/2013/07/catching-all-exceptions.html new file mode 100644 index 00000000..f790aaa9 --- /dev/null +++ b/public/blog/2013/07/catching-all-exceptions.html @@ -0,0 +1,141 @@ + Catching all exceptions +

Catching all exceptions

July 12, 2013

GravatarBy Michael Snoyman

Note: This blog post is also available on the School of +Haskell. +I'd recommend reading it there, as the active code snippets may greatly enhance +the material.


A commonly discussed piece of functionality is "catching all exceptions." The goal usually is to write reliable functions, which can recover from any kind of problem that exists in some library, or perhaps in some callback passed into the function, which the library author has no control over. Thanks to extensible exceptions, writing this kind of "catch any exception" is pretty trivial in Haskell:

import Control.Exception
+
+catchAny :: IO a -> (SomeException -> IO a) -> IO a
+catchAny = Control.Exception.catch
+
+dangerous :: IO Int
+dangerous = error "Fool you!"
+
+main :: IO ()
+main = do
+    result <- catchAny dangerous $ \e -> do
+        putStrLn $ "Got an exception: " ++ show e
+        putStrLn "Returning dummy value of -1"
+        return (-1)
+    print result

But this catchAny function isn't quite correct, due to asynchronous exceptions. I'd like to explain what the problem is, demonstrate a fix for it (inspired by John Lato using Simon Marlow's async library), and then generalize it even further using monad-control.

Async exceptions

Let's consider the following theoretical workflow:

  • I have a potentially exception-throwing function I want to run, called dangerous.
  • This function should be run by a larger function, called worker. It should handle exceptions thrown by dangerous gracefully.
  • I want to make sure that worker runs for no more than 5 milliseconds. I'll use the timeout function to ensure this.
  • But unbeknownst to me, dangerous tends to take about 10 milliseconds.

Below is an implementation of the above logic, using the catchAny we defined earlier. Before you run this code, consider what the expected behavior here should be. In particular, should worker run to completion or not?

import Control.Exception
+import System.Timeout
+import Control.Concurrent
+
+catchAny :: IO a -> (SomeException -> IO a) -> IO a
+catchAny = Control.Exception.catch
+
+dangerous :: IO Int
+dangerous = do
+    putStrLn "Succeeds this time, but takes some time"
+    threadDelay 10000
+    return 5
+    
+worker :: IO ()
+worker = do
+    x <- catchAny dangerous $ \e -> do
+        putStrLn $ "Caught an exception: " ++ show e
+        return (-1)
+    putStrLn $ "x + 10 == " ++ show (x + 10)
+
+main :: IO ()
+main = do
+    res <- timeout 5000 worker
+    case res of
+        Nothing -> putStrLn "worker did not run to completion"
+        Just () -> putStrLn "worker ran to completion"

In an ideal world, worker would be stopped before it finished, since it takes more than the 10 milliseconds provided to it to completely run. However, if you run the above code, you'll see that worker does in fact complete. What gives? Well, this is what actually happens when you run this code:

  • The timeout function forks a new thread to run worker in. If worker does not complete within 5 ms, that new thread is thrown a timeout exception. This kind of throwing is done by the throwTo function, and is an asynchronous exception.
  • Meanwhile, worker starts running, and wraps dangerous with catchAny.
  • Since dangerous takes 10 ms, the timeout exception is called when the new thread is inside dangerous, which itself is inside catchAny. dangerous has no exception handling, so the exception propagates up to worker.
  • worker's catchAny catches all exceptions, and therefore treats the timeout exception as if it was thrown from dangerous itself. It therefore continues processing, completely ignoring the command to timeout.

This is a little tricky, so make sure you understand the situation properly before continuing.

Non-solution: examine the types

My first inclination for solving this problem was to look at the types of the exception being caught. If it was a timeout exception, or any other kind of asynchronous exception, catchAny could simply ignore it. This looks something like:

catchAny :: IO a -> (SomeException -> IO a) -> IO a
+catchAny m f =
+    Control.Exception.catch m onExc
+  where
+    onExc e
+        | shouldCatch e = f e
+        | otherwise = throwIO e
+    shouldCatch e
+        | show e == "<<timeout>>" = False
+        | Just (_ :: AsyncException) <- fromException e = False
+        | otherwise = True

As you can see, this does in fact solve our problem. catchAny now ignores the timeout exception, so it is propagated to worker, terminating the computation. However, it has a few problems. I won't profess to understand all of the problems, but here's the most salient in my mind: the types have nothing to do with whether an exception is synchronous or asynchronous. Consider that, for some strange reason, we decided to asynchronously throw an IOException to a worker thread, e.g.:

main :: IO ()
+main = do
+    threadId <- forkIO worker
+    eresult <- try $ readFile "does-not-exist.txt"
+    case eresult of
+        Left e -> throwTo threadId (e :: IOException)
+        Right _ -> putStrLn "Funny, that shouldn't have worked"
+    -- Give the forked thread time to finish
+    threadDelay 50000

Since our catchAny knowns nothing about asynchronously thrown IOExceptions, our worker thread will continue doing work even after we try to kill the thread. This example is clearly a bit contrived, but consider if we had some kind of user quota system, where we send a custom asynchronous exception whenever a thread uses too much disk space. There's no way a generic catchAny could know about every kind of custom exception type a user defines. And even if we did, it's clear that any exception type could be thrown either synchronously or asynchronously.

Real solution: separate worker thread

John Lato described a very straight-forward means of doing the right thing, leveraging Simon Marlow's excellent async library. In fact, that library is so excellent that it solved some of my implementation details before I even realized they existed... more on that in a moment.

The concept is simple: if you have some function you want to catch all exceptions for, fork a new thread and run the function there. Catch all exceptions thrown in that new thread, and return them to the original thread (via usage of software transactional memory). Now, if any async exceptions are thrown to the original thread, they are unaffected by the exception catching code. And the cool part that the async library took care of automatically: if the original thread gets an async exception, automatically propagate it down to the worker thread so that it terminates work immediately.

The amazing thing is just how simple this code is. We'll switch over to implementing tryAny instead of catchAny, since it's easier with the async library, and then we can build catchAny on top of that.

Note: the async library hasn't yet been deployed to the FP Haskell Center at time of writing, so I'll include the necessary code inline from async.

tryAny :: IO a -> IO (Either SomeException a)
+tryAny action = withAsync action waitCatch
+
+catchAny :: IO a -> (SomeException -> IO a) -> IO a
+catchAny action onE = tryAny action >>= either onE return

Our solution is now very concise, built on top of quality libraries, and it resilient to any changes in the future to the exception hierarchy.

This is really our complete solution to the problem as described. The next two sections describe two optional enhancements to this solution.

Going deeper

What we really want is to be completely isolated from any exceptions generated by a piece of code. The solution above is still vulnerable to one issue: exceptions from pure code hiding in an unevaluated thunk. Even the most brute force catchAny is susceptible to this problem.

import Control.Exception
+
+catchAny :: IO a -> (SomeException -> IO a) -> IO a
+catchAny = Control.Exception.catch
+
+dangerous :: IO Int
+dangerous = return $ error "Unevaluated!"
+
+main :: IO ()
+main = do
+    res <- catchAny dangerous (const $ return (-1))
+    putStrLn "About to print the result"
+    putStrLn $ "Result: " ++ show res
+    putStrLn "Hmm... does this ever get printed?"

What we want to do is force evaluation of the value, and if forcing throws any exceptions, catch them. With the deepseq package, this is easy. Let's call these new functions tryAnyDeep and catchAnyDeep, and base them on our previously defined tryAny:

tryAnyDeep :: NFData a => IO a -> IO (Either SomeException a)
+tryAnyDeep action = tryAny $ do
+    res <- action
+    return $!! res -- here's the magic
+
+catchAnyDeep :: NFData a => IO a -> (SomeException -> IO a) -> IO a
+catchAnyDeep action onE = tryAnyDeep action >>= either onE return
+
+dangerous :: IO Int
+dangerous = return $ error "Unevaluated!"
+
+main :: IO ()
+main = do
+    res <- catchAnyDeep dangerous (const $ return (-1))
+    putStrLn "About to print the result"
+    putStrLn $ "Result: " ++ show res
+    putStrLn "Hmm... does this ever get printed?"

We can now have complete* confidence in the values returned from catchAny.

* Complete confidence, assuming we trust the NFData instances, but that's a different problem.

Transformers

OK, one final twist: can we catch exceptions in a monad transformer stack? Many of you may be aware that I'm a big advocate of Bas van Dijk's monad-control package, and the related lifted-base package. monad-control allows for a consistent manner of lifted control operations within a monad transformer stack. Can we generalize our tryAny and catchAny functions to work with arbitrary transformer stacks? Fortunately, we can:

tryAnyIO :: IO a -> IO (Either SomeException a)
+tryAnyIO action = withAsync action waitCatch
+
+tryAny :: MonadBaseControl IO m => m a -> m (Either SomeException a)
+tryAny action =
+    -- MAGIC!
+    liftBaseWith (\runInIO -> tryAnyIO (runInIO action)) >>=
+    either (return . Left) (liftM Right . restoreM)
+
+catchAny :: MonadBaseControl IO m => m a -> (SomeException -> m a) -> m a
+catchAny action onE = tryAny action >>= either onE return
+
+tryAnyDeep :: (MonadBaseControl IO m, NFData a)
+           => m a
+           -> m (Either SomeException a)
+tryAnyDeep action = tryAny $ do
+    res <- action
+    return $!! res -- here's the magic
+
+catchAnyDeep :: (MonadBaseControl IO m, NFData a)
+             => m a
+             -> (SomeException -> m a)
+             -> m a
+catchAnyDeep action onE = tryAnyDeep action >>= either onE return
+
+dangerous :: Monad m => m Int
+dangerous = return $ error "Unevaluated!"
+
+main :: IO ()
+main = flip runReaderT () $ do
+    res <- catchAnyDeep dangerous (const $ return (-1))
+    liftIO $ putStrLn "About to print the result"
+    liftIO $ putStrLn $ "Result: " ++ show res
+    liftIO $ putStrLn "Hmm... does this ever get printed?"

That implementation of tryAny is a little bit hairy, but it essentially means:

  • Capture the monadic state when we start running (via liftWithBase).
  • Put the state inside the IO monad by modifying the internal value (via runInIO).
  • Now that we have an IO action, run tryAnyIO on it.
  • Get back the result.
    • If an exception was thrown, then return that exception.
    • If a value was returned, unwrap the new monadic state from and extract the actual return value (via restoreM).

Moving forward

I've added these functions to the classy-prelude Github repo, and after a bit more testing will be releasing them. But I think including something like this in a more accessible place makes a lot of sense, as we should be trying to make the correct approach easier to implement.

I'd be happy to hear ideas on how to improve the code, or where the correct place to put these functions might be.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/07/four-more-chapters.html b/public/blog/2013/07/four-more-chapters.html new file mode 100644 index 00000000..70710f2a --- /dev/null +++ b/public/blog/2013/07/four-more-chapters.html @@ -0,0 +1,26 @@ + Four more chapters ready +

Four more chapters ready

July 19, 2013

GravatarBy Michael Snoyman

As I mentioned on Google+, I've just changed the default book link on the site +to point to the new version 1.2. The book is not yet fully migrated over to +version 1.2 of Yesod, but the entire basics section (i.e., the first 11 +chapters) are. In addition, as of today, I've just converted four more +chapters:

Some comments about notable changes:

  • RESTful content had a fairly substantial overhaul to reflect the new +TypedContent approach we have. I personally think the new approach (and +therefore the new documentation) is much easier to follow.

  • Similarly, due to the major change in how the monad transformers work, +Yesod's monads is quite different. There are also some explanations for +design decisions towards the end. If you want to understand some of the deeper +parts of Yesod, I'd recommend reading this chapter again.

  • The blog example (finally) has proper formatting, instead of the terrible +"let's copy some Literate Haskell" hack that I used previously.

  • JSON web service is mostly unchanged, and is pretty boring. I'll be adding a +fancier JSON example after all the chapters are converted.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/07/runtime-lucius-mixins.html b/public/blog/2013/07/runtime-lucius-mixins.html new file mode 100644 index 00000000..ad80df52 --- /dev/null +++ b/public/blog/2013/07/runtime-lucius-mixins.html @@ -0,0 +1,23 @@ + Runtime Lucius: now with mixins! +

Runtime Lucius: now with mixins!

July 1, 2013

GravatarBy Michael Snoyman

About two months ago, I announced that Lucius now had mixin +support. +Unfortunately, it was missing something important: support in runtime Lucius. +Many of you have probably never used runtime Lucius, but it's the component +underlying Lucius's ability to do live code reloading during development. So +without this feature, it's impossible to use mixins when using yesod devel.

As of shakespeare-css 1.0.6.1, this is no longer a problem: mixins should now +work perfectly with yesod devel. In order to take advantage of this, just add +a minimum bound on your shakespeare-css constraint in your cabal file. (The +next release of yesod-platform will include this change.)

If anyone finds any problems, let me know.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/08/exceptions-and-monad-transformers.html b/public/blog/2013/08/exceptions-and-monad-transformers.html new file mode 100644 index 00000000..78a67a6d --- /dev/null +++ b/public/blog/2013/08/exceptions-and-monad-transformers.html @@ -0,0 +1,269 @@ + Exceptions and monad transformers +

Exceptions and monad transformers

August 19, 2013

GravatarBy Michael Snoyman

John Wiegley and I are currently working- together with some members of the community- on various exception handling related code. As part of this work, we're also going to be publishing a number of tutorials on this topic. Eventually, we'll collect all of this information together into a more cohesive whole, but for now we wanted to get the content out there to the community as we produce it.

Note that this content is available on the School of Haskell; I recommend reading it there to be able to use the active code.


Love them or hate them, runtime exceptions are a reality of writing Haskell code. Consider the following fake code:

myFunc = do
+    resource <- acquireScarceResource
+    useResource resource
+    releaseResource resource

In a world without exceptions, this seems completely reasonable. However, what happens if the call to useResource throws a runtime exception? In such a case, releaseResource would never be called, and our scarce resource would never be released. In concurrent code dealing with mutexes, such a bug could lead to a deadlock.

The solution to this problem is well known: use the bracket function. Then our above example reduces to myFunc = bracket acquireScarceResource releaseResource useResource. We're now safe from any exception thrown by useResource, and even from asynchronous exceptions (a topic I'll try to avoid in this post).

But let's analyze the type signature of bracket:

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

How do we use such a function in monad transformer stack, where the functions we pass in won't live in the IO monad itself?

Note that for the rest of this tutorial, I'm going to talk about the finally function instead of bracket, which is really just a simplified version of the latter. The reasoning used will apply to not only bracket, but also to many other functions in Control.Exception. Its signature is IO a -> IO b -> IO a, and its purpose is to ensure that the second function is called regardless of what exceptions are thrown by the first (or any asynchronous exceptions... sorry for bringing those up again).

The simple case: the ReaderT transformer

Let's forget about any kind of grand solution. Instead, let's take one of the simplest monad transformers that exists, ReaderT, and see how we can write a finally that works with it. Since a ReaderT simply passes in some environment to all actions, this turns out to be really easy:

import Control.Monad.Reader
+import Control.Exception (finally, try, ErrorCall)
+
+finallyReaderT :: ReaderT r IO a -> ReaderT r IO b -> ReaderT r IO a
+finallyReaderT a sequel = do
+    r <- ask
+    liftIO $ runReaderT a r `finally` runReaderT sequel r
+
+main :: IO ()
+main = do
+    res <- try $ runReaderT
+        (step1 `finallyReaderT` step2)
+        "This is the environment"
+    print (res :: Either ErrorCall ())
+  where
+    step1 = do
+        r <- ask
+        liftIO $ putStrLn $ "In step1, r == " ++ show r
+        error "Erroring out, bye!"
+    step2 = do
+        r <- ask
+        liftIO $ putStrLn $ "In step2, r == " ++ show r

That turns out to be pretty simple. And by reusing the existing finally function, we can feel comfortable knowing that our implementation is correct (or at least as correct as finally itself).

First complication: mutable state

But ReaderT is a really simple case to support, since it's just a read-only environment. Let's up the ante a bit, and try using StateT instead. We'll now need to deal with threading the mutable state variable through. Before jumping into the code, let's consider abstractly how would could achieve this threading. We have two possible cases: an exception is thrown before calling the cleanup function, or an exception is not thrown. If no exception is thrown, then we can extract the state value from the result of the first function call, pass that to the cleanup function, and then return the final state value as the new mutable state.

But if an exception is thrown, there won't be a chance to extract the updated state value from the first function. In that case, we'll only have the initial state value available to pass through to the cleanup function. (The new result state is irrelevant, since the exception is going to be rethrown anyway.) As described until now, let's see what this kind of finally implementation may look like.

import Prelude hiding (catch)
+import Control.Monad.State
+import Control.Exception
+
+finallyStateT :: StateT s IO a -> StateT s IO b -> StateT s IO a
+finallyStateT a sequel = do
+    s1 <- get
+    (result, s2) <- liftIO $ runStateT a s1 `catch` \e -> do
+        _ignored <- runStateT sequel s1
+        throwIO (e :: SomeException)
+    (_ignored, s3) <- liftIO $ runStateT sequel s2
+    put s3
+    return result
+
+main :: IO ()
+main = do
+    res <- try $ runStateT
+        (step1 `finallyStateT` step2)
+        1
+    print (res :: Either ErrorCall ((), Int))
+  where
+    step1 = do
+        s <- get
+        liftIO $ putStrLn $ "In step1, s == " ++ show s
+        put $ s + 1
+        error "Erroring out, bye!" -- Try commenting me out
+    step2 = do
+        s <- get
+        liftIO $ putStrLn $ "In step2, s == " ++ show s
+        put $ s + 1

This seems to work, but there are a two problems:

  • We were not able to reuse the standard finally function to implement this, since it wouldn't allow us to manually thread state. Practically, that means our implementation is susceptible to asynchronous exceptions (argh... those came up again).
  • The behavior is pretty inconsistent. In one case, we use the original mutable state value and ignore the updated state from the cleanup function. In the other, we use the updated state value from the first function and keep the updated mutable state.

So let's instead try out a different approach. Regardless of whether an exception is thrown or not, we'll call the cleanup function using the original mutable state, and always ignore the state produced by the cleanup function.

import Control.Monad.State
+import Control.Exception
+
+finallyStateT :: StateT s IO a -> StateT s IO b -> StateT s IO a
+finallyStateT a sequel = do
+    s1 <- get
+    (result, s2) <- liftIO $ runStateT a s1 `finally`
+                             runStateT sequel s1
+    put s2
+    return result
+
+main :: IO ()
+main = do
+    res <- try $ runStateT
+        (step1 `finallyStateT` step2)
+        1
+    print (res :: Either ErrorCall ((), Int))
+  where
+    step1 = do
+        s <- get
+        liftIO $ putStrLn $ "In step1, s == " ++ show s
+        put $ s + 1
+        error "Erroring out, bye!" -- Try commenting me out
+    step2 = do
+        s <- get
+        liftIO $ putStrLn $ "In step2, s == " ++ show s
+        put $ s + 1

This solves my two complaints from before, but unfortunately introduces a new one: the behavior is not really intuitive in the non-exception case. Nonetheless, in my opinion, this is the right way to implement things due to the consistency. We'll cover a method to get back the more intuitive semantics towards the end of this post.

Second complication: alternative exit paths

The previous section essentially provided two approaches to writing a transformer-based finally: base it off of primitives like catch, or reuse finally. It turns out that both of these approaches have quite a bit of prior art. In the first case, there are the MonadCatchIO-mtl and MonadCatchIO-transformers packages, as well as the more recent exceptions package. These all define a typeclass for the primitive operations of catching and masking asynchronous exceptions (we just can't escape those, can we?).

Let's extract the catch logic from our first StateT example above to demonstrate the technique:

import Prelude hiding (catch)
+import Control.Monad.State
+import Control.Exception
+
+-- show
+catchStateT :: Exception e
+            => StateT s IO a
+            -> (e -> StateT s IO a)
+            -> StateT s IO a
+catchStateT a onE = do
+    s1 <- get
+    (result, s2) <- liftIO $ runStateT a s1 `catch` \e ->
+        runStateT (onE e) s1
+    put s2
+    return result
+
+finallyStateT :: StateT s IO a -> StateT s IO b -> StateT s IO a
+finallyStateT a sequel = do
+    result <- a `catchStateT` \e -> do
+        _ignored <- sequel
+        liftIO $ throwIO (e :: SomeException)
+    _ignored <- sequel
+    return result
+-- /show
+
+main :: IO ()
+main = do
+    res <- try $ runStateT
+        (step1 `finallyStateT` step2)
+        1
+    print (res :: Either ErrorCall ((), Int))
+  where
+    step1 = do
+        s <- get
+        liftIO $ putStrLn $ "In step1, s == " ++ show s
+        put $ s + 1
+        error "Erroring out, bye!" -- Try commenting me out
+    step2 = do
+        s <- get
+        liftIO $ putStrLn $ "In step2, s == " ++ show s
+        put $ s + 1

That's certainly a bit prettier than what we had before (though it's still not async-exception safe). And what's really nice is that this definition of finallyStateT doesn't actually have anything StateT-specific about it. So presuming we had a catch function for some other transformer, we could reuse the same definition. Let's try this out for ErrorT.

import Prelude hiding (catch)
+import Control.Monad.Error
+import Control.Exception
+
+catchErrorT :: (Exception e, Error s)
+            => ErrorT s IO a
+            -> (e -> ErrorT s IO a)
+            -> ErrorT s IO a
+catchErrorT a onE = do
+    eresult <- liftIO $ runErrorT a `catch` \e ->
+        runErrorT (onE e)
+    either throwError return eresult
+
+finallyErrorT :: Error s => ErrorT s IO a -> ErrorT s IO b -> ErrorT s IO a
+finallyErrorT a sequel = do
+    result <- a `catchErrorT` \e -> do
+        _ignored <- sequel
+        liftIO $ throwIO (e :: SomeException)
+    _ignored <- sequel
+    return result
+
+main :: IO ()
+main = do
+    res <- try $ runErrorT
+        (step1 `finallyErrorT` step2)
+    print (res :: Either ErrorCall (Either String ()))
+  where
+    step1 = do
+        liftIO $ putStrLn $ "In step1"
+        throwError "This should be interesting"
+    step2 = liftIO $ putStrLn $ "In step2"

Go ahead and run that code snippet. Do you notice something not there to be noticed? That's right, the cleanup function is never called! Here we have a function called finally which, despite its name, doesn't actually call the cleanup function when it finally exits. What gives?

The issue is that, with an ErrorT s IO stack, there are three ways to exit the function: normally, with a runtime exception, or with a throwError result. In finallyErrorT, the call to catchErrorT accounts for the runtime exception case, and the following call to sequel accounts for the normal exit case. But there's no way to account for the throwError case without adding ErrorT-specific logic to finally (we'll do that in a moment).

You can try this example out with MonadCatchIO-mtl or MonadCatchIO-transformers and reproduce this buggy behavior. This affects both the ErrorT and ContT transformers. In the exceptions package, this also applies to the CatchT type. (John Wiegley discussed this issue with Edward, who described this as a documentation bug, since CatchT should not be used on top of IO.)

Doing this correctly is certainly possible, as you can see with the following example:

import Control.Monad.Error
+import Control.Exception
+
+-- show
+finallyErrorT :: Error s => ErrorT s IO a -> ErrorT s IO b -> ErrorT s IO a
+finallyErrorT a sequel = do
+    eresult <- liftIO $ runErrorT a `finally` runErrorT sequel
+    either throwError return eresult
+-- /show
+
+main :: IO ()
+main = do
+    res <- try $ runErrorT
+        (step1 `finallyErrorT` step2)
+    print (res :: Either ErrorCall (Either String ()))
+  where
+    step1 = do
+        liftIO $ putStrLn $ "In step1"
+        throwError "This should be interesting"
+    step2 = liftIO $ putStrLn $ "In step2"

But this seems to imply that we would need to write a separate implementation of finally for each and every transformer, which would be tedious and error-prone. There must be a better way.

monad-control

If you look at our various implementations so far, they all follow a similar pattern: embedding the state of the monad inside the value, running the underlying monadic action, and then rebuilding the monadic state from the result. This procedure can be done with many different transformers (basically, all common transformers except ContT). To capture this concept, we have the MonadTransControl and MonadBaseControl typeclasses in the monad-control package.

monad-control is frankly pretty complicated, and I'm not going to delve into all of its details here. Instead, I'll point you to the lifted-base package, which uses monad-control to create transformer-friendly versions of many common functions in base. As you'd expect based on this tutorial, exception handling functions are present, but also concurrency, timeouts, and mutable variables (which will become important in just a moment).

monad-control is- as far as I know- the third attempt at a library to cover these concepts. Previous solutions were monad-peel and my own MonadInvertIO, part of earlier versions of neither. While in my opinion these other two approaches are easier to understand, monad-control is highly optimized, and therefore gets my recommendation as the go-to library for handling monad transformer stacks.

More intuitive mutable state

We're left with one annoyance with the monad-control-based solution: we lose any mutations performed to our mutable state either before an exception is thrown, or in our cleanup functions. Is there some way to keep the elegance of monad-control but get back the more intuitive behavior? The answer is yes, but you may not like it.

When dealing with either StateT or WriterT transformers, the behavior which I would consider most intuitive would be that any mutations are immediately captured. Unfortunately, this can't be achieved at all using the standard StateT and WriterT implementations, since the monadic state is thrown away as soon as an exception is thrown.

In this case, my solution is to use a mutable variable- such as an IORef- and keep a reference to it in a ReaderT. As an example, this technique is used by the HandlerT transformer used in Yesod.

To demonstrate the concept, here's an implementation of the RWST monad transformer which uses IORefs for holding mutable state. For completeness, I'll include appropriate instances for monad-control and demonstrate usage of lifted-base.

{-# OPTIONS_GHC -Wall -Werror #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+import Control.Monad.RWS.Class
+import Control.Monad.Reader
+import Control.Monad.Base
+import Control.Applicative (Applicative)
+import Data.IORef.Lifted
+import Data.Monoid (Monoid, (<>), mempty)
+import Control.Exception.Lifted
+import Control.Monad.Trans.Control
+
+data Env r w s = Env
+    { envReader :: !r
+    , envWriter :: !(IORef w)
+    , envState  :: !(IORef s)
+    }
+
+newtype RWST r w s m a = RWST (ReaderT (Env r w s) m a)
+    deriving (Functor, Applicative, Monad, MonadIO, MonadTrans)
+
+instance Monad m => MonadReader r (RWST r w s m) where
+    ask = RWST $ liftM envReader ask
+    local f (RWST g) =
+        RWST $ local f' g
+      where
+        f' env = env { envReader = f $ envReader env }
+
+instance (MonadBase IO m, Monoid w) => MonadWriter w (RWST r w s m) where
+    tell w = RWST $ ask >>= flip modifyIORef (<> w) . envWriter
+    listen (RWST (ReaderT f)) = RWST $ ReaderT $ \env -> do
+        iwriter <- newIORef mempty
+        result <- f env { envWriter = iwriter }
+        w <- readIORef iwriter
+        return (result, w)
+    pass (RWST (ReaderT f)) = RWST $ ReaderT $ \env -> do
+        (result, g) <- f env
+        modifyIORef (envWriter env) g
+        return result
+
+instance MonadBase IO m => MonadState s (RWST r w s m) where
+    get = RWST $ ask >>= readIORef . envState
+    put s = RWST $ ask >>= flip writeIORef s . envState
+
+instance (MonadBase IO m, Monoid w) => MonadRWS r w s (RWST r w s m)
+
+runRWST :: (MonadBase IO m, Monoid w) => RWST r w s m a -> r -> s -> m (a, s, w)
+runRWST (RWST (ReaderT f)) r s = do
+    iwriter <- newIORef mempty
+    istate <- newIORef s
+    a <- f $ Env r iwriter istate
+    w <- readIORef iwriter
+    s' <- readIORef istate
+    return (a, s', w)
+
+instance MonadBase b m => MonadBase b (RWST r w s m) where
+    liftBase = lift . liftBase
+
+instance MonadTransControl (RWST r w s) where
+    newtype StT (RWST r w s) a = StRWS {unStRWS :: a}
+    liftWith f = RWST $ ReaderT $ \r -> f $ \(RWST t) -> liftM StRWS $ runReaderT t r
+    restoreT = RWST . ReaderT . const . liftM unStRWS
+
+instance MonadBaseControl b m => MonadBaseControl b (RWST r w s m) where
+    newtype StM (RWST r w s m) a = ST { unST :: ComposeSt (RWST r w s) m a }
+    liftBaseWith = defaultLiftBaseWith ST
+    restoreM     = defaultRestoreM unST
+
+main :: IO ()
+main = do
+    res <- try $ runRWST
+        (step1 `finally` step2)
+        "This is the environment"
+        0
+    print (res :: Either ErrorCall ((), Int, [Bool]))
+  where
+    step1 = do
+        liftIO $ putStrLn $ "In step1"
+        modify (+ 1)
+        error "User exception"
+    step2 = do
+        s <- get
+        liftIO $ putStrLn $ "In step2: " ++ show s

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/09/classy-mono.html b/public/blog/2013/09/classy-mono.html new file mode 100644 index 00000000..2d5374a3 --- /dev/null +++ b/public/blog/2013/09/classy-mono.html @@ -0,0 +1,38 @@ + Announce: mono-traversable and classy-prelude 0.6 +

Announce: mono-traversable and classy-prelude 0.6

September 27, 2013

GravatarBy Greg Weber

We are pleased to announce the first release of the mono-traversable package and version 0.6 of the classy-prelude. A special thanks goes to FPComplete and DocMunch for sponsoring its development and to Edward Kmett for advising the development of mono-traversable.

Goals

This release takes the implementation of classy-prelude in an entirely different direction that we are very excited about. +First lets review why classy-prelude was created:

classy-prelude tackles a few problems with the existing Prelude that most experienced Haskell programers are doing already.

  • removing all partial functions
  • modernizing the prelude
    • generally use Text instead of String (basic-prelude also does this)
    • encourage the use of appropriate data structures such as Vectors or HashMaps instead of always using lists and associated lists

But classy-prelude was also created to confront what I believe to be Haskell's most obvious wart: name-spacing and the need to qualify functions. +Haskell has only offered up one solution to avoid using module qualifications or mangled function names: make greater use of generic type-classes.

Change of implementation: from ad-hoc to lawful mono-traversable typeclasses

Part of the problem of the existing Prelude is that it tends to just operate on lists. My understanding is that the core Haskell libraries are moving in the direction of using polymorphic type-classes (for example, more usage of Foldable and Traversable).

However, there is still a problem with this direction: it does not work with monomorphic containers such as Text and ByteString. A Functor operates over a container, so we can fmap over both a Vector a and a list [a]. But Text and ByteString are monomorphic containers: their type does not tell us what they contain, so there is no way we can fmap over them. But it is frustrating: Text is obviously a container of characters and ByteString is obviously a container of bytes. classy-prelude solved this in an ad-hoc way, by creating a new type-class for each function to operate on multiple containers and adding on more type parameters as needed. For example, map was implemented through a CanMap type class:

class CanMap ci co i o | ci -> i, co -> o, ci o -> co, co i -> ci where
+    map ∷ (i → o) → ci → co

The previous approach of classy prelude met its design goals, but it had drawbacks. The main problem was that APIs with lots of type variables and functional dependencies give hard to decipher error messages. I also came across a case of using a complex data structure where GHC could not resolve the types and switching to the Prelude version immediately resolved it. But switching to the standard Prelude version of a function (when operating over lists) was already one of my techniques to help understand the error messages. I was much happier with the error messages from using fmap (over polymorphic containers) than using the map from classy-prelude.

The original classy-prelude approach missed the chance to solve the underlying monomorphic vs polymorphic problem in a well-grounded way. This is the motivation for the mono-traversable package: we just use a type family to declare the type that the mono-morphic container contains.

Note that all typeclasses have been prefixed with Mono, and functions have +been prefixed with o The mnemonic for o is only one or alternatively +mono, but m is overused in Haskell, so take the second letter instead. +We are open to suggestions for better naming schemes.

type family Element mofu
+type instance Element T.Text = Char                                                      
+type instance Element [a] = a  
+
+class MonoFunctor mofu where
+    omap ∷ (Element mofu → Element mofu) → mofu → mofu
+    default omap ∷ (Functor f, Element (f a) ~ a, f a ~ mofu) ⇒ (a → a) → f a → f a 
+    omap = fmap
+
+instance MonoFunctor T.Text where
+    omap = T.map
+
+instance MonoFunctor [a]

All of the laws for the polymorphic typeclasses apply to their monomorphic +cousins. Thus, even though a MonoFunctor instance for Set could +be defined, it is omitted since it could violate the functor +law of omap f . omap g = omap (f . g) because a Set must compress duplicate elements created by f.

Note that omap does not change type like fmap, its type is the equivalent of (a -> a) -> f a -> f a, there is no b. Users of the classy-prelude no longer have a single map function. map is now aliased to fmap, and they have to choose between fmap and omap (and a different map for Set).

The MonoTraversable module of mono-traversable defined MonoFunctor, MonoFoldable, and MonoTraversable.

Any Functor or Foldable can be made into a mono by using a default instance that will use Functor or Foldable.

instance MonoFunctor  (Maybe a)
+instance MonoFoldable (Maybe a)

The MonoFoldable typeclass provides a lot of functionality, and is strictly more inclusive than the normal Foldable functionality. So the classy-prelude exports just the MonoFoldable definitions:

concatMap ∷ (Monoid m, MonoFoldable c) ⇒ (Element c → m) → c → m 
+concatMap = ofoldMap

Previously that would have had 4 type variable with functional dependencies between them, but now we have just 2 type variables constrainted by 2 type-classes and a type family. The constraints all help communicate what the types express in a straightforward way, whereas functional dependencies requires more contemplation to figure out what is actually going on. For an implementer this is much nicer. From an end user perspective the function is fairly intuitive and they already understood how it is behaved, but now the error messages should be easier to decipher when something goes wrong. Of course, deciphering error messages is intimately related to understanding the types. The lawful typeclass approach is also an approach that can create simpler and easier to understand types and thus better error messages for end users.

mono-traversable also exports some typeclasses that we consider more experimental (IsSet, IsMap, and IsSequence), but that we hope can provide the common functionality that is needed for a Prelude. We look forward to your feedback and help to improve these.

Some of the functionality discussed here is similar to the ListLike package, but we are hoping mono-traversable is a refinement of the underlying type-classes. Perhaps one of the problems of ListLike is that you are encouraged to import it qualified: hopefully the combination of mono-traversable name mangling and classy-prelude is a big improvement so that users can use mono-traversable unqualified or choose to entirely replace the Prelude and use non-mangled names.

Partial functions and non-empty data structures

Another important aspect of classy-prelude's modernization is providing better alternatives to parital functions. In the 0.6 release we try to make it easier to use semigroups, including Data.List.NonEmpty. Some of Haskell's partial functions come from attempting to operate on a list that must be non-empty. We have a powerful type sytem, so we should be putting it to use! Rather than partial functions, or even functions from safe, we can define data structures have at least one element with Data.List.NonEmpty

head (x:|[]) == x

mono-traversable includes a very preliminary NonNull type-class that attempts to extend the functionality of Data.List.NonEmpty from just lists to any type of sequential data.

Performance

Besides encouraging appropriate data structures, mono-traversable is also focused on allowing for high performance definitions. Some other packages such as monoid-subclasses tackle some of the functionality, but only dealing with an entire Monoid is not as performant as operating on individual elements. Similarly, the experimental NonNull typeclass is much less featureful than the non-empty package, but is designed to use data structures in a performant way.

Summary

  • classy-prelude is now based on lawful typeclasses, particularly the addition of mono-traversable
  • there is a lot of experimentation going on here, so we look forward to your feedback!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/09/folding-lines-conduit.html b/public/blog/2013/09/folding-lines-conduit.html new file mode 100644 index 00000000..b31f5e94 --- /dev/null +++ b/public/blog/2013/09/folding-lines-conduit.html @@ -0,0 +1,125 @@ + Folding lines in conduit +

Folding lines in conduit

September 24, 2013

GravatarBy Michael Snoyman

This blog post is in response to a post by Gabriel Gonzalez on perfect +streaming. +The issue he raises is a good one: how to do efficient processing of chunks of +data, where each chunk may contain a large amount of data that should be +streamed.

While I appreciate the stated problem, I dislike the given solution. Gabriel's +approach is to invert the entirety of the stream processing, forcing the user +to have to explicitly pull data out of a producer instead of the more usual, +declarative semantics most of the streaming data libraries (enumerator, +conduit, and pipes) adhere to. (In fact, this critique is not limited to the +specific technique employed here, but to the general approach to chunked data +employed by pipes-bytestring and pipes-parse. I'm writing up a more detailed +blog post on that subject separately.)

Nonetheless, Gabriel's criticism of the simplistic approach the lines +function takes in conduit is valid. While the function is convenient for many +common use cases, it is not robust enough for data with large lines. But +instead of inverting the entirety of the streaming library to fix the problem, +I'd rather just reuse a more common idiom in Haskell: the fold.

If you want to just jump in, the full code is available on School of +Haskell. +There are some auxilary functions in there that should really be in +Data.Conduit.Text itself. But let's focus on the foldLines function:

foldLines :: Monad m
+          => (a -> ConduitM Text o m a)
+          -> a
+          -> ConduitM Text o m a
+foldLines f =
+    start
+  where
+    start a = CL.peek >>= maybe (return a) (const $ loop $ f a)
+
+    loop consumer = do
+        a <- takeWhileText (/= '\n') =$= do
+            a <- consumer
+            CL.sinkNull
+            return a
+        dropText 1
+        start a

The logic here is pretty straight-forward, let's just step through it. The user +provides an initial value for the accumulator, which is initially passed to the +start helper function. start peeks at the input stream; if there's no data +in the stream, we immediately return the accumulator. If there is data in the +stream, peek ensures that the data will be put back on the stream. We use +const to ignore that initial chunk (we'll consume it in a moment), and pass +the accumulator to the user supplied function to get the user's consuming +function.

loop is able to use all the glory of standard conduit composition. We use +the takeWhileText function to stream everything up to the first newline +character to the user's consumer. sinkNull is employed to ensure the entire +input is consumed, regardless of the behavior of the user function. Notice also +that the user is unable to accidentally consume more content than the current +line. After all of that, we return the new accumulator value, use dropText +to drop the newline character on the stream, and then we start again with the +new accumulator.

To demonstrate usage of this function, I've included a small function for +getting the line number and character count for all the lines in a file.

type LineNumber = Int
+type CharCount = Int
+data LineStat = LineStat !LineNumber !CharCount
+
+myFunc :: Monad m => LineNumber -> ConduitM Text LineStat m LineNumber
+myFunc number' = do
+    count <- CL.fold (\count t -> count + T.length t) 0
+    yield $ LineStat number count
+    return number
+  where
+    number = number' + 1
+
+showLineStat :: LineStat -> Text
+showLineStat (LineStat number count) = T.concat
+    [ "Line number "
+    , T.pack $ show number
+    , " has character count of: "
+    , T.pack $ show count
+    ]
+
+main :: IO ()
+main = runResourceT
+     $ CB.sourceFile "input.txt"
+    $$ CT.decode CT.utf8
+    =$ void (foldLines myFunc 0)
+    =$ CL.map showLineStat
+    =$ unlinesText
+    =$ CT.encode CT.utf8
+    =$ CB.sinkHandle stdout

myFunc is able to use all the standard conduit machinery to do its work. It +uses a standard fold to sum up the length of each line, and then yields a +LineStat for each line. For this kind of usage, it would probably make sense +to include some kind of a scan-like function, but folding works fine.

main itself is a bit verbose, since it has to deal with character +encoding/decoding issues. (Tangentially, I've considered adding file read/write +support to Data.Conduit.Text, but don't like adding implicit character +encoding/decoding. I'd be interested on hearing people's thoughts on this.)

Bonus points: if you feel like optimizing this, try using +blaze-builder-conduit and turn showLineStat into a function of type +LineStat -> Builder.

I think this style of solution is far preferable to having to reinvent the +wheel to deal with chunked data. Unfortunately, this style of coding seems to +be out of reach for pipes. To understand why, let's look at the implementation +of takeWhileText, the powerhouse underneath foldLines:

takeWhileText :: Monad m
+              => (Char -> Bool)
+              -> Conduit Text m Text
+takeWhileText p =
+    loop
+  where
+    loop = await >>= maybe (return ()) go
+    go t =
+        case T.span p t of
+            (x, y)
+                | T.null y -> yield x >> loop
+                | otherwise -> yield x >> leftover y

The logic is pretty simple. Loop over the input. If there is no input, we're +done. If there is an input chunk available, split it based on the provided +predicate. If the second part (y) is null, it means that the entire chunk +matched the predicate, so we should yield the chunk and continue looping.

If y is not null, then we simply yield the piece that did match (x) and +then return the rest as leftovers, to be consumed later. To me, this is the +most natural way to express the algorithm. However, pipes does not provide +leftover support, and therefore has to invert streaming data to instead be +explicit pulling from a producer. As I mentioned before, I'll go into more +details in a later blog post on the problems with this approach.

Gabriel's FreeT trick is interesting, and certainly clever. Given the +constraints of pipes, it seems like a good solution to the problem. However, +let's not confuse a constrained good solution with the ideal solution. In my +opinion, being able to reuse commonly used abstractions is far preferable to +needing to invent lots of clever hacks.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/09/lens-based-classy-prelude.html b/public/blog/2013/09/lens-based-classy-prelude.html new file mode 100644 index 00000000..d0d7888b --- /dev/null +++ b/public/blog/2013/09/lens-based-classy-prelude.html @@ -0,0 +1,68 @@ + lens-based classy-prelude +

lens-based classy-prelude

September 3, 2013

GravatarBy Michael Snoyman

I recently had an email thread with Greg Weber and Max Cantor about +classy-prelude. We ended up focusing on classy-prelude's implementation of +map. Currently, classy-prelude defines a CanMap typeclass as follows:

class CanMap ci co i o | ci -> i, co -> o, ci o -> co, co i -> ci where
+    map :: (i -> o) -> ci -> co

Our conversation revolved mostly around how this leads to difficult type +signatures and error messages, which is certainly a known problem with the +class-based approach. However, that's not what I want to focus on now. (If +anyone's interested in that conversation, let me know, I think it's an +interesting one to discuss more broadly as well.)

The issue we're trying to solve with this typeclass is dealing with many +different shapes of containers, from polymorphic without constraints (e.g., +list, vector), to constrained polymorphic (e.g., Set) to completely monomorphic +(e.g., ByteString, Text). I wanted to investigate prior art in this area, so I +decided to look at Edward's ever-popular lens package, something I'm always +itching to learn more about.

I'm by no means an expert on this package, but it seems like the relevant type +class for the map function would be Each, defined as:

class (Functor f, Index s ~ Index t) => Each f s t a b | s -> a, t -> b, s b -> t, t a -> s where
+  each :: IndexedLensLike (Index s) f s t a b

I was shocked at just how similar these two typeclasses were to each other. +Using each, it's possible to define a map function:

map :: Each.Each Lens.Mutator s t a b => (a -> b) -> s -> t
+map = Lens.over Each.each

I tried replacing this in classy-prelude (using FP Haskell +Center to do my coding, of course), and the +result +was nearly perfect. As expected, I had to modify some type signatures in my +test suite to use Each instead of CanMap. Also, there appear are no +instances of Each for Set and HashSet; please see my explanation +below based on a very good explanation I got from Edward.

To me, this is a very interesting direction to consider heading in. +classy-prelude has always focused on pragmatism, and retaining the +programming style most users are accustomed to. lens, on the other hand, +takes a much more principled approach to its type classes, but has a steeper +learning curve. Perhaps by building classy-prelude on top of lens in this +manner, we can get an easy-to-learn library which gradually exposes users to +powerful and well-designed abstractions. This would also allow the community to +focus on making instances for one set of typeclasses, and then users could +essentially layer whatever high-level interface on top of it that they want.

I haven't looked into the other typeclasses in classy-prelude yet, but I have +a strong feeling that many of them can be implemented on top of lens classes.

Is this something others find interesting and worth pursuing?


Coming back to that original thread from Greg and Max, I am a bit concerned +that such a change would only make the error message and type signature issues +even harder to deal with. But classy-prelude has never been a +beginner-friendly project, and at least if both classy-prelude and lens +users got the same terrifying error messages, it might be easier to build up +some common documentation on how to address them.

No Set/HashSet instances

I'm sure most of us are familiar with the common identity:

map f . map g = map (f . g)

This is also the second functor law:

fmap f . fmap g = fmap (f . g)

However, this identity does not apply with Set, as you can see with a simple +example:

import qualified Data.Set as Set
+
+newtype AlwaysEq a = AlwaysEq { unAlwaysEq :: a }
+
+instance Eq (AlwaysEq a) where
+    _ == _ = True
+instance Ord (AlwaysEq a) where
+    _ `compare` _ = EQ
+
+main :: IO ()
+main = do
+    let s = Set.fromList [1..3]
+    print $ (Set.map unAlwaysEq . Set.map AlwaysEq) s
+    print $ Set.map (unAlwaysEq . AlwaysEq) s

Said another way, maping on a Set can change its shape, by causing some +elements to be removed. This is in constrast to other examples of map, which +ensure the shape is maintained.

As far as classy-prelude is concerned, I'd be content to reexport Set.map +under a name like setMap, but not have it represented by the map function.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/09/yesod-platform-1-2-4.html b/public/blog/2013/09/yesod-platform-1-2-4.html new file mode 100644 index 00000000..3260d7dd --- /dev/null +++ b/public/blog/2013/09/yesod-platform-1-2-4.html @@ -0,0 +1,25 @@ + Yesod Platform version 1.2.4 +

Yesod Platform version 1.2.4

September 8, 2013

GravatarBy Michael Snoyman

I'm happy to announce the release of yesod-platform 1.2.4. The Yesod Platform +is a collection of Yesod together with all of its upstream dependencies, pegged +at specific versions which are known to compile and work together. By using the +Yesod Platform, you can often times avoid dependency headaches. Installing is +usually a matter of running:

cabal update && cabal install yesod-platform --force-reinstalls

This release is mostly just a collection of upstream version bumps, though +there are a number of incremental improvements to Yesod itself as well. I +haven't really kept up-to-date with release announcements since Yesod 1.2 came +out, so here's a short list of changes since that release:

  • Improved JSON responses for yesod-auth and yesod-core (Tero Laitinen and Greg Weber).
  • Protocol-independent links for CDN-hosted resources (Iku Iwasa).
  • A nicer yesod devel refresh page (Chris Done).
  • Ability to switch logging level dynamically via shouldLogIO.
  • Improved email authentication password security.
  • Better request body support in yesod-test (Konstantine Rybinov).
  • bootstrap and normalize updates in scaffolding (Iku Iwasa).
  • Update many crypto libraries to latest dependencies (Alexey Kotlyarov and others).
  • No double-compression in gzip middleware (John Lenz).
  • Many dependency updates and documentation improvements (Lubomír Sedlář, Fredrik Carlen and others).

Yesod itself has kept a stable API since 1.2, though there has been some +upstream API breakage. Most users should be able to upgrade to the newest +yesod-platform without incident. If you notice any problems, please bring them +up on the mailing list so that others know what to look out for.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/09/zipsinks-conduit-extra.html b/public/blog/2013/09/zipsinks-conduit-extra.html new file mode 100644 index 00000000..7ef5f9e8 --- /dev/null +++ b/public/blog/2013/09/zipsinks-conduit-extra.html @@ -0,0 +1,35 @@ + ZipSinks and conduit-extra +

ZipSinks and conduit-extra

September 11, 2013

GravatarBy Petr Pudlák

Introducing conduit-extra

Michael Snoyman recently added a new package called conduit-extra into the conduit framework. It's aimed on incubating new ideas around conduit, testing them and seeing if they're worth adding into the main package.

In this post I'd like to introduce one of its first additions.

ZipSink

Motivating example

Our task will be to compute several different power sums from a sequence of numbers. We would like to solve the task by constructing a Sink that will read numbers on its input and compute first n power sums of its input:

powerSums :: (Monad m, Num a) => Int -> Sink a m [a]

The first obvious step is to create a sink that computes the k-th power sum:

import Control.Applicative
+import Control.Seq
+import Data.Conduit
+import Data.Conduit.Extra
+import Data.Conduit.List (fold, sourceList)
+import Data.Traversable (traverse)
+
+powerSum :: (Monad m, Num a) => Int -> Sink a m a
+powerSum k = fold (\s x -> s + x^k) 0

Now we'd like to generalize it to a list of power sums. Unfortunately, if we want to compute a list of sums at once, we'll have to keep the list of sums as the state of fold:

powerSumsAttempt1 :: (Monad m, Num a) => Int -> Sink a m [a]
+powerSumsAttempt1 n = fold f (repeat 0)
+  where
+    f ss x = zipWith (+) ss (map (x^) [1..n])

This involves mapping and zipping and the code is somewhat obscure, clearly much less readable than our original powerSum.

Moreover, while it produces the correct result, it's still problematic. We soon realize that it leaks memory for large inputs, because even though fold is strict, thunks accumulate inside the list. So we have to force the evaluation of the list during folding (using Control.Seq):

powerSumsAttempt2 :: (Monad m, Num a) => Int -> Sink a m [a]
+powerSumsAttempt2 n = fold f (repeat 0)
+  where
+    f ss x = withStrategy (seqList rseq) $ zipWith (+) ss (map (x^) [1..n])

Clearly, this solution has several major drawbacks:

  • The original simple idea is completely lost within auxiliary code.
  • The code isn't composable. We couldn't use powerSum as a building block. Instead, we had to reimplement a more complex variant from scratch.
  • In this case we're dealing with a collection of parallel folds, which can be expressed as a fold over a list. But what if our building blocks were more complex sinks, not simple folds? Then there would be no way how to combine them together, even manually.
  • We don't want to deal with strictness and evaluation, we want conduit to handle it for us.

ZipSinks applicative functor

What we need is a way how to parallelize sinks, combine them so that the input is fed to all of them, and combine the results at the end.

Recall that ConduitM is a Monad and an Applicative and allows to compose conduits using their operations. But the semantic of these instances is different. It's sequential instead of parallel. Expression condF <*> condX runs condF first, when it finishes it continues with condX and then applies the result of condF to the result of condX.

Therefore conduit-extra has introduced a simple newtype wrapper for Sinks

newtype ZipSink i m r = ZipSink { getZipSink :: Sink i m r }

which implements Applicative with the parallel semantics:

  • pure creates a sink that doesn't consume any input, just returns a given value;
  • <*> runs two sinks in parallel until both finish, and then applies the result of the first one to the second one.

Now we can run powerSums in parallel as

powerSumZ :: (Monad m, Num a) => Int -> ZipSink a m a
+powerSumZ = ZipSink . powerSum
+
+powerSumTuple :: (Monad m, Num a) => Int -> Int -> Sink a m (a, a)
+powerSumTuple k l = getZipSink $ (,) <$> powerSumZ k <*> powerSumZ l

Not just that, we can use all the existing functions for Applicatives. Here we had two sinks and created one sink that returned a tuple. Our final aim is to create a sink that returns a list, provided we can create a sink using powerSumZ for each member of [0..n]. This sounds like a job for traverse:

traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)

which, specialized to our case, is typed as

traverse :: (Int -> ZipSink a m b) -> [Int] -> ZipSink a m [b]

It solves our problem completely and very elegantly:

powerSums :: (Monad m, Num a) => Int -> Sink a m [a]
+powerSums n = getZipSink $ traverse powerSumZ [0..n]

Let's print some test results:

main :: IO ()
+main = (sourceList [1..100] $$ powerSums 4) >>= print

which produces

[100,5050,338350,25502500,2050333330]

Notes

  • The above scenario is quite often, therefore conduit-extra already provides a helper function for Sinks that uses ZipSinks only internally:

      broadcast :: (Monad m, Traversable t) => t (Sink i m r) -> Sink i m (t r)

    So an alternative way how to implement powerSums would be

      powerSums' n = broadcast $ map powerSum [0..n]
  • ZipSink's <*> is implemented using zipSinks from Data.Conduit.Util.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/10/core-flaw-pipes-conduit.html b/public/blog/2013/10/core-flaw-pipes-conduit.html new file mode 100644 index 00000000..ac750dba --- /dev/null +++ b/public/blog/2013/10/core-flaw-pipes-conduit.html @@ -0,0 +1,338 @@ + The core flaw of pipes and conduit +

The core flaw of pipes and conduit

October 9, 2013

GravatarBy Michael Snoyman

This blog post has actually been through many iterations as I've investigated +the problems more thoroughly. After looking at the various examples I'll be +bringing below quite a bit, I've come to a conclusion: there is just one single +design decision in pipes which leads to the problems I'll describe. And +conduit has inherited half of this issue, leading to it getting some of these +issues as well (and in some cases different issues).

In this blog post, I'm hoping to motivate the fact that there is actually a +problem. I've been working on some experimental code in conduit which changes +this design, thereby simplifying the internal structure, keeping all of its +current features, and solving the two ways in which conduit currently does not +follow the category laws. I'll describe all of these issues in this post, and +save the new design for my next post.

Note that, while this blog post was actually written first, it can be +considered a continuation of my previous blog +post, which +gives some very concrete examples of the resource issues I raise below.

Goal of this series

Since some people seemed to misunderstand my purpose with this series, let me +make it crystal clear: pipes promises a lot of elegance in dealing with +streaming data, whereas conduit includes functionality which has been demanded +in real world code. Many people have asked me if there is some way to merge +these two advantages, usually asking if conduit can be built on top of pipes. I +started that investigation, and came away with the answer "no" based on the +points below. However, with the change I'll be describing, I think there's a +very good chance for finding a common solution for the goals of both packages.

The flaw: automatic termination

If you look at the core concepts of pipes (e.g., the Pipe datatype in pipes +1.0), +things are very simple. A Pipe can yield a value downstream, await for a value +from upstream, perform a monadic action, and complete processing. This core is +simpler than conduit's core, which includes leftovers and finalizers, as well +as failure when awaiting from upstream.

However, this simplicity includes a heavy cost: there's no way to detect +termination of a stream. As soon as one component of a pipeline terminates, the +rest of the pipeline terminates also. This behavior can be convenient in many +ways; the identity pipe, for example, is expressed simply as forever $ await >>= yield. +However, this decision ends up pushing complexity into many other parts of the +ecosystem, and in some cases makes proper behavior impossible to achieve.

conduit is not immune to this issue. conduit does not have automatic +termination on the consuming side, but does have it on the producing side.

I've held off on commenting on these limitations in pipes previosly, since +until recently pipes has not provided any form of solution for many of the problems +I'm going to raise. With the advent of pipes-bytestring, pipes-parse, and +pipes-safe, there's enough of a solution available to make a meaningful +analysis. After looking at these solutions, my conclusion is:

  • pipes has removed complexity from its core. However, this hasn't in fact +solved complexity, it's merely pushed the complexity to other helper +libraries. In my opinion, the overall solution is far more complex than a +single consistent solution would be.

  • In trying to solve some of these problems outside of the core, pipes has lost +many of its touted principles, such as easy composition.

  • And in some cases, the layered pipes solution does not actually provide +the guarantees we'd expect. Said another way, pipes is buggy.

The remainder of this post will be examples of limitations in pipes and conduit +that result from this functionality. Note that, even though most of the issues +I raise have workarounds, I will not be discussing those in general. My goal is +to point out that the core abstraction is deficient, not address possible +workarounds.

pipes: How do I fold?

conduit provides a single abstraction which addresses all of the data +processing functionality it supports. You get prompt resource handling, chunked +data support, and the ability to fold over an input stream. In pipes, these are +all handled by a separate abstraction. To clarify what I mean, compare the type +signatures for a summing sink (receives all input values and adds them up) and +a printing sink (i.e., prints all input to stdout) in conduit:

sum :: (Monad m, Num a) => Consumer a m a
+print :: (MonadIO m, Show a) => Consumer a m ()

Notice how both of these are conceptually the same: they are both consumers, +which can be composed with other conduits in the normal way (i.e. the =$= +operator and monadic bind). Now compare the types in pipes:

sum :: (Monad m, Num a) => Producer a m () -> m a
+print :: MonadIO m => Show a => Consumer' a m r

These two things are fundamentally different. The first is a function that +takes a data producer and processes it. It does not get to take advantage of +normal composition (though that can be achieved by instead composing on the +producer). pipes has these two separate approaches for processing a stream of +data, and each must be used at different points.

To understand why this is the case, let's look at a simplistic implementation +of sum in conduit:

sum =
+    loop 0
+  where
+    loop x = await >>= maybe (return x) (\y -> loop $! x + y)

The await function returns a Maybe value. If upstream has no more output, +then the sum function is notified with a Nothing value, and can return the +sum it has computed. In pipes, however, if upstream closes, await will simply +never return.

pipes: Dummy return values

You could implement a limited sum function in pipes, such as "add up the +first 10 elements." This would look something like this (I'm specializing to +Integer to help the explanation later):

sum :: Monad m
+    => Int -- ^ total values to add
+    -> Consumer' Integer m Integer
+sum count0 =
+    loop count0 0
+  where
+    loop 0 total = return total
+    loop count total = await >>= \i -> loop (count - 1) (total + i)

That's simple enough. In fact, it's even simpler than the conduit version, +since it doesn't need to pay attention to whether upstream terminated. (Put a +bookmark on that comment, I'll get back to it momentarily.)

So let's go ahead and try to use this. A naive caller function may look like this:

main = do
+    x <- runEffect $ mapM_ yield [1..20] >-> sum 10
+    print x

However, this generates a compiler error:

Couldn't match type `Integer' with `()'

The issue is that the producer has a return type of (), whereas we want to +return an Integer from sum. pipes requires that all components of the +pipeline have the same return value, since any one of them can terminate +computation. In order to work around this, we need to use some kind of a return +value from the producer. A Maybe value works well for this:

main = do
+    x <- runEffect $
+        (mapM_ yield [1..20] >> return Nothing)
+        >-> fmap Just (sum 10)
+    print x

Now our return value is of type Maybe Integer, not Integer. But if we think +about it, this is perfectly logical, since the sum function can't return a +value unless there are at least 10 values in the stream.

This comes back to the fact that the conduit version of the above sum +function is more complicated. That's because it will explicitly deal with +termination of the upstream. This grants it the ability to return the current +total, or if so desired, emulate the pipes behavior above and return a +Nothing to indicate not enough input was provided.

conduit: lack of upstream return values

There's a bit of a mismatch in the conduit abstraction: the most downstream +component (the Sink) can provide a return value, but the rest of the upstream +components cannot. This was an explicit design decision, and in my experience +it's what users actually need the vast majority of the time. (I only needed an +upstream return value once in all of my conduit usage, and was able to work +around the problem using some low-level tricks.) However, this does present +some more abstract problems. For one, there's no meaningful right identity in +conduit.

Remember that in conduit, upstream has automatic termination, while downstream +does not. This explains why only downstream can provide a return value. +However, if we turn off automatic termination on both sides, we can get values +returned from both upstream and downstream. (Yes, this claim is pretty vague +right now, I'll elaborate fully in my next blog post.)

pipes: Prompt resource finalization

Consider the following simplistic file reading function in conduit:

readFile :: FilePath -> Source (ResourceT IO) String
+readFile file = bracketP
+    (do h <- IO.openFile file IO.ReadMode
+        putStrLn $ "{" ++ file ++ " open}"
+        return h )
+    (\h -> do
+        IO.hClose h
+        putStrLn $ "{" ++ file ++ " closed}" )
+    fromHandle
+  where
+    fromHandle h = forever $ liftIO (IO.hGetLine h) >>= yield
+
+main :: IO ()
+main = runResourceT $ producer $$ CL.mapM_ (liftIO . putStrLn)
+
+producer = do
+    readFile "input.txt" $= CL.isolate 4
+    liftIO $ putStrLn "Some long running computation"

It uses the bracketP combinator, which uses ResourceT to ensure exception +safety, while using conduit's built-in deterministic resource handling to +ensure prompt finalization. Our data producer streams four lines of data from +the file, and then runs some (theoretically) long-running computation to +generate some more output to be placed in the same output stream. Running this +program gives fairly expected results:

{input.txt open}
+line 1
+line 2
+line 3
+line 4
+{input.txt closed}
+Some long running computation

As we would hope, the input file is opened and closed before the long running +computation even starts. Now let's look at the same code in pipes. This example +is taken from the pipes-safe documentation, modified slightly to include this +long running computation concept:

readFile :: FilePath -> Producer' String (SafeT IO) ()
+readFile file = bracket
+    (do h <- IO.openFile file IO.ReadMode
+        putStrLn $ "{" ++ file ++ " open}"
+        return h )
+    (\h -> do
+        IO.hClose h
+        putStrLn $ "{" ++ file ++ " closed}" )
+    P.fromHandle
+
+main :: IO ()
+main = do
+    runSafeT $ runEffect $ producer >-> P.stdoutLn
+
+producer = do
+    readFile "input.txt" >-> P.take 4
+    liftIO $ putStrLn "Some long running computation"

pipes-safe provides a bracket function, very similar to conduit's bracketP. +It also provides SafeT, which is strikingly similar to ResourceT. Besides +minor differences in operators and functions names, this code is basically +identical. So running it should produce the same output, right?

{input.txt open}
+line 1
+line 2
+line 3
+line 4
+Some long running computation
+{input.txt closed}

That's a bit worrisome. The input file is kept open during the entire long +running computation! This problem is identified in the pipes-safe release +announcement +from January.

The reason pipes is not able to guarantee prompt finalization is that the data +producer is never given a chance to perform its own cleanup. In the line:

readFile "input.txt" >-> P.take 4

Assuming input.txt has more than four characters, the call to P.fromHandle in +readFile will never exit. Instead, processing will halt as soon as take 4 +returns. I consider this behavior to actually be a bug: the bracket function +has distinctly different semantics than Control.Exception.bracket, and scarce +resources will be kept open for an indefinitely long time (until the SafeT +block is exited).

conduit: lack of assocativity

conduit doesn't get away free here either. conduit also doesn't allow the +upstream to continue processing after downstream completes. Instead, it adds a +new concept: a finalizer function can be yielded with each value. However, +this implementation approach doesn't allow for deterministic ordering of +finalizers. This bug was originally identified by Dan +Burton. +However, by getting rid of early termination in producers, we can solve this +problem and take back full associativity.

pipes: Chunking and leftovers

The other major feature that conduit bakes into the core which pipes does not +is leftovers. Leftover support is necessary for a few different things, but the +need is most apparent when dealing with chunked data structures like +ByteString and Text. Consider a program which will write the first 20 bytes +from the file "input.txt" to the file "output1.txt", and the second 20 bytes to +"output2.txt". This is trivial in conduit:

main :: IO ()
+main = runResourceT $ sourceFile "input.txt" $$ do
+    isolate 20 =$ sinkFile "output1.txt"
+    isolate 20 =$ sinkFile "output2.txt"

With an input.txt of:

hellohellohellohelloworldworldworldworldbyebyebyebye

output1.txt ends up with:

hellohellohellohello

and output2.txt is:

worldworldworldworld

(You can inflate the number 20 to something much larger to make the need for +streaming data more realistic.)

Let's imagine that the first chunk of data that is read from input.txt is 60 +bytes large. The first call to isolate will read that chunk, split it into 20 +and 40 byte chunks, send the 20 byte piece off to output1.txt, and return the +remaining 40 bytes as leftovers. The second call to isolate can then read +that chunk in and repeat the process.

It's hard for me to imagine this being much more declarative. We're able to +leverage conduit's two forms of composition. Monadic composition allows us to +string together the two consumers to consume successive data from the producer. +And we're able to use fusion to combine the isolate calls with the sinkFile +calls, and to connect the source with the combined sink.

This kind of dual composition has been the hallmark of pipes since its first +release, so certainly building up something similar should be trivial. With the +newly released pipes-bytestring, let's try to naively copy our conduit code +over.

main :: IO ()
+main =
+    withFile "input.txt" ReadMode $ \input ->
+    withFile "output1.txt" WriteMode $ \output1 ->
+    withFile "output2.txt" WriteMode $ \output2 ->
+        runEffect $ fromHandle input >-> do
+            take 20 >-> toHandle output1
+            take 20 >-> toHandle output2

When I run this code, output2.txt is empty! To understand why, let's consider +how isolate works in conduit. We get an initial chunk of (say) 60 bytes, +split off the first 20, and return the remaining 40 as leftovers. But pipes has +no leftover support, so take simply drops the data on the floor! Not only is +this unintuitive behavior, and completely undocumented, but is +non-deterministic: if the first chunk was instead 30 bytes, only 10 bytes of +data would be lost. If it was 100 bytes, 80 bytes would be lost. I'd consider +the very presence of this function to be an inherent flaw in this library that +needs to be rectified immediately.

(By the way, drop is even worse than take. I'll leave it to reader comments +to discover why.)

In order to get the right behavior, you have to use the splitAt function instead:

main :: IO ()
+main =
+    withFile "input.txt" ReadMode $ \input ->
+    withFile "output1.txt" WriteMode $ \output1 ->
+    withFile "output2.txt" WriteMode $ \output2 -> do
+        input' <- runEffect $ splitAt 20 (fromHandle input) >-> toHandle output1
+        void $ runEffect $ splitAt 20 input' >-> toHandle output2

There are a number of points that need to be elucidated here:

  • The do-block is no longer performing any kind of composition of Pipes, but +rather just IO composition. In fact, we've had to completely give up on +"vertical composition" of pipes to make this work.

  • We have to explicitly pass around the producer. I'm familiar with this style +of coding, since early versions of conduit encouraged it, and it's not an +experience I'd want to repeat. It's easy to accidentally pass around the old +producer instead of the new one, for example. And this is exactly the kind of +drudgery that streaming libraries should be able to liberate us from!

This approach to leftovers just inverts the whole concept of a producer to a +pull-based model. This is a valid approach, but it sacrifices so much of the +elegance and simplisity we have in a streaming library, and pushes it to a user +problem. The API is now seemingly doubled, between "Pipes" and "Splitters" as +the API documentation calls them. (This is similar to the issues I raise above +regarding folds.)

While these limitations can be worked +around, +I believe the workarounds defeat so much of the elegance of the declarative +approach pipes claims. conduit keeps that elegance by baking leftovers directly +into the core abstraction.

pipes: Simple parsing

As a further illustration of the problems of lack of proper chunked data +support, consider the following trivial conduit snippet:

parseA :: Monad m => Sink Text m A
+parseC :: Monad m => Sink Text m C
+
+myParse :: Monad m => Sink Text m (A, C)
+myParse = (,) <$> parseA <*> parseC

Since monadic composition works naturally for Sinks- even chunked Sinks- +composing two different Sink can be achieved by using standard Applicative +operators. Such easy composition is not possible (AFAICT) with pipes-parse or +pipes-bytestring.

So my claim is: pipes has simplified its core by leaving out leftover support, +resulting in some really complicated user-facing APIs. conduit includes the +complexity in one place, the core, and the rest of the codebase reaps the +benefits.

conduit: Lack of identity in presence of leftovers

conduit solves leftovers by baking it into the core abstraction as a separate +concept. This has been criticized by Gabriel and others (rightfully so) in that +it makes the core harder to reason about. The manner in which this issue +manifests is that identity does not preserve leftovers. In other words, +idConduit =$= leftover x /= leftover x.

At this point, you're probably wondering: I get the problems with leftovers, +how does this indict automatic termination as the cause? I'll have to be a bit +vague until my next post, but the basic idea is that there's an incredibly easy +way to implement leftovers: each time a component completes, it returns both +its return value and its leftovers. When this component is monadically composed +with another component, the leftovers are supplied as input to that new +component. And when composed via fusion (a.k.a., vertical composition), the +leftovers are provided as part of the result.

pipes and conduit: isolate

I don't think the iteratee approach gets nearly enough credit; in some cases, +we're still not completely caught up. Take for example the isolate +function, +which has the following description:

isolate n reads at most n elements from the stream, and passes them to its iteratee. If the iteratee finishes early, elements continue to be consumed from the outer stream until n have been consumed.

This kind of function could be incredibly useful for something like consuming +an HTTP request body. A web server will determine the length of the request +body from the content-length header, and then stream that body to the +application. If the application doesn't consume the entire body, isolate can +ensure that the rest of the input is flushed, so that the next request is +available for the webserver to continue processing.

A simpler example of this would be a function to consume lines. Consider the +following approach in conduit:

line :: Monad m => Conduit Char m Char
+line = do
+    mc <- await
+    case mc of
+        Nothing -> return ()
+        Just '\n' -> return ()
+        Just c -> yield c >> line

The algorithm is simple: get a character. If there is no character, or it's a +newline, we're done processing. Otherwise, yield the character downstream, and +continue. Let's try to use this function to get the second line of input:

main = do
+    secondLine <- mapM_ yield "Hello\nWorld\n" $$ do
+        line =$ return ()
+        line =$ CL.consume
+    putStrLn secondLine

We'd expect the output to be World, but unfortunately it's not. The actual +output is Hello. The reason is that the Sink attached to the first call to +line does not consume any of the input provided by line. As a result, it +terminates immediately, and therefore line also terminates immediately, since +producers automatically terminate. In fact, line is never called here at all!

One workaround is to provide a modified line that takes a Sink as its first +argument, e.g.:

-- This is the same as the previous line
+lineHelper :: Monad m => Conduit Char m Char
+
+line :: Monad m
+     => Sink Char m a
+     -> Sink Char m a
+line sink = lineHelper =$ do
+    result <- sink
+    CL.sinkNull -- discard the rest of the line
+    return result

Then, instead of using fusion to combine line with our sinks, we just pass +them as arguments, e.g.:

main = do
+    secondLine <- mapM_ yield "Hello\nWorld\n" $$ do
+        line $ return () -- note: replaced =$ with $
+        line $ CL.consume
+    putStrLn secondLine

While this works, it's not ideal. Like the pipes solutions to folding and +leftovers, we're left with two different and conflicting approaches which don't +compose with each other.

Sneek preview

To give a bit of a sneak peek for the next post, let's consider what an ideal +version of line may look like. It would need to be able to continue consuming +input after calling yield. We may even call that something like tryYield, +and allow yield to maintain its current auto-termination behavior. This would +look like:

line :: Monad m => Conduit Char m Char
+line = do
+    mc <- await
+    case mc of
+        Nothing -> return ()
+        Just '\n' -> return ()
+        Just c -> tryYield c >> line

We're still left with a question. The current behavior of conduit would mean +that no input is consumed if downstream is already closed. With the +hypothetical line function I just wrote, one character will be consumed +before tryYield is ever called. Is there any way to perfectly model the +previous behavior and ensure no actions are performed if downstream is closed? +I'll let you know in the next blog post.

Conclusion

I want to be clear: the pipes design is very elegant. Some of the issues I've +listed above can be worked around. If you're OK with having to use a separate +set of functions for writing folds, for example, the approach will work. +However, there are other cases- like prompt resource finalization- for which +there does not appear to be a readily available workaround. If you don't have +need of prompt resource finalization, then this limitation may not bother you. +For other cases, it could be a deal-breaker.

On the conduit side, we're looking at three identified flaws: associativity +affecting finalizers ordering, guaranteed emptying when using a Conduit, and +identity regarding leftovers. These would all be nice to fix, but at the same +time none of them are major issues. So the question is: would making this kind +of a change be worth it?

Before making any decisions, I think it's worth analyzing the new design. I +think at the very least it will give us new insights into our existing +approaches, and maximally may let us drastically improve our streaming +libraries.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/10/pipes-resource-problems.html b/public/blog/2013/10/pipes-resource-problems.html new file mode 100644 index 00000000..39d9d4f8 --- /dev/null +++ b/public/blog/2013/10/pipes-resource-problems.html @@ -0,0 +1,273 @@ + pipes resource problems +

pipes resource problems

October 7, 2013

GravatarBy Michael Snoyman

I promised a blog post elaborating on my concerns with the pipes 4.0 release. +What you're reading now is not that blog post; this is an introduction to it. +Right now, I'm trying to motivate the fact that there's a serious problem. I've +been having a private conversation with Gabriel about this specific concern, +and I don't believe my concerns have made much of an impact. So I'm raising +them here. A quick summary is:

  • pipes has known cases in which it will not clean up resources promptly.
  • Worse yet, the cleanup behavior is in many cases unreliable, in non-obvious ways.
  • The problem can easily lead to programs crashing.

The crashing program

Let me jump right in to a concrete example of that third point, which is IMO +the most troubling. Let's create a directory with lots of files:

{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad             (forM_)
+import           Filesystem                (createTree)
+import           Filesystem.Path.CurrentOS (decodeString, directory,
+                                            encodeString, (</>))
+
+main = do
+    let files = do
+            let pieces = map (decodeString . show) [1..20 :: Int]
+            p1 <- pieces
+            p2 <- pieces
+            p3 <- pieces
+            return $ "input" </> p1 </> p2 </> p3
+    forM_ (zip files [1 :: Int ..]) $ \(fp, num) -> do
+        createTree $ directory fp
+        writeFile (encodeString fp) (show num)

I'd like to write a program to create a clone of this "input" directory, into a +directory called "output". Using filesystem-conduit, this is pretty easy:

{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad.IO.Class
+import           Data.Conduit
+import           Data.Conduit.Filesystem
+import           Filesystem                (createTree)
+import           Filesystem.Path.CurrentOS
+
+main = runResourceT $ traverse False "input" $$ awaitForever (\infile -> do
+    Just suffix <- return $ stripPrefix "input/" infile
+    let outfile = "output" </> suffix
+    liftIO $ createTree $ directory outfile
+    sourceFile infile =$ sinkFile outfile
+    )

traverse creates a stream of all of the files found in the input path, +traversing subdirectories. For each file, we create the output filename, create +the directory for the output file, and then connect sourceFile to sinkFile +to perform the actual copy. Each of these functions guarantees prompt cleanup +of the file handles they hold, and the enclosing runResourceT ensures that +resources will be cleaned up, even in the event of an exception.

Let's translate this code to use pipes-safe, which claims to support prompt +finalization of resources. (Note that I've included an implementation of +traverse here, based on the conduit implementation.)

{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad
+import           Control.Monad.IO.Class
+import           Filesystem                (createTree, isDirectory, isFile,
+                                            listDirectory)
+import           Filesystem.Path.CurrentOS
+import           Pipes
+import qualified Pipes.Prelude             as P
+import           Pipes.Safe
+import           Pipes.Safe.Prelude
+import           Prelude                   hiding (FilePath, readFile,
+                                            writeFile)
+
+main = runSafeT $ runEffect $ traverse "input" >-> forever (await >>= \infile -> do
+    Just suffix <- return $ stripPrefix "input/" infile
+    let outfile = "output" </> suffix
+    liftIO $ createTree $ directory outfile
+    readFile (encodeString infile) >-> writeFile (encodeString outfile)
+    )
+
+traverse :: MonadIO m => FilePath -> Producer FilePath m ()
+traverse root =
+    liftIO (listDirectory root) >>= pull
+  where
+    pull [] = return ()
+    pull (p:ps) = do
+        isFile' <- liftIO $ isFile p
+        if isFile'
+            then yield p >> pull ps
+            else do
+                follow' <- liftIO $ isDirectory p
+                if follow'
+                    then do
+                        ps' <- liftIO $ listDirectory p
+                        pull ps
+                        pull ps'
+                    else pull ps

Go ahead and run that program. You should get output that looks something like:

copy-pipes.hs: input/13/1/12: openFile: resource exhausted (Too many open +files)

The exact file it crashes on may be different, and if you've increased your +ulimits, the program may succeed. But the core problem is that pipes provides +no means of guaranteeing that a resource is cleaned up. What's even more +troubling is that the behavior of pipes is worse than that of lazy I/O. Since +pipes continues to hold on to open file handles after they are no longer +needed, the garbage collector has no chance of helping us. By contrast, the +following lazy I/O version of the program generally runs without crashing:

{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad
+import           Control.Monad.IO.Class
+import           Filesystem                (createTree, isDirectory, isFile,
+                                            listDirectory)
+import           Filesystem.Path.CurrentOS
+import           Prelude                   hiding (FilePath)
+
+main = traverse "input" $ \infile -> do
+    Just suffix <- return $ stripPrefix "input/" infile
+    let outfile = "output" </> suffix
+    createTree $ directory outfile
+    readFile (encodeString infile) >>= writeFile (encodeString outfile)
+
+traverse :: MonadIO m => FilePath -> (FilePath -> m a) -> m ()
+traverse root f =
+    liftIO (listDirectory root) >>= pull
+  where
+    pull [] = return ()
+    pull (p:ps) = do
+        isFile' <- liftIO $ isFile p
+        if isFile'
+            then f p >> pull ps
+            else do
+                follow' <- liftIO $ isDirectory p
+                if follow'
+                    then do
+                        ps' <- liftIO $ listDirectory p
+                        pull ps
+                        pull ps'
+                    else pull ps

Why this is a problem

For many of us, the primary goal of a streaming data library is to provide for +deterministic resource handling. While not the only issue with lazy I/O, its +non-determinism was high on the list. The issue is that, based on various +environmental factors, cleanup of resources could be delayed until the next +garbage collection, whose timing cannot be guaranteed.

There are three different aspects to resource handling in a streaming library:

  • On demand acquisition (a.k.a., laziness).
  • Prompt finalization.
  • Exception safety.

One of the beauties of the iteratee pattern is that it allows for all three of +these to be addressed on the data producer side. However, exception safety +cannot be guaranteed on the data consumer side. When I started work on conduit, +I wanted to ensure that both the producer and consumer could reliably allocate +resources on demand in an exception safe manner. This pattern is allowed via +the resourcet package. conduit itself then provides on demand acquisition and +prompt finalization.

pipes-safe includes a SafeT transformer which is almost identical to ResourceT. +And this transformer guarantees that, short of a program crash, a cleanup +action is always called. However, just like ResourceT, it can give no +guarantees about promptness. I'll get into the details of why in my next blog +post, but pipes is unable to guarantee that it will run code at a specific +point.

Let's look at one of the examples from the pipes-safe docs:

runSafeT $ runEffect $ readFile "readFile.hs" >-> P.take 4 >-> P.stdoutLn

Running this will in fact promptly close readFile.hs. But that's just due to +the small nature of this example. What actually happens is that readFile +opens the file, after reading four lines, the pipeline terminates, and then +runSafeT closes the file. This reliance on SafeT to do normal resource +finalization is the problem. The first example I gave demonstrates that in +simple real-world examples, there may be many operations between resource +acquisition and exiting SafeT.

It's true that the example I started off with could be rewritten to embed the +SafeT block inside, and run two separate pipelines instead of one. That's +certainly true, but that's only because of the simplicity of the example. A +more difficult example to work around would be one where the data source needs +to be shared amongst the consumers, and therefore you can't get away with +running multiple pipelines. The following example in conduit opens an input +file and splits it into 50 byte chunks:

import Data.Conduit
+import Data.Conduit.List (peek)
+import Data.Conduit.Binary
+
+main =
+    runResourceT $ sourceFile "input.dat" $$ loop 0
+  where
+    loop i = do
+        mx <- peek
+        case mx of
+            Nothing -> return ()
+            Just _ -> do
+                let fp = "out-conduit/" ++ show i
+                isolate 50 =$ sinkFile fp
+                loop $ i + 1

Given a large enough input file (I used /usr/share/dict/words), the following +pipes version will crash:

import Pipes
+import qualified Pipes.Prelude as P
+import Pipes.Safe
+import qualified Pipes.Safe.Prelude as P
+
+main =
+    runSafeT $ runEffect $ P.readFile "input.dat" >-> loop 0
+  where
+    loop i = do
+        let fp = "out-pipes/" ++ show i
+        P.take 50 >-> P.writeFile fp
+        loop $ i + 1

Note that, due to differences in sourceFile in conduit and readFile in +pipes-safe, these programs are doing slightly different things: conduit deals +with 50 byte chunks, while pipes is dealing with 50 line chunks. This +distinction is irrelevant for the current discussion, I'm just trying to keep +the examples concise by using functions built into the libraries.

Ignoring any issues of which programs can or cannot be rewritten to work with +pipes, the more glaring issue is that pipes makes it easy and natural to write +programs which have very detrimental behavior regarding resources.

UPDATE: There's a more sophisticated example the better demonstrates the +problem at the end of this blog post.

It's unreliable

One last point is that this behavior is unreliable. Consider this example again:

runSafeT $ runEffect $ readFile "readFile.hs" >-> P.take 4 >-> P.stdoutLn

Above, I claimed that readFile would not end up closing the file handle. This +wasn't strictly accurate. If the file contains less than four lines, +readFile will close the file handle. This isn't quite non-deterministic +behavior, since we can clearly state how the program will behave on different +inputs. However, it's pretty close: depending on the size of a file, the +program will either do the right or the wrong thing, and we have no way to +restructure our program to change this.

Given the fact that, for many of us, the entire attraction of getting away from +lazy I/O is to take back deterministic, prompt resource management, this +behavior is unsettling. What concerns me even more is that the pipes libraries +claim to support proper behavior, but on basic analysis clearly don't.

In our conversations, Gabriel told me that pipes is about much more than just a +lazy I/O replacement. I have no objection to that, and pipes can and should +continue to research those directions. But in its current form, pipes is not +performing the bare minimum functionality to be considered a replacement for +iteratees or conduit.

Again, my point in this blog post is simply to establish the fact that there's +a problem. I'll get into more details about the cause in the following blog +post, and a solution to that problem in the one after that.

Update: a more complicated problem

After publishing this blog post, there was a discussion on Reddit which pointed +out the presence of withFile in pipes-safe, of which I was previously +unaware. That allows the two examples I gave above to be implemented, but +doesn't actually solve the core problem that finalizers within a pipeline +cannot be run promptly. Here's a more complicated example to demonstrate this.

The following snippet of conduit code loops over all of the files in an input +folder, and spits each 50-byte chunk into a separate file in the output folder. +At no point is more than one file handle open for reading and one for writing.

{-# LANGUAGE OverloadedStrings #-}
+import Data.Conduit
+import Data.Conduit.List (peek)
+import Data.Conduit.Filesystem
+import Data.Conduit.Binary (isolate)
+import Data.String (fromString)
+
+main =
+    runResourceT $ src $$ loop 0
+  where
+    src = traverse False "input" $= awaitForever sourceFile
+    loop i = do
+        mx <- peek
+        case mx of
+            Nothing -> return ()
+            Just _ -> do
+                let fp = "out-conduit/" ++ show i
+                isolate 50 =$ sinkFile (fromString fp)
+                loop $ i + 1

To me, this demonstrates the beauty of composition that conduit provides. Our +src above never has to be structured in such a way to deal with resource +allocation or finalization; sourceFile automatically handles it correctly.

I'm not aware of any solution to this problem in pipes.

Update 2: Gabriel has provided a solution for this in pipes:

{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Monad
+import Data.DirStream
+import Pipes
+import Pipes.Safe
+import Pipes.ByteString
+import qualified Pipes.ByteString.Parse as P
+import Pipes.Parse
+
+import qualified Filesystem as F
+import qualified Filesystem.Path.CurrentOS as F
+import System.IO
+
+main =
+    runSafeT $ runEffect $ (`evalStateT` src) (loop 0)
+  where
+    src = for (every (childFileOf "/usr/bin")) readFile'
+    loop i = do
+        eof <- isEndOfBytes
+        unless eof $ do
+            let fp = F.decodeString ("out/" ++ show i)
+            runSafeT $ runEffect $
+                hoist lift (input >-> P.take 50) >-> writeFile' fp
+            loop (i + 1)
+
+-- `childOf` returns all children, including directories.  This is just a quick filter to get only files
+childFileOf :: (MonadSafe m) => F.FilePath -> ListT m F.FilePath
+childFileOf file = do
+    path <- childOf file
+    isDir <- liftIO $ isDirectory path
+    guard (not isDir)
+    return path
+
+-- Work around `FilePath` mismatch.  See comments below
+readFile' :: (MonadSafe m) => F.FilePath -> Producer ByteString m ()
+readFile' file =
+    bracket (liftIO $ F.openFile file ReadMode) (liftIO . hClose) fromHandle
+
+writeFile' :: (MonadSafe m) => F.FilePath -> Consumer ByteString m r
+writeFile' file =
+    bracket (liftIO $ F.openFile file WriteMode) (liftIO . hClose) toHandle

I think this is a good demonstration of the fact that having proper resource +handling in the core is (1) more composable, (2) much safer and (3) far easier +to use.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/10/prelude-replacements-libraries.html b/public/blog/2013/10/prelude-replacements-libraries.html new file mode 100644 index 00000000..4965cfa0 --- /dev/null +++ b/public/blog/2013/10/prelude-replacements-libraries.html @@ -0,0 +1,17 @@ + Prelude replacements are for application developers +

Prelude replacements are for application developers

October 2, 2013

GravatarBy Greg Weber

A previous post announced a new library mono-traversable and an overhaul of a prelude replacement classy-prelude.

Comments on Reddit made me realize that since many Haskellers do not use Prelude replacements many of them are confused about how Prelude replacements are meant to be used.

Library authors vs. Application Developers

This is a concept that I cannot harp on enough.

Application Developers are free to do whatever they want if they aren't sharing code. +This should be apparent that if you are building a binary it does not effect anyone else. +There are open source applications that are trying to share their code, and the more important this is for the application, the more it will become similar to a library. However, most Haskell application developers are mostly attempting to execute or distribute a working program, not share code.

Library authors are the ones with the constraints. Anything that is part of the API or dependency set will affect users of the library. Even code that is not a direct part of the API can affect a user's ability to read and understand the library code.

Lets keep this in mind as we discuss Prelude replacements.

Prelude replacements are for application developers

There is no need to view prelude replacements as some sort of a threat to the Prelude or as a source of fragmentation. In fact the opposite is the case: prelude replacements are an important tool to help the community standardize on continuing to use the Haskell Prelude.

Rather than requiring full community buy-in or fragmentation, prelude replacements work best with partial buy-in. It is a false assumption that prelude replacements cause fragmentation. Prelude replacements are best for application developers that are not distributing libraries. Most non-trivial application developers already end up creating their own modified Prelude, they just are not sharing it as a library with everyone else.

It is an entirely different story for library authors. The goal of a library author is to take as much pain as possible away from their users and put it into the library. They should be very cautious about using prelude replacements (or any other non-standard form of convenience). It adds a dependency, can make their code harder for users to read, and may leak non-standard functions or types. If a prelude replacement is required, consider something with as few changes as possible, such as basic-prelude, or consider building your own from core-prelude. Another possibility is taking just the desired parts from prelude replacements. For example, if someone wants the power of the mono-traversable library, they should just use that library directly rather than classy-prelude.

Prelude replacements can greatly help the standard Prelude

So I hope we have established that when used appropriately (judiciously in libraries), prelude replacements cause no harm. Their main goal is to make application developers more productive, but they have the side effect of giving real world information to discussions about improving the Prelude.

classy-prelude will give valuable real-world feedback on potential improvements to the Prelude such as standardising semigroups. I was going to open a libraries discussion issue about adding an Eq constraint to groupBy, but I would have had to spend a lot of time discussing the issue on a mail list. Instead we were able to very quickly make the change to classy-prelude (actually we as calling the function groupOn), and we can gather experience over the coming months from actually using it.

A few of the improvements in classy-prelude will be adopted in the Prelude, and most will be rejected. This process helps keep the Prelude informed of potential options and stay up to date.

How library authors should use new libraries

mono-traversable is a new library. It is very useful for writing code that can operate over different monomorphic containers, but the library itself may make drastic changes or someone may come up with a better library. It has not been vetted by community usage yet.

mono-traversable introduces some new typeclasses. MonoFoldable fully generalize Foldable, so it is possible to just export Mono versions of functions. However, for library authors, if you want users to use your functions on polymorphic structures, I would advise also exporting the normal polymorphic Foldable version of your function in addition to the MonoFoldable version and letting users decide which to use them. Do feel free to create Mono instances of the data types you define though.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/10/segfaults.html b/public/blog/2013/10/segfaults.html new file mode 100644 index 00000000..d60a6eb0 --- /dev/null +++ b/public/blog/2013/10/segfaults.html @@ -0,0 +1,29 @@ + A warning about segfaults +

A warning about segfaults

October 4, 2013

GravatarBy Michael Snoyman

This is just a word of warning for Yesod users. There appears to be a serious +bug in GHC 7.4.2 which can be triggered when using Persistent. We've +experienced this quite a few times at FP Complete, but have never been able to +get a minimal reproducing test case. Recently an issue was filed with the same +behavior, and John Wiegley +recommended I write a blog post to warn users.

The problem comes up when using the get404 function. It seems that under some +circumstances, this function will return an invalid value if the given key does +not exist. If this value is then used, it results in a segfault of the process. +If this situation arises, you can work around the problem by using get and +case statements. Please see my comments on the Github +issue for +more details.

This bug does not seem to exist on the GHC 7.6 series, so upgrading GHC +versions may be the simplest way to prevent this problem.

If anyone has minimal examples of this occurring, please email me, I'd love to +include a test case for this in either the Persistent or Yesod test suite to +ensure we're protected against such a regression in GHC.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/10/simpler-conduit-core.html b/public/blog/2013/10/simpler-conduit-core.html new file mode 100644 index 00000000..85302919 --- /dev/null +++ b/public/blog/2013/10/simpler-conduit-core.html @@ -0,0 +1,825 @@ + Simpler conduit core +

Simpler conduit core

October 10, 2013

GravatarBy Michael Snoyman

NOTE I strongly recommend reading this post on School of +Haskell. +It makes extensive usage of active code.

In my last blog post, I made the case that automatic termination is the cause +of a lot of problems in both pipes and conduit. In this blog post, I'd like to:

  1. Derive a simple core datatype that does not have automatic termination.

  2. Discuss the desired behavior of this datatype.

  3. Assess what convenience functionality we lost along the way, and see if we +can get it back with an added layer on top of the core.

Note that this blog post is meant to demonstrate an idea; it's not to be taken as a working implementation. There are many details that still need to be worked out. I'm hoping that presenting my thoughts in this manner will give enough of a basis for an informed discussion.

Deriving the core datatype

I want to start as simple as possible. Let's begin with the core datatype from +pipes 1.0. This leaves out any extra complexity we don't want to deal with in +this process, like leftovers, finalizers, or bidirectionality.

data Pipe i o m r
+    = Pure r
+    | M (m (Pipe i o m r))
+    | Yield (Pipe i o m r) o
+    | Await (i -> Pipe i o m r)

Quick recap: M is for performing monadic actions. Yield passes a value +downstream, and gives up control of the flow of execution to downstream. If +downstream meanwhile returns Pure, the current Pipe will never get control +of execution again. Similarly, Await asks for a value from upstream, and will +similarly terminate of upstream returns Pure.

Let's implement the identity pipe in terms of the raw constructors:

idP :: Monad m => Pipe i i m r
+idP = Await (Yield idP)

We'll also need some kind of function to compose two pipes together. The type for this function is:

fuse :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r

You can experiment with some sample code here:

import Control.Monad
+
+data Pipe i o m r
+    = Pure r
+    | M (m (Pipe i o m r))
+    | Yield (Pipe i o m r) o
+    | Await (i -> Pipe i o m r)
+
+fuse :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r
+fuse _ (Pure r) = Pure r
+fuse up (M m) = M (liftM (fuse up) m)
+fuse up (Yield down o) = Yield (fuse up down) o
+fuse up0 (Await down) =
+    go up0
+  where
+    go (Pure r) = Pure r
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse up (down b)
+    go (Await up) = Await (go . up)
+
+(>->) = fuse
+
+idP :: Monad m => Pipe i i m r
+idP = Await (Yield idP)
+
+consume :: Monad m => Int -> Pipe i o m [i]
+consume =
+    go id
+  where
+    go front 0 = Pure (front [])
+    go front count = Await $ \i -> go (front . (i:)) (count - 1)
+
+yieldMany :: Monad m => [o] -> Pipe i o m r
+yieldMany [] = error "FIXME"
+yieldMany (o:os) = Yield (yieldMany os) o
+
+runPipe :: Monad m => Pipe () () m r -> m r
+runPipe (Pure r) = return r
+runPipe (M m) = m >>= runPipe
+runPipe (Await f) = runPipe (f ())
+runPipe (Yield f ()) = runPipe f
+
+main = runPipe (yieldMany [1..] >-> idP >-> consume 10) >>= print

In this blog post, I want to modify the core datatype to allow for +non-termination. We'll start with the Await side of the equation, and allow +the identity pipe guide our design for the rest of the other constructors. +(Note: we're going to target simplicity here, not efficiency. Efficiency can be +addressed another time.)

Await and Yield: add Maybe

The simplest way to allow for non-termination is to modify Await to include a +Maybe wrapper:

Await (Maybe i -> Pipe i o m r)

This means that, when awaiting for a response from upstream, we can be informed +via Nothing that no values are available. However, our idP no longer +compiles. That's because we're getting a Maybe i, but Yield expects an i. +Let's fix this by modifying our Yield constructor also:

Yield (Pipe i o m r) (Maybe o)

Now our original idP continues to compile. This change now allows us to write a fold, e.g.:

fold :: Monad m => (r -> i -> r) -> r -> Pipe i o m r
+fold f =
+    loop
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure r

Finalization

However, we still haven't actually fixed the non-termination problem. As soon as one Pipe terminates, the whole Pipe terminates. The practical issue is that prompt termination is still not achieved. Try out the following example:

import Control.Monad
+
+data Pipe i o m r
+    = Pure r
+    | M (m (Pipe i o m r))
+    | Yield (Pipe i o m r) (Maybe o)
+    | Await (Maybe i -> Pipe i o m r)
+
+fuse :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r
+fuse _ (Pure r) = Pure r
+fuse up (M m) = M (liftM (fuse up) m)
+fuse up (Yield down o) = Yield (fuse up down) o
+fuse up0 (Await down) =
+    go up0
+  where
+    go (Pure r) = Pure r
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse up (down b)
+    go (Await up) = Await (go . up)
+
+(>->) :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r
+(>->) = fuse
+
+idP :: Monad m => Pipe i i m r
+idP = Await (Yield idP)
+
+consume :: Monad m => Int -> Pipe i o m [i]
+consume =
+    go id
+  where
+    go front 0 = Pure (front [])
+    go front count = Await $ \mi ->
+        case mi of
+            Just i -> go (front . (i:)) (count - 1)
+            Nothing -> Pure (front [])
+-- show Data producer with finalization
+yieldMany :: [o] -> Pipe i o IO r
+yieldMany [] = M (putStrLn "Finalization" >> return (Yield (yieldMany []) Nothing))
+yieldMany (o:os) = Yield (yieldMany os) (Just o)
+-- /show
+
+fold :: Monad m => (r -> i -> r) -> r -> Pipe i o m r
+fold f =
+    loop
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure r
+
+runPipe :: Monad m => Pipe () () m r -> m r
+runPipe (Pure r) = return r
+runPipe (M m) = m >>= runPipe
+runPipe (Await f) = runPipe (f Nothing)
+runPipe (Yield f _) = runPipe f
+
+-- show When does the finalizer get called?
+main = do
+    runPipe (yieldMany [1..10] >-> idP >-> fold (+) 0) >>= print
+    runPipe (yieldMany [1..10] >-> idP >-> consume 5) >>= print
+-- /show

If the input stream it fully consumed, then Finalization is printed. +Otherwise, it's not. We need to change our semantics so that we don't exit until all Pipes exit.

Downstream is already notified when upstream is done producing data, via Nothing getting passed with Yield. We need a similar mechanism on the upstream side. In this case, we want it to be a Maybe result value. This value needs to be present as soon as the Pipe begins execution. To allow for this, we're going to refactor our type a bit as follows:

data Step i o m r
+    = Pure r
+    | M (m (Step i o m r))
+    | Yield (Pipe i o m r) (Maybe o)
+    | Await (Maybe i -> Step i o m r)
+
+type Pipe i o m r = Maybe r -> Step i o m r

Now a Pipe is notified if downstream has already completed execution. I'd like to focus on one important distinction in the Step type's constructors: whereas M and Await represent the next thing to be done via a Step value, Yield represents it with a Pipe value. The reason for this distinction is that, when you call Yield, downstream has a chance to continue processing, and may provide a return value if it hadn't provided one previously. In the M and Await cases, downstream never gets a chance to provide a new result value.

Let's think about the identity pipe. Its semantics should be that, if downstream is done processing, it's also done processing. If downstream is not done processing, it should await for a new value from upstream and yield it downstream. This turns out to be pretty easy to implement:

idP :: Monad m => Pipe i i m r
+idP Nothing = Await (Yield idP)
+idP (Just r) = Pure r

Below is our full running example.

import Control.Monad
+
+data Step i o m r
+    = Pure r
+    | M (m (Step i o m r))
+    | Yield (Pipe i o m r) (Maybe o)
+    | Await (Maybe i -> Step i o m r)
+
+type Pipe i o m r = Maybe r -> Step i o m r
+
+-- show Fusion is a bit more complicated now
+fuse :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r
+fuse up down mr = fuse' up (down mr)
+
+fuse' :: Monad m
+      => (Maybe r -> Step a b m r)
+      -> Step b c m r
+      -> Step a c m r
+fuse' up0 (Pure r) =
+    go $ up0 $ Just r
+  where
+    go (Pure r') = Pure r'
+    go (M m) = M (liftM go m)
+    go (Yield up _) = go $ up $ Just r
+    go (Await up) = Await $ \ma -> go $ up ma
+fuse' up (M m) = M (liftM (fuse' up) m)
+fuse' up (Yield down o) = Yield (fuse up down) o
+fuse' up0 (Await down) =
+    go $ up0 Nothing
+  where
+    -- It's easy to get this next clause wrong.
+    -- We need to make sure that we give downstream
+    -- a chance to finish processing, not just terminate
+    -- immediately.
+    go (Pure r) = fuse' (\_ -> Pure r) (down Nothing)
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse' up (down b)
+    go (Await up) = Await (go . up)
+-- /show
+
+(>->) :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r
+(>->) = fuse
+
+idP :: Monad m => Pipe i i m r
+idP Nothing = Await (Yield idP)
+idP (Just r) = Pure r
+
+-- show Consumers can just ignore downstream results
+consume :: Monad m => Int -> Pipe i o m [i]
+consume count0 _ =
+    go id count0
+  where
+    go front 0 = Pure (front [])
+    go front count = Await $ \mi ->
+        case mi of
+            Just i -> go (front . (i:)) (count - 1)
+            Nothing -> Pure (front [])
+-- /show
+
+-- show Finalization
+-- Note that we only finalize once downstream completes.
+-- We could instead finalize as soon as our input is empty,
+-- which would allow for more promptness. Try implementing that
+-- change. Make sure that the finalizer only gets called once!
+yieldMany :: [o] -> Pipe i o IO r
+yieldMany _ (Just r) = M (putStrLn "Finalization" >> return (Pure r))
+yieldMany [] Nothing = Yield (yieldMany []) Nothing
+yieldMany (o:os) Nothing = Yield (yieldMany os) (Just o)
+-- /show
+
+fold :: Monad m => (r -> i -> r) -> r -> Pipe i o m r
+fold f r0 _ =
+    loop r0
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure r
+
+runPipe :: Monad m => Pipe () () m r -> m r
+runPipe f = runStep (f Nothing)
+
+runStep :: Monad m => Step () () m r -> m r
+runStep (Pure r) = return r
+runStep (M m) = m >>= runStep
+runStep (Await f) = runStep (f Nothing)
+runStep (Yield f _) = runPipe f
+
+main = do
+    runPipe (yieldMany [1..10] >-> idP >-> fold (+) 0) >>= print
+    runPipe (yieldMany [1..10] >-> idP >-> consume 5) >>= print

That's it, we've moved from automatic termination to manual termination. We can now fold, get prompt finalization, and even modify result values from downstream. Let's play with a few more changes.

Draining upstream

Consider writing a Pipe that takes precisely 5 values from upstream and passes them downstream. If downstream finishes early, it still takes those 5 values. This is not currently possible in pipes or conduit. Let's try it out in our new framework:

import Control.Monad
+
+data Step i o m r
+    = Pure r
+    | M (m (Step i o m r))
+    | Yield (Pipe i o m r) (Maybe o)
+    | Await (Maybe i -> Step i o m r)
+
+type Pipe i o m r = Maybe r -> Step i o m r
+
+fuse :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r
+fuse up down mr = fuse' up (down mr)
+
+fuse' :: Monad m
+      => (Maybe r -> Step a b m r)
+      -> Step b c m r
+      -> Step a c m r
+fuse' up0 (Pure r) =
+    go $ up0 $ Just r
+  where
+    go (Pure r') = Pure r'
+    go (M m) = M (liftM go m)
+    go (Yield up _) = go $ up $ Just r
+    go (Await up) = Await $ \ma -> go $ up ma
+fuse' up (M m) = M (liftM (fuse' up) m)
+fuse' up (Yield down o) = Yield (fuse up down) o
+fuse' up0 (Await down) =
+    go $ up0 Nothing
+  where
+    go (Pure r) = fuse' (\_ -> Pure r) (down Nothing)
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse' up (down b)
+    go (Await up) = Await (go . up)
+
+(>->) :: Monad m
+     => Pipe a b m r
+     -> Pipe b c m r
+     -> Pipe a c m r
+(>->) = fuse
+
+idP :: Monad m => Pipe i i m r
+idP Nothing = Await (Yield idP)
+idP (Just r) = Pure r
+
+consume :: Monad m => Int -> Pipe i o m [i]
+consume count0 _ =
+    go id count0
+  where
+    go front 0 = Pure (front [])
+    go front count = Await $ \mi ->
+        case mi of
+            Just i -> go (front . (i:)) (count - 1)
+            Nothing -> Pure (front [])
+
+fold :: Monad m => (r -> i -> r) -> r -> Pipe i o m r
+fold f r0 _ =
+    loop r0
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure r
+
+runPipe :: Monad m => Pipe () () m r -> m r
+runPipe f = runStep (f Nothing)
+
+-- show
+takeExactly :: Monad m => Int -> Pipe i i m r
+takeExactly 0 (Just r) = Pure r
+takeExactly 0 Nothing = Yield (takeExactly 0) Nothing
+takeExactly count _ = Await $ \mi -> Yield (takeExactly (count - 1)) mi
+-- We can optimize the above a bit by skipping
+-- extra Awaits, give it a shot.
+
+yieldMany :: Show o => [o] -> Pipe i o IO r
+yieldMany rest (Just r) = M $ do
+    putStrLn $ "Finalization: " ++ show rest
+    return (Pure r)
+yieldMany [] Nothing = Yield (yieldMany []) Nothing
+yieldMany (o:os) Nothing = Yield (yieldMany os) (Just o)
+
+main = runPipe (yieldMany [1..10] >-> takeExactly 5 >-> fold (+) 0) >>= print
+-- /show
+
+runStep :: Monad m => Step () () m r -> m r
+runStep (Pure r) = return r
+runStep (M m) = m >>= runStep
+runStep (Await f) = runStep (f Nothing)
+runStep (Yield f _) = runPipe f
+

Downstream result type

There's a nice generalization we can provide here. There's no reason that the upstream and downstream result types need to be the same. In fact, if we just add an extra type parameter (d for downstream result), the rest of our code can remain the same. Here's an example:

import Control.Monad
+
+-- show
+data Step i o d m r
+    = Pure r
+    | M (m (Step i o d m r))
+    | Yield (Pipe i o d m r) (Maybe o)
+    | Await (Maybe i -> Step i o d m r)
+
+type Pipe i o d m r = Maybe d -> Step i o d m r
+
+-- Notice the way that a->b->c flows downstream.
+-- On the other hand, the result values x->y->z
+-- "bubble up" from downstream.
+fuse :: Monad m
+     => Pipe a b y m z
+     -> Pipe b c x m y
+     -> Pipe a c x m z
+-- /show
+fuse up down mr = fuse' up (down mr)
+
+fuse' :: Monad m
+      => (Maybe y -> Step a b y m z)
+      -> Step b c x m y
+      -> Step a c x m z
+fuse' up0 (Pure r) =
+    go $ up0 $ Just r
+  where
+    go (Pure r') = Pure r'
+    go (M m) = M (liftM go m)
+    go (Yield up _) = go $ up $ Just r
+    go (Await up) = Await $ \ma -> go $ up ma
+fuse' up (M m) = M (liftM (fuse' up) m)
+fuse' up (Yield down o) = Yield (fuse up down) o
+fuse' up0 (Await down) =
+    go $ up0 Nothing
+  where
+    go (Pure r) = fuse' (\_ -> Pure r) (down Nothing)
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse' up (down b)
+    go (Await up) = Await (go . up)
+
+(>->) :: Monad m
+     => Pipe a b y m z
+     -> Pipe b c x m y
+     -> Pipe a c x m z
+(>->) = fuse
+
+-- show
+-- Identity keeps the same stream value (i) and result
+-- value (r) for both upstream and downstream.
+idP :: Monad m => Pipe i i r m r
+idP Nothing = Await (Yield idP)
+idP (Just r) = Pure r
+-- /show
+
+-- show
+-- Consumers can just ignore the downstream result.
+consume :: Monad m => Int -> Pipe i o d m [i]
+consume count0 _ =
+    go id count0
+  where
+    go front 0 = Pure (front [])
+    go front count = Await $ \mi ->
+        case mi of
+            Just i -> go (front . (i:)) (count - 1)
+            Nothing -> Pure (front [])
+-- /show
+
+fold :: Monad m => (r -> i -> r) -> r -> Pipe i o d m r
+fold f r0 _ =
+    loop r0
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure r
+
+runPipe :: Monad m => Pipe () () d m r -> m r
+runPipe f = runStep (f Nothing)
+
+runStep :: Monad m => Step () () d m r -> m r
+runStep (Pure r) = return r
+runStep (M m) = m >>= runStep
+runStep (Await f) = runStep (f Nothing)
+runStep (Yield f _) = runPipe f
+
+-- show
+-- Producers can simply return the downstream result
+-- as its own result value.
+takeExactly :: Monad m => Int -> Pipe i i r m r
+takeExactly 0 (Just r) = Pure r
+takeExactly 0 Nothing = Yield (takeExactly 0) Nothing
+takeExactly count _ = Await $ \mi -> Yield (takeExactly (count - 1)) mi
+
+yieldMany :: Show o => [o] -> Pipe i o r IO r
+yieldMany rest (Just r) = M $ do
+    putStrLn $ "Finalization: " ++ show rest
+    return (Pure r)
+yieldMany [] Nothing = Yield (yieldMany []) Nothing
+yieldMany (o:os) Nothing = Yield (yieldMany os) (Just o)
+-- /show
+
+main = runPipe (yieldMany [1..10] >-> takeExactly 5 >-> fold (+) 0) >>= print

However, we can also use this functionality to modify the result type. For example, we may want to return any unused values from our input list in yieldMany. This would look like:

import Control.Monad
+
+data Step i o d m r
+    = Pure r
+    | M (m (Step i o d m r))
+    | Yield (Pipe i o d m r) (Maybe o)
+    | Await (Maybe i -> Step i o d m r)
+
+type Pipe i o d m r = Maybe d -> Step i o d m r
+
+fuse :: Monad m
+     => Pipe a b y m z
+     -> Pipe b c x m y
+     -> Pipe a c x m z
+fuse up down mr = fuse' up (down mr)
+
+fuse' :: Monad m
+      => (Maybe y -> Step a b y m z)
+      -> Step b c x m y
+      -> Step a c x m z
+fuse' up0 (Pure r) =
+    go $ up0 $ Just r
+  where
+    go (Pure r') = Pure r'
+    go (M m) = M (liftM go m)
+    go (Yield up _) = go $ up $ Just r
+    go (Await up) = Await $ \ma -> go $ up ma
+fuse' up (M m) = M (liftM (fuse' up) m)
+fuse' up (Yield down o) = Yield (fuse up down) o
+fuse' up0 (Await down) =
+    go $ up0 Nothing
+  where
+    go (Pure r) = fuse' (\_ -> Pure r) (down Nothing)
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse' up (down b)
+    go (Await up) = Await (go . up)
+
+(>->) :: Monad m
+     => Pipe a b y m z
+     -> Pipe b c x m y
+     -> Pipe a c x m z
+(>->) = fuse
+
+idP :: Monad m => Pipe i i r m r
+idP Nothing = Await (Yield idP)
+idP (Just r) = Pure r
+
+consume :: Monad m => Int -> Pipe i o d m [i]
+consume count0 _ =
+    go id count0
+  where
+    go front 0 = Pure (front [])
+    go front count = Await $ \mi ->
+        case mi of
+            Just i -> go (front . (i:)) (count - 1)
+            Nothing -> Pure (front [])
+
+fold :: Monad m => (r -> i -> r) -> r -> Pipe i o d m r
+fold f r0 _ =
+    loop r0
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure r
+
+runPipe :: Monad m => Pipe () () d m r -> m r
+runPipe f = runStep (f Nothing)
+
+runStep :: Monad m => Step () () d m r -> m r
+runStep (Pure r) = return r
+runStep (M m) = m >>= runStep
+runStep (Await f) = runStep (f Nothing)
+runStep (Yield f _) = runPipe f
+
+takeExactly :: Monad m => Int -> Pipe i i r m r
+takeExactly 0 (Just r) = Pure r
+takeExactly 0 Nothing = Yield (takeExactly 0) Nothing
+takeExactly count _ = Await $ \mi -> Yield (takeExactly (count - 1)) mi
+
+-- show
+yieldMany :: Monad m => [o] -> Pipe i o r m ([o], r)
+yieldMany rest (Just r) = Pure (rest, r)
+yieldMany [] Nothing = Yield (yieldMany []) Nothing
+yieldMany (o:os) Nothing = Yield (yieldMany os) (Just o)
+
+main = runPipe (yieldMany [1..10] >-> takeExactly 5 >-> fold (+) 0) >>= print
+-- /show
+-- IGNORED

Leftovers/chunked data

So I promised a solution to leftovers as well. Actually, we can base a solution on that preceding example. When we return a result, we should also return any unconsumed input with it. When we monadically compose two Pipes, we want the leftovers from the first to be injected into the second to be used any time the second awaits. We haven't actually shown a Monad instance yet, and without creating a newtype in place of our type synonym, we can't. But let's play around with some pipeReturn and pipeBind functions:

import Control.Monad
+
+-- show
+data Step i o d m r
+    = Pure [i] r
+    | M (m (Step i o d m r))
+    | Yield (Pipe i o d m r) (Maybe o)
+    | Await (Maybe i -> Step i o d m r)
+
+type Pipe i o d m r = Maybe ([o], d) -> Step i o d m r
+
+pipeReturn :: Monad m => r -> Pipe i o d m r
+pipeReturn r _downRes = Pure [] r
+
+pipeBind :: Monad m
+         => Pipe i o d m a
+         -> (a -> Pipe i o d m b)
+         -> Pipe i o d m b
+pipeBind f g mr =
+    go $ f mr
+  where
+    go (Pure is a) = inject is (g a) mr
+    go (M m) = M (liftM go m)
+    go (Yield p o) = Yield (pipeBind p g) o
+    go (Await f) = Await (go . f)
+
+inject :: Monad m => [i] -> Pipe i o d m r -> Pipe i o d m r
+inject [] p = p
+inject is0 p0 =
+    go is0 . p0
+  where
+    go [] p = p
+    go is (Pure is' r) = Pure (is' ++ is) r
+    go is (M m) = M (liftM (go is) m)
+    go is (Yield p o) = Yield (go is . p) o
+    go (i:is) (Await p) = go is (p $ Just i)
+
+peek :: Monad m => Pipe i o d m (Maybe i)
+peek _ = Await $ \mi ->
+    case mi of
+        Nothing -> Pure [] Nothing
+        Just i -> Pure [i] (Just i)
+
+peekFold :: Monad m => (r -> i -> r) -> r -> Pipe i o d m (Maybe i, r)
+peekFold f r = peek `pipeBind` \a -> fold f r `pipeBind` \b -> pipeReturn (a, b)
+
+main = do
+    runPipe (yieldMany [1..10] >-> peek) >>= print
+    runPipe (yieldMany [1..10] >-> peekFold (+) 0) >>= print
+-- /show
+
+fuse :: Monad m
+     => Pipe a b y m z
+     -> Pipe b c x m y
+     -> Pipe a c x m z
+fuse up down mr = fuse' up (down mr)
+
+fuse' :: Monad m
+      => (Maybe ([b], y) -> Step a b y m z)
+      -> Step b c x m y
+      -> Step a c x m z
+fuse' up0 (Pure cs z) =
+    go $ up0 $ Just (cs, z)
+  where
+    go (Pure bs y) = Pure bs y
+    go (M m) = M (liftM go m)
+    go (Yield up _) = go $ up $ Just ([], z)
+    go (Await up) = Await $ \ma -> go $ up ma
+fuse' up (M m) = M (liftM (fuse' up) m)
+fuse' up (Yield down o) = Yield (fuse up down) o
+fuse' up0 (Await down) =
+    go $ up0 Nothing
+  where
+    go (Pure bs y) = fuse' (\_ -> Pure bs y) (down Nothing)
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse' up (down b)
+    go (Await up) = Await (go . up)
+
+(>->) :: Monad m
+     => Pipe a b y m z
+     -> Pipe b c x m y
+     -> Pipe a c x m z
+(>->) = fuse
+
+idP :: Monad m => Pipe i i r m r
+idP Nothing = Await (Yield idP)
+idP (Just (is, r)) = Pure is r
+
+consume :: Monad m => Int -> Pipe i o d m [i]
+consume count0 _ =
+    go id count0
+  where
+    go front 0 = Pure [] (front [])
+    go front count = Await $ \mi ->
+        case mi of
+            Just i -> go (front . (i:)) (count - 1)
+            Nothing -> Pure [] (front [])
+
+fold :: Monad m => (r -> i -> r) -> r -> Pipe i o d m r
+fold f r0 _ =
+    loop r0
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure [] r
+
+runPipe :: Monad m => Pipe () () d m r -> m r
+runPipe f = runStep (f Nothing)
+
+runStep :: Monad m => Step () () d m r -> m r
+runStep (Pure _ r) = return r
+runStep (M m) = m >>= runStep
+runStep (Await f) = runStep (f Nothing)
+runStep (Yield f _) = runPipe f
+
+takeExactly :: Monad m => Int -> Pipe i i r m r
+-- We specifically ignore leftovers, since we want to ensure
+-- that we consumed exactly the given number of elements
+-- from the stream.
+takeExactly 0 (Just (_, r)) = Pure [] r
+takeExactly 0 Nothing = Yield (takeExactly 0) Nothing
+takeExactly count _ = Await $ \mi -> Yield (takeExactly (count - 1)) mi
+
+yieldMany :: Monad m => [o] -> Pipe i o r m ([o], [o], r)
+yieldMany rest (Just (os, r)) = Pure [] (rest, os, r)
+yieldMany [] Nothing = Yield (yieldMany []) Nothing
+yieldMany (o:os) Nothing = Yield (yieldMany os) (Just o)

Early termination

So we've done all of this work to get rid of early termination. But in practice, it's often very convenient to have early termination. For example, a common pipes and conduit idiom to create a producer from an infinite list is:

mapM_ yield [1..]

Without some kind of early termination, this will never exit. Fortunately, having early exit from a Monad is a solved problem. We could either layer something like MaybeT in our transformer stack, or add a new constructor to our Step datatype. There are a few complications introduced by this, notably that we have an extra type parameter (t for termination) which must be unified with our result type, but the approach works.

import Control.Monad
+-- show
+data Step i o d t m r
+    = Pure [i] r
+    | M (m (Step i o d t m r))
+    | Yield (Pipe i o d t m r) (Maybe o)
+    | Await (Maybe i -> Step i o d t m r)
+    | Stop [i] t
+
+type Pipe i o d t m r = Maybe ([o], d) -> Step i o d t m r
+
+yield :: Monad m => o -> Pipe i o d d m ()
+yield _ (Just (_, d)) = Stop [] d
+yield o Nothing = Yield (pipeReturn ()) (Just o)
+
+stop :: Monad m => Pipe i o d d m r
+stop (Just (_, r)) = Stop [] r
+stop Nothing = Yield stop Nothing
+
+pipeMapM_ :: Monad m
+          => (a -> Pipe i o d t m ())
+          -> [a]
+          -> Pipe i o d t m ()
+pipeMapM_ _ [] = pipeReturn ()
+pipeMapM_ f (x:xs) = f x `pipeBind` (const (pipeMapM_ f xs))
+
+main :: IO ()
+main = do
+    let producer =
+            pipeMapM_ yield [1..] `pipeBind`
+            const stop
+    runPipe (producer >-> takeExactly 10 >-> fold (+) 0)
+        >>= print
+-- /show
+pipeReturn :: Monad m => r -> Pipe i o d t m r
+pipeReturn r _downRes = Pure [] r
+
+pipeBind :: Monad m
+         => Pipe i o d t m a
+         -> (a -> Pipe i o d t m b)
+         -> Pipe i o d t m b
+pipeBind f g mr =
+    go $ f mr
+  where
+    go (Pure is a) = inject is (g a) mr
+    go (M m) = M (liftM go m)
+    go (Yield p o) = Yield (pipeBind p g) o
+    go (Await p) = Await (go . p)
+    go (Stop is t) = Stop is t
+
+inject :: Monad m => [i] -> Pipe i o d t m r -> Pipe i o d t m r
+inject [] p = p
+inject is0 p0 =
+    go is0 . p0
+  where
+    go [] p = p
+    go is (Pure is' r) = Pure (is' ++ is) r
+    go is (M m) = M (liftM (go is) m)
+    go is (Yield p o) = Yield (go is . p) o
+    go (i:is) (Await p) = go is (p $ Just i)
+    go is (Stop is' r) = Stop (is' ++ is) r
+
+peek :: Monad m => Pipe i o d t m (Maybe i)
+peek _ = Await $ \mi ->
+    case mi of
+        Nothing -> Pure [] Nothing
+        Just i -> Pure [i] (Just i)
+
+peekFold :: Monad m => (r -> i -> r) -> r -> Pipe i o d t m (Maybe i, r)
+peekFold f r = peek `pipeBind` \a -> fold f r `pipeBind` \b -> pipeReturn (a, b)
+
+fuse :: Monad m
+     => Pipe a b y z m z
+     -> Pipe b c x y m y
+     -> Pipe a c x z m z
+fuse up down mr = fuse' up (down mr)
+
+fuse' :: Monad m
+      => (Maybe ([b], y) -> Step a b y z m z)
+      -> Step b c x y m y
+      -> Step a c x z m z
+fuse' up0 (Pure cs z) =
+    go $ up0 $ Just (cs, z)
+  where
+    go (Pure bs y) = Pure bs y
+    go (M m) = M (liftM go m)
+    go (Yield up _) = go $ up $ Just ([], z)
+    go (Await up) = Await $ \ma -> go $ up ma
+    go (Stop bs y) = Stop bs y
+fuse' up (M m) = M (liftM (fuse' up) m)
+fuse' up (Yield down o) = Yield (fuse up down) o
+fuse' up0 (Await down) =
+    go $ up0 Nothing
+  where
+    go (Pure bs y) = fuse' (\_ -> Pure bs y) (down Nothing)
+    go (M m) = M (liftM go m)
+    go (Yield up b) = fuse' up (down b)
+    go (Await up) = Await (go . up)
+    go (Stop bs y) = fuse' (\_ -> Stop bs y) (down Nothing)
+fuse' up0 (Stop cs z) = fuse' up0 (Pure cs z)
+
+(>->) :: Monad m
+     => Pipe a b y z m z
+     -> Pipe b c x y m y
+     -> Pipe a c x z m z
+(>->) = fuse
+
+idP :: Monad m => Pipe i i r t m r
+idP Nothing = Await (Yield idP)
+idP (Just (is, r)) = Pure is r
+
+consume :: Monad m => Int -> Pipe i o d t m [i]
+consume count0 _ =
+    go id count0
+  where
+    go front 0 = Pure [] (front [])
+    go front count = Await $ \mi ->
+        case mi of
+            Just i -> go (front . (i:)) (count - 1)
+            Nothing -> Pure [] (front [])
+
+fold :: Monad m => (r -> i -> r) -> r -> Pipe i o d t m r
+fold f r0 _ =
+    loop r0
+  where
+    loop r = Await $ \mi ->
+        case mi of
+            Just i -> loop $! f r i
+            Nothing -> Pure [] r
+
+runPipe :: Monad m => Pipe () () d r m r -> m r
+runPipe f = runStep (f Nothing)
+
+runStep :: Monad m => Step () () d r m r -> m r
+runStep (Pure _ r) = return r
+runStep (M m) = m >>= runStep
+runStep (Await f) = runStep (f Nothing)
+runStep (Yield f _) = runPipe f
+runStep (Stop _ r) = return r
+
+takeExactly :: Monad m => Int -> Pipe i i r t m r
+-- We specifically ignore leftovers, since we want to ensure
+-- that we consumed exactly the given number of elements
+-- from the stream.
+takeExactly 0 (Just (_, r)) = Pure [] r
+takeExactly 0 Nothing = Yield (takeExactly 0) Nothing
+takeExactly count _ = Await $ \mi -> Yield (takeExactly (count - 1)) mi
+
+yieldMany :: Monad m => [o] -> Pipe i o r t m ([o], [o], r)
+yieldMany rest (Just (os, r)) = Pure [] (rest, os, r)
+yieldMany [] Nothing = Yield (yieldMany []) Nothing
+yieldMany (o:os) Nothing = Yield (yieldMany os) (Just o)

Conclusion

The approach I've shown here is not optimized. A faster way of implementing this is to collapse Pipe and Step back into a single data type, and have a dedicated constructor to asking for downstream results. It's also more efficient to skip all of the Maybe wrappers. Instead of Await taking a Maybe i, Await can have two fields: one for when a value is available from upstream, and the other for when no such value is available.

Though it's currently highly experimental, I've implemented these ideas in an experimental branch of conduit. A quick translation from our approach is:

  • Stop is called Terminate.
  • Await has been deconstructed into two fields.
  • Instead of Yield providing a Maybe o, it provides an o. The functionality provided by yielding Nothing is now handled by the Empty constructor.
  • Check is used for checking for downstream results.
  • Yield and Empty are also able to check for downstream results. In the case of Empty, we are guaranteed that downstream must provide a result. In the case of Yield, we include the extra field as an optimization, since composing Yield and Check is otherwise so common.

I've been able to replicate the current conduit API on top of this. The full test suite passes, including some tests for identity and associativity that failed previously. And the benchmarks show this approach as comparable to previous versions.

This approach seems very promising to me, but I also think we need to take time to analyze it properly. I look forward to hearing what the rest of the community thinks.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/11/wai-2-http-client.html b/public/blog/2013/11/wai-2-http-client.html new file mode 100644 index 00000000..80448f74 --- /dev/null +++ b/public/blog/2013/11/wai-2-http-client.html @@ -0,0 +1,51 @@ + WAI 2.0 and http-client +

WAI 2.0 and http-client

November 13, 2013

GravatarBy Michael Snoyman

Kazu and I have been working on some +changes to WAI and Warp for a 2.0 +release. At this point, most of the API changes are done, and could benefit +from some input from the rest of the community. At the same time, the work +we've been doing inspired me to finally write the http-client library, which is +intended to be the new underlying engine for http-conduit in its upcoming 2.0 +release. This blog post is intended to give an overview of the coming changes.

WAI

I originally sent an email to +web-devel about +the planned changes to WAI. For the most part, the changes Kazu and I have +implemented coincide with the conclusions of that thread, namely:

  • The Request constructor has been moved to an Internal module.
  • serverName and serverPort have been removed.
  • requestBody no longer lives in ResourceT IO.
  • As an optimization to avoid extra lookups, requestHeaderHost is now part of Request. We may add other such optimization fields in the future.
  • FilePart now contains the total file size, which is necessary for properly and efficiently supporting range requests.
  • Instead of living in ResourceT, ResponseSource allows for bracket-like semantics to provide for exception safety. This allows for a lot of optimizations in Warp, and leads to some other simplifications which I'll get to later. The responseSourceBracket helper function is a convenient way to get exception safety.

In addition to interface changes, Kazu has made a number of optimizations to +the internals of Warp. A big thank you to Gregory Collins who provided some +recommendations on this front.

I'll hold off on making any comments about performance, as that's Kazu's domain +and he's still adding some optimizations.

http-client

There are a few questions which I've received multiple times about http-conduit:

  • Is it possible to get a slimmed-down version, without SSL support, to be used for testing purposes?
  • Is it possible to use OpenSSL instead of the tls package?
  • If all I need is a way to get a lazy ByteString, can I have a lower-dependency package?

It turns out that the http-conduit code base is pretty well set up for this +kind of change. The concept of a connection has already been abstracted in such +a way that it's trivial to use different backends for SSL connections. And the +internals of the package don't really use conduit very much, rather, the package just +exports a conduit interface for convenience.

So after some hacking, I present to you the http-client +megarepo. It's broken up into five +packages currently:

  • http-client provides the core functionality, without any SSL or conduit support.
  • http-client-tls uses the tls and connection packages to provide SSL support.
  • http-client-openssl uses OpenSSL for that purpose.
  • http-client-multipart provides helper functions for generating multipart request bodies.
  • http-client-conduit provides some helper functions for interacting with conduit.

On top of all this, I've set up a branch of +http-conduit which +provides mostly the same API, but uses http-client for all of its +functionality. Now, some random thoughts:

  • http-client does not use resourcet at all; it uses the bracket idiom instead. This is important for both (1) simplifying the library, and (2) making it easier to integrate with WAI 2.0, which also doesn't use resourcet. http-conduit continues to provide a resourcet-powered interface for ease-of-use.
  • Most functionality provided by http-conduit has been ported to http-client, including:

    • Connection pooling, which is an important optimization when you're regularly connecting to the same host (e.g., API access).
    • Cookies (browser API will continue in http-conduit-browser)
    • Multipart messages
    • Proxies
    • Socks proxies (when using http-client-tls)
    • Basic authentication
  • There's a distinct lack of extensive testing in http-client, since most testing is still being done in http-conduit. I'd certainly like to increase the test coverage in http-client, possibly by simply porting http-conduit test cases over. But for now, the http-conduit test suite is a sufficient means of testing http-client.

I think http-client is at a very nice level of abstraction for people building +higher-level APIs. If you're in such a boat, I'd really like to hear feedback. +I think we're headed towards too much fragmentation in the HTTP client space +right now, and having a shared engine could be a good solution to the problem.

Yesod/http-reverse-proxy/keter

Not only can Yesod be ported over to WAI 2.0 without any API changes, but it +can do so with just a few CPP declarations, so that Yesod will compile both +with the current version of WAI and WAI 2.0. The only real problematic change +is the removal of ResourceT from the WAI protocol. This is handled in Yesod by:

  • Starting a new ResourceT block for each Yesod request.
  • When returning a non-streaming response, closing the ResourceT block when passing the response value to Warp.
  • When returning a streaming response, using the bracket-like functionality of ResponseSource to make sure the ResourceT block is closed.

http-reverse-proxy and keter both end up a bit simpler as a result of this +change. Since neither http-client nor Warp deal with resourcet any more, the +reverse proxy layer can ignore it as well. Basically, we can just peel off a +layer of monad transformers from ever being used.

What's not changing: conduit

In this whole rewrite process, I actually wrote a conduit-free branch of WAI for testing purposes. It worked, but Kazu and I came to the conclusion that:

  1. Removing conduit provided no discernible performance improvement.
  2. Both the internal Warp code, and external user code, became harder to manage without conduit involved.

I also investigated some possible changes to conduit itself, based on last +month's blog post. +For the moment, I'm not pursuing that route further, but I don't want to +relegate that discussion to a footnote in this blog post. I'll hopefully give +that topic more attention later.

Feedback requested!

None of the changes discussed here are set in stone yet, so now's the perfect +time to clone the repos and start experimenting. In particular, I'm looking for +feedback from the following groups:

  • Users of WAI, either for writing WAI applications or WAI-based frameworks.
  • Users of http-conduit, who are either happy with the current API, or would be interested in any of the changes mentioned above (e.g., switching tls for OpenSSL).
  • Developers of other HTTP client libraries, who could consider collaboration on a single, shared HTTP client engine.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/12/announcing-wai-2-http-client.html b/public/blog/2013/12/announcing-wai-2-http-client.html new file mode 100644 index 00000000..6ea77a9c --- /dev/null +++ b/public/blog/2013/12/announcing-wai-2-http-client.html @@ -0,0 +1,44 @@ + Announcing: WAI 2.0 and http-client +

Announcing: WAI 2.0 and http-client

December 4, 2013

GravatarBy Michael Snoyman

I'm happy to announce the release of the Web Application Interface (WAI) 2.0, +as well as the first non-experimental release of +http-client (version 0.2). +The lion's share of the work on the new WAI and Warp was done by Kazu, and he's +planning on a separate write-up on some of the changes he's made.

On the http-client side, this release includes OpenSSL support both via the +pure-Haskell tls package +and the OpenSSL +library, as well as a +new version of http-conduit +(2.0) built on top of http-client. The API of http-conduit remains mostly +unchanged. A big thank you to Joseph Abrahamson for providing very helpful +feedback on the API design.

My previous blog post +discussed the motivations behind these releases, so I won't go into those +details here. At a high level, here are some of the motivations for these changes:

  • WAI 2.0
    • Improving performance of Warp
    • Scaling on multicore if complied with coming GHC 7.8
    • Cleaning up API of Warp and enriching documentations
  • http-client
    • Shared infrastructure for multiple high-level HTTP client libraries.
    • Choice of SSL backend.
    • Lower dependency requirements for simple HTTP queries.
    • Easier to test out new high-level APIs.

The only other thing I want to touch on is the affect on the rest +of the package ecosystem triggered by these releases.

Together with this release, I'm releasing new versions of all of the WAI and +Yesod package sets, as well as +http-reverse-proxy and +keter. Kazu is releasing new +versions of fast-logger and wai-logger as well. I highly recommend either +upgrading all of these packages or sticking to the old versions entirely; +mixing-and-matching may work, but has not been highly tested. (As usual, +Stackage, FP Haskell Center and yesod-platform will continue to provide sets of +compatible packages.)

I've taken a lot of care in the new release of Yesod to keep backwards +compatibility, so unless you were depending specifically on WAI libraries, your +old code should continue building without issue. That said, if you run into any +upgrade issues, please bring them up on the mailing list so that, minimally, +others can learn from the experience, and possibly the issues can be addressed +in the libraries themselves.

While I'm very excited with this release, and think it pushes the library +ecosystem in the right direction, I'm even happier that Yesod is continuing to +maintain a high level of API stability.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/12/book-updated-for-1-2.html b/public/blog/2013/12/book-updated-for-1-2.html new file mode 100644 index 00000000..8b44569d --- /dev/null +++ b/public/blog/2013/12/book-updated-for-1-2.html @@ -0,0 +1,23 @@ + Book fully updated for 1.2 +

Book fully updated for 1.2

December 11, 2013

GravatarBy Michael Snoyman

Yesterday I said that +the Yesod book was almost fully updated for version 1.2, and that I'd let +everyone know when it was completely updated. Turns out that the last chapter +took far less time to update than I'd expected. So I'm happy to announce that +the Yesod book is now fully revised and up-to-date with version 1.2 of Yesod. +The final chapter on using +Sphinx is now available, and +benefitted greatly from the simplified streaming data primitives introduced in +Yesod 1.2.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2013/12/book-updates-1-2.html b/public/blog/2013/12/book-updates-1-2.html new file mode 100644 index 00000000..67228f68 --- /dev/null +++ b/public/blog/2013/12/book-updates-1-2.html @@ -0,0 +1,28 @@ + Book updates for 1.2: almost complete! +

Book updates for 1.2: almost complete!

December 10, 2013

GravatarBy Michael Snoyman

Over the past week, I've been busy updating the Yesod book for version 1.2. I'm +happy to announce that this process is almost complete. The only chapter left +to be updated is the Sphinx example. Of particular note, I'd like to point +people at the Wiki/chat subsite +example, which has been +substantially updated to reflect changes in Yesod 1.2's subsite handling. This +chapter gives a pretty good overview of a decently complex application, so I +recommend people trying to get a better grasp of Yesod's type system check it +out.

I'm going to continue working on the Sphinx example, and will post when it's +available. After that, I'm planning on adding new content to the book. For now, +my plans are focused on a few new examples. Here are the ideas I've come up +with so far:

  • JSON serving
  • Using Fay for client side development
  • Providing streaming data via respondSource
  • Initializing data to be put into the foundation datatype
  • Getting configuration information from environment variables

I'm open to input for other ideas for examples. Also, if anyone can think of +content that's lacking in the rest of the book, please let me know. Now's the +time to fill in any missing pieces.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/announcing-resource-monad.html b/public/blog/2014/01/announcing-resource-monad.html new file mode 100644 index 00000000..9a0c242f --- /dev/null +++ b/public/blog/2014/01/announcing-resource-monad.html @@ -0,0 +1,50 @@ + Announcing: the Resource monad +

Announcing: the Resource monad

January 28, 2014

GravatarBy Michael Snoyman

Back in June, Gabriel Gonzalez wrote a blog post on the Resource +monad. At +the time, I thought it was an interesting idea, but I didn't have a very good +use case for it. However, while working on Persistent +2.0, I +realized it was a great way to abstract the concept of acquiring a database +connection, and allow both ResourceT and non-ResourceT access to +Persistent.

So with Gabriel's permission to steal his idea, I added the Resource monad to +the resourcet +package. +The internal representation is slightly different than the one presented in +Gabriel's blog post. In order to provide proper async exception safety, the internal structure is:

data Allocated a = Allocated !a !(IO ())
+
+newtype Resource a = Resource ((forall b. IO b -> IO b) -> IO (Allocated a))
+
+instance Monad Resource where
+    return a = Resource (\_ -> return (Allocated a (return ())))
+    Resource f >>= g' = Resource $ \restore -> do
+        Allocated x free1 <- f restore
+        let Resource g = g' x
+        Allocated y free2 <- g restore `E.onException` free1
+        return $! Allocated y (free2 `E.finally` free1)

Allocated provides a value and its cleanup method. Resource is a function +from mask's restore function to an action returning Allocated. By being +set up in this way, we know that async exceptions are not thrown in the +intermediate steps of monadic bind.

Usage of the API is pretty simple. We can create a file-opening resource:

openFileResource :: FilePath -> IOMode -> Resource Handle
+openFileResource fp mode = mkResource (openFile fp mode) hClose

Using the Applicative instance, we can then build this up into a Resource +for allocating both a file reader and writer:

myHandles :: Resource (Handle, Handle)
+myHandles = (,)
+    <$> openFileResource "input.txt" ReadMode
+    <*> openFileResource "output.txt" WriteMode

And then we can allocate these Handles with either the bracket pattern:

bracketCopy :: IO ()
+bracketCopy = with myHandles $ \(input, output) ->
+    sourceHandle input $$ sinkHandle output

or using ResourceT itself:

resourcetCopy :: IO ()
+resourcetCopy = runResourceT $ do
+    (releaseKey, (input, output)) <- allocateResource myHandles
+    sourceHandle input $$ sinkHandle output
+    release releaseKey

Hopefully others will find this abstraction useful as well.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/conduit-transformer-exception.html b/public/blog/2014/01/conduit-transformer-exception.html new file mode 100644 index 00000000..81c234a7 --- /dev/null +++ b/public/blog/2014/01/conduit-transformer-exception.html @@ -0,0 +1,82 @@ + Improved transformer and exception support in conduit +

Improved transformer and exception support in conduit

January 20, 2014

GravatarBy Michael Snoyman

I'm happy to announce a new release of +conduit (version 1.0.11). +This release was instigated by a pull request from Patrick +Wheeler, who wrote the majority +of the code that's been included.

tl;dr: +Data.Conduit.Lift +allows you to run individual components of a pipeline with different monad +transformers, and +catchC/tryC +let you deal with exceptions inside an individual component. Read on for more +details.

Patrick's pull request adds a number of functions for working with monad +transformers in conduit. While conduit has always had support for arbitrary +base monads, and for quite a while has allowed transforming the base monad via +hoist, +there's been one missing feature: the ability to have different pieces of a +pipeline use different transformer stacks.

While hoist on its own would allow a ReaderT transformer to be removed from +a stack, the same technique wouldn't work for any of the more interesting +transformers. This problem has been known for a while, and has in fact been +documented in the +Wiki +for over two years. Patrick's approach solves the problem.

Suppose you want to write a Sink which will sum up all of the incoming Ints into a single Int. (And forget for the moment that this is easy to do with Data.Conduit.List.fold.) An easy way to do this would be to use a StateT:

sumSink :: Monad m => Sink Int (StateT Int m) Int
+sumSink = do
+    awaitForever $ modify . (+)
+    get

Calling this looks like the following:

main :: IO ()
+main = evalStateT (mapM_ yield [1..10] $$ sumSink) 0 >>= print

Unfortunately, this has some problems:

  • The calling site needs to deal with unwrapping the StateT, when in reality the StateT usage is merely an implementation detail of the sumSink function.

  • Now all of the components of our pipeline need to have a matching StateT transformer. This can be arranged, especially by using hoist, but it's an unnecessary complication.

  • What happens if you have two components in a pipeline that each want to use their wrapping StateT in a different way? The two components would now conflict with each other. Consider this (only slightly contrived) example of a Conduit which accumulates its input values into a sum and yields that sum:

    import Data.Conduit
    +import Control.Monad.State
    +
    +sumSink :: Monad m => Sink Int (StateT Int m) Int
    +sumSink = do
    +    awaitForever $ modify . (+)
    +    get
    +
    +accumConduit :: Monad m => Conduit Int (StateT Int m) Int
    +accumConduit = awaitForever $ \i -> do
    +    total <- get
    +    let total' = total + i
    +    yield total'
    +    put total'
    +
    +main :: IO ()
    +main = evalStateT (mapM_ yield [1..10] $$ accumConduit =$ sumSink) 0 >>= print

    Unfortunately, this doesn't work as we'd hope, since each component overwrites the stored value of the sibling component. (And if you're curious, try swapping the ordering of the yield and put calls in accumConduit.)

The Data.Conduit.Lift module provides a simple solution to all of these problems. You can use whatever monad transformers you like when implementing a component of a pipeline, and then unwrap the transformer for that component alone. The function names look just like their transformers counterparts, but replace the trailing T with a C. To fix our example from above:

import Data.Conduit
+import Data.Conduit.Lift
+import Control.Monad.State
+
+sumSink :: Monad m => Sink Int m Int
+sumSink = execStateC 0 $ awaitForever $ modify . (+)
+
+accumConduit :: Monad m => Conduit Int m Int
+accumConduit = evalStateC 0 $ awaitForever $ \i -> do
+    total <- get
+    let total' = total + i
+    put total'
+    yield total'
+
+main :: IO ()
+main = (mapM_ yield [1..10] $$ accumConduit =$ sumSink) >>= print

Additionally, if you'd like to use the strict variants of Writer, State, or RWS, replace the trailing C with a trailing SC. Hopefully this technique will prove useful, and its usage is clear enough from this example. If there are questions about how to use it, please bring them up so I can update the documentation as needed.

Once I saw the technique used for these functions, I realized it could be used to implement an oft-requested piece of functionality: exception handling. To date, there was no way to deal with exceptions in the middle of a conduit pipeline. Instead, exception handlers had to be used outside of the pipeline.

As of this release of conduit, there's another option (or three, to be precise): catchC, handleC, and tryC, which behave almost identically to their Control.Exception counterparts. The important thing to note is that they will only catch exceptions thrown in the current component of the pipeline, not a different component. As an example, consider this failed attempt at copying files:

{-# LANGUAGE OverloadedStrings #-}
+import Data.Conduit
+import Data.Conduit.Binary
+import Data.ByteString (ByteString)
+import Data.ByteString.Char8 (pack)
+import Control.Exception (IOException)
+
+main :: IO ()
+main = runResourceT $ src $$ sinkFile "/does/not/exist/output.txt"
+
+src :: Source (ResourceT IO) ByteString
+src = sourceFile "/does/not/exist/input.txt" `catchC` \e ->
+    yield (pack $ "Could not read input file: " ++ show (e :: IOException))

Both the source and sink end up throwing exceptions. While the exception handler in the source would catch its thrown exception, the exception from the sink will remain uncaught. This leads to one very important caveat. Taken straight from the API docs:

Note: this will not catch exceptions thrown by other components! For example, if an exception is thrown in a Source feeding to a Sink, and the Sink uses catchC, the exception will not be caught.

Due to this behavior (as well as lack of async exception handling), you should not try to implement combinators such as onException in terms of this primitive function.

As before, if you need full exception safety for cleaning up scarce resources, ResourceT will remain your best bet (though, if you're so inclined, you can use the bracket pattern instead). But for the cases where you simply want to deal with exceptional circumstances, catchC and family should provide you with the tools you need.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/initializing-foundation-data.html b/public/blog/2014/01/initializing-foundation-data.html new file mode 100644 index 00000000..f3a89016 --- /dev/null +++ b/public/blog/2014/01/initializing-foundation-data.html @@ -0,0 +1,182 @@ + Initializing data in the foundation datatype +

Initializing data in the foundation datatype

January 27, 2014

GravatarBy Michael Snoyman

I've added a very short chapter to the Yesod book giving an example of performing initialization before your application starts. The technique isn't complicated (which explains why the chapter is so short), but new users often get stuck trying to find the canonical way to do this. Hopefully this chapter will clarify the issue. If there's anything unclear in the example, please let me know so I can update it.

+ +

Below is the current version of the content, though I recommend reading from the link above to ensure you get the newest version of the content.

+ +
+ + +

This example is meant to demonstrate a relatively simple concept: performing +some initialization of data to be kept in the foundation datatype. There are +various reasons to do this, though the two most important are:

+
    +
  • +

    +Efficiency: by initializing data once, at process startup, you can avoid + having to recompute the same value in each request. +

    +
  • +
  • +

    +Persistence: we want to store some information in a mutable location which + will be persisted between individual requests. Often times, this is done via + an external database, but it can also be done via an in-memory mutable + variable. +

    +
  • +
+ +

To demonstrate, we’ll implement a very simple website. It will contain a single +route, and will serve content stored in a Markdown file. In addition to serving +that content, we’ll also display an old-school visitor counter indicating how +many visitors have been to the site.

+
+

Step 1: define your foundation

+

We’ve identified two pieces of information to be initialized: the Markdown +content to be display on the homepage, and a mutable variable holding the +visitor count. Remember that our goal is to perform as much of the work in the +initialization phase as possible and thereby avoid performing the same work in +the handlers themselves. Therefore, we want to preprocess the Markdown content +into HTML. As for the visitor count, a simple IORef should be sufficient. So +our foundation data type is:

+
data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+
+

Step 2: use the foundation

+

For this trivial example, we only have one route: the homepage. All we need to do is:

+
    +
  1. +

    +Increment the visitor count. +

    +
  2. +
  3. +

    +Get the new visitor count. +

    +
  4. +
  5. +

    +Display the Markdown content together with the visitor count. +

    +
  6. +
+

One trick we’ll use to make the code a bit shorter is to utilize record +wildcard syntax: App {..}. This is convenient when we want to deal with a +number of different fields in a datatype.

+
getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+
+

Step 3: create the foundation value

+

When we initialize our application, we’ll now need to provide values for the +two fields we described above. This is normal IO code, and can perform any +arbitrary actions needed. In our case, we need to:

+
    +
  1. +

    +Read the Markdown from the file. +

    +
  2. +
  3. +

    +Convert that Markdown to HTML. +

    +
  4. +
  5. +

    +Create the visitor counter variable. +

    +
  6. +
+

The code ends up being just as simple as those steps imply:

+
main :: IO ()
+main = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+

Conclusion

+

There’s no rocket science involved in this example, just very straightforward +programming. The purpose of this chapter is to demonstrate the commonly used +best practice for achieving these often needed objectives. In your own +applications, the initialization steps will likely be much more complicated: +setting up database connection pools, starting background jobs to batch process +large data, or anything else. After reading this chapter, you should now have a +good idea of where to place your application-specific initialization code.

+

Below is the full source code for the example described above:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import qualified Data.Text.Lazy.IO as TLIO
+import           Text.Markdown
+import           Yesod
+
+data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+main :: IO ()
+main = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/lens-generator-persistent.html b/public/blog/2014/01/lens-generator-persistent.html new file mode 100644 index 00000000..4eec7344 --- /dev/null +++ b/public/blog/2014/01/lens-generator-persistent.html @@ -0,0 +1,80 @@ + Lens generator in Persistent +

Lens generator in Persistent

January 1, 2014

GravatarBy Michael Snoyman

I've just released version 1.3.1 of persistent-template, which adds a new +options: mpsGenerateLenses. When enabled, this option will cause lenses to be +generated instead of normal field accessors. An example is worth a thousand +words:

{-# LANGUAGE EmptyDataDecls    #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE GADTs             #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Sqlite
+import           Database.Persist.TH
+import           Control.Lens
+
+share
+  [ mkPersist sqlSettings { mpsGenerateLenses = True }
+  , mkMigrate "migrateAll"
+  ] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+
+    Just john <- get johnId
+
+    liftIO $ putStrLn $ john ^. personName
+
+    janeId <- insert $ john & personName .~ "Jane Joe"
+    Just jane <- get janeId
+    liftIO $ print jane

Not a major feature, but convenient. It can also be combined with the +relatively new mpsPrefixFields option if your field names are unique, e.g.:

share
+  [ mkPersist sqlSettings { mpsGenerateLenses = True, mpsPrefixFields = False }
+  , mkMigrate "migrateAll"
+  ] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+
+    Just john <- get johnId
+
+    liftIO $ putStrLn $ john ^. name
+
+    janeId <- insert $ john & name .~ "Jane Joe"
+    Just jane <- get janeId
+    liftIO $ print jane

Note that this isn't the first use of lenses in +Persistent. +While this implementation is fully compatible with the lens package, it +introduces no dependency on that package, due to the beautiful way in which +lenses work.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/new-fast-logger.html b/public/blog/2014/01/new-fast-logger.html new file mode 100644 index 00000000..6013e432 --- /dev/null +++ b/public/blog/2014/01/new-fast-logger.html @@ -0,0 +1,50 @@ + Faster and scalable fast-logger +

Faster and scalable fast-logger

January 31, 2014

GravatarBy Kazu Yamamoto

Faster and scalable fast-logger

As you may know, Michael and I released a set of packages for WAI 2.0 including Warp 2.x and fast-logger 2.x. +They are much faster than their old versions. +I will explain how I re-designed fast-logger in this article and how I improved the performance of Warp in the next one.

The coming GHC 7.8 and Handle

One of the coolest features of the coming GHC 7.8 is multicore IO manager. +The runtime system of GHC 7.6 or earlier provides parallel GC, efficient memory allocation mechanism and the IO manager. +So, a concurrent program complied by GHC 7.6 or earlier should be scaled on a multicore system if the +RTS -Nx command-line option is specified.

Unfortunately, it appeared that this is not the case. +This is mainly because the IO manager has several bottlenecks. +Andreas Voellmy analyzed the bottlenecks and implemented the multicore IO manager (mainly for Linux). +I helped him in testing it and porting it to BSD variants.

If you compile your concurrent program by the coming GHC 7.8, the executable scales on multicores. +No modifications are necessary. +Just specify the +RTS -Nx option (and -qa -A32m if necessary). +It is really nice, isn't it?

However, GHC 7.8 would disclose another bottleneck of your program.

I noticed this when I was trying to get Mighty prepared for GHC 7.8. +Mighty 2.x uses the pre-fork technique to scale on multicore systems. +That is, Mighty 2.x forks processes according to its configuration file to get over the bottlenecks of the IO manager. +If you want to know the pre-fork technique in detail, please read Mighttpd – a High Performance Web Server in Haskell.

I modified Mighty with the pre-fork code removed because it will be not necessary for GHC 7.8. +This change made Mighty much simpler. +This is why I spent much my time to develop the multicore IO manager. +I bumped up the version of Mighty to 3.

Mighty 3 scaled on multicore systems as I expected when logging is off. Unfortunately, Mighty 3 with logging enabled does not scales at all. +For instance, I run Mighty 3 with logging on a 12 core machine. +The performance of +RTS -N10 is worse than that of +RTS -N1.

Why? That is because fast-logger uses Handle. Since this Handle is shared by all user threads and Handle is protected with MVar, the Handle is a global giant lock!

Re-designing fast-logger

As I wrote in Mighttpd – a High Performance Web Server in Haskell, I tested many ideas when I implemented fast-logger at the beginning. Handle is the fastest among them but the performance of web servers loses 49% if fast-logger is enabled. I did not have other speed-up techniques at that time.

The experiment above reminds me this performance issue of logging. After working with Michael and Andreas, I became familiar with GHC much more in detail. Now I can design new fast-logger.

Logger consists of a buffer, its size, and a reference to a log message queue:

data Logger = Logger (MVar Buffer) !BufSize (IORef LogStr)

A log message is defined as follows:

data LogStr = LogStr !Int Builder
+
+instance Monoid LogStr where
+    mempty = LogStr 0 (toBuilder BS.empty)
+    LogStr s1 b1 `mappend` LogStr s2 b2 = LogStr (s1 + s2) (b1 <> b2)

That is, log messages are Builder with its length. Because a log message is an instance of Monoid, a log message itself behaves as a queue. We can append a log message to a queue with (<>) in O(1). Atomic append operation is ensured with atomicModifyIORef'.

Here is a simplified code to append a log message to a queue. (Note that pushLog is not disclosed.)

pushLog :: FD -> Logger -> LogStr -> IO ()
+pushLog fd logger@(Logger mbuf size ref) nlogmsg@(LogStr nlen nbuilder) = do
+    mmsg <- atomicModifyIORef' ref checkBuf
+    case mmsg of
+        Nothing  -> return ()
+        Just msg -> withMVar mbuf $ \buf -> writeLogStr fd buf size msg
+  where
+    checkBuf ologmsg@(LogStr olen _)
+      | size < olen + nlen = (nlogmsg, Just ologmsg)
+      | otherwise          = (ologmsg <> nlogmsg, Nothing)

When a log message is appended to a queue, its total length is compared with the current buffer size (in checkBuf). If the total length is bigger than the buffer size, the queue is swapped with the log message. Then, the buffer is locked. The log messages in the old queue are copied into the buffer then the buffer is written into its corresponding file. Otherwise, the log message is just appended to the queue.

Why is this new approach fast? Well, the new one takes a lock only when it flushes its buffer and the locking can be obtained in almost all cases. But the old one tries to take a lock everytime when each message is copied into Handle's buffer.

To my experiment, this is not good enough yet to scale on multicore systems. So, I prepared Logger per core. +It is really effective. +The API provides the following abstract data type:

data LoggerSet = LoggerSet (Maybe FilePath) (IORef FD) (Array Int Logger)

You can create LoggerSet with the following APIs:

newFileLoggerSet :: BufSize -> FilePath -> IO LoggerSet
+newStdoutLoggerSet :: BufSize -> IO LoggerSet
+newStderrLoggerSet :: BufSize -> IO LoggerSet

To log a message, pushLogStr is used:

pushLogStr :: LoggerSet -> LogStr -> IO ()

When a user thread calls pushLogStr, a Logger is selected according to the core number on which the thread is running.

The new fast logger loses only about 10% of performance on any numbers of cores.

I would like to thank Michael Snoyman, Toralf Wittner, Tobias Florek, and Gregory Collins for their contributions to fast-logger.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/psa-please-use-yesod-platform.html b/public/blog/2014/01/psa-please-use-yesod-platform.html new file mode 100644 index 00000000..7826aa4e --- /dev/null +++ b/public/blog/2014/01/psa-please-use-yesod-platform.html @@ -0,0 +1,15 @@ + PSA: Please use yesod-platform +

PSA: Please use yesod-platform

January 15, 2014

GravatarBy Michael Snoyman

Recently, a large number of the dependencies for Yesod (aeson, fast-logger, warp, attoparsec, etc) have gone through major version bumps. At the same time, a number of other dependencies of Yesod have not relaxed their version bounds for the most recent versions of their dependencies. This situation leads to a lot of confusion for cabal trying to figure out a coherent installation path.

This is precisely the situation for which the yesod-platform situation was designed. If you are having trouble getting Yesod installed, please try running cabal install yesod-platform instead of yesod. (In fact, unless you have reason to do otherwise, I highly recommend always doing this.) If you have an existing project with its own set of dependencies, go into that directory and run cabal install . yesod-platform, which will attempt to choose a version of yesod-platform with dependencies that fit your constraints. If you use some kind of sandboxing, modify the above commands as necessary.

And let me take this opportunity to mention: if you're trying to build your code on a stable platform, you should make sure that you pin down the versions of all of your dependencies. Greg Weber wrote a great blog post explaining a technique that should fit into most people's standard build processes. Using the Stackage pinned version set will also guarantee builds that are coherent. And of course, building on FP Haskell Center guarantees stable and tested versions of packages which will be maintained.

UPDATE There are two important command line options I should have mentioned. If you're running into dependency hell, try appending the following: --max-backjumps=-1 --reorder-goals.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/st-monad-conduit.html b/public/blog/2014/01/st-monad-conduit.html new file mode 100644 index 00000000..32576909 --- /dev/null +++ b/public/blog/2014/01/st-monad-conduit.html @@ -0,0 +1,144 @@ + The ST monad and conduit +

The ST monad and conduit

January 16, 2014

GravatarBy Michael Snoyman

I've recently been working on some higher level analysis utilities based on top +of +conduit. +A major component of this work is performing high-performance numerical +calculations on large streams of data. So I was particularly intriguied when I +saw a StackOverflow +question +that touched on the same points. I'd like to elaborate on my answer to that +question, and demonstrate a possible addition to the conduit library.

The issue at play in that question is the desire to tally up the frequency of +each octet in a stream of data. If you look through the answers, it quickly +becomes apparent that using some kind of a mutable packed data structure +(either Vector or Array) provides drastically better performance than +immutable data structures. For our purposes, let's stick with the vector +library, though the discussion here applies equally well to array.

The actions we need to perform are very straight-forward: read in the entirety +of the data from some source (let's say standard input), and perform a mutating +action for each and every octet that we receieve. We could read the entire +stream of data using lazy I/O, but as readers of this blog are likely aware, +I'd rather avoid lazy I/O when possible. So in my answer, I used conduit to +read in the stream of data. The answer looks like this:

import           Control.Monad.Trans.Class   (lift)
+import           Data.ByteString             (ByteString)
+import           Data.Conduit                (Consumer, ($$))
+import qualified Data.Conduit.Binary         as CB
+import qualified Data.Vector.Unboxed         as V
+import qualified Data.Vector.Unboxed.Mutable as VM
+import           System.IO                   (stdin)
+
+freqSink :: Consumer ByteString IO (V.Vector Int)
+freqSink = do
+    freq <- lift $ VM.replicate 256 0
+    CB.mapM_ $ \w -> do
+        let index = fromIntegral w
+        oldCount <- VM.read freq index
+        VM.write freq index (oldCount + 1)
+    lift $ V.freeze freq
+
+main :: IO ()
+main = (CB.sourceHandle stdin $$ freqSink) >>= print

We now have a reusable freqSink component that will consume a stream of +ByteStrings to produce a Vector Int of the frequencies. The Sink creates +a new mutable vector to hold the frequency values, maps over all the input +octets, and for each octet updates the mutable vector. Finally, it freezes the +mutable vector into an immutable one and returns it.

I like almost everything about this, except for two characters: IO. Our +freqSink function sets the base monad to be IO, implying that freqSink +may perform actions that have an impact on the outside world. However, we know +that this isn't the case: by analyzing the code, we see that all of the +mutating changes are contained within the little world that freqSink creates +for itself. In other words, this function is referentially transparent, but the +type signature is saying otherwise.

Fortunately, Haskell already has the perfect solution for this kind of a +problem: the ST monad. All we need to do is swap IO for ST s and +freqSink will be properly annotated as being referentially transparent. But +when we make this change, we get the following error message:

Couldn't match type `ST s0' with `IO'

The problem is that, while freqSink is refentially transparent, +sourceHandle is not. Since the source is capable of performing arbitrary +IO, it has to live in the IO base monad, and since the two components live +in the same processing pipeline, freqSink must match that base monad as well. +While this all works, it's still quite disappointing.

But perhaps we can have our cake and eat it too. We want freqSink's type +signature to be refentially transparent, which means it needs to live in ST. +What we need is some way to turn an ST-based Sink into an IO-based +Sink. And there's a function that let's us do just that: +unsafeSTToIO. +This ends up looking like:

import           Control.Monad.Morph         (hoist)
+import           Control.Monad.ST            (ST)
+import           Control.Monad.ST.Unsafe     (unsafeSTToIO)
+import           Control.Monad.Trans.Class   (lift)
+import           Data.ByteString             (ByteString)
+import           Data.Conduit                (Consumer, ($$))
+import qualified Data.Conduit.Binary         as CB
+import qualified Data.Vector.Unboxed         as V
+import qualified Data.Vector.Unboxed.Mutable as VM
+import           System.IO                   (stdin)
+
+freqSink :: Consumer ByteString (ST s) (V.Vector Int)
+freqSink = do
+    freq <- lift $ VM.replicate 256 0
+    CB.mapM_ $ \w -> do
+        let index = fromIntegral w
+        oldCount <- VM.read freq index
+        VM.write freq index (oldCount + 1)
+    lift $ V.freeze freq
+
+main :: IO ()
+main = (CB.sourceHandle stdin $$ hoist unsafeSTToIO freqSink) >>= print

This once again works, and freqSink's type signature now indicates that it is +referentially transparent. However, we've put two heavy burdens on users of our +freqSink function:

  • They need to know about hoist and understand how to use it.
  • They need to pull in an unsafe function and know in which circumstances it's safe to use it.

What we really want is to provide a general purpose function which is +completely safe. We want to contain the concept of "I can safely swap out the +base monad of this Conduit with some other base monad." So I've just pushed a +new commit to +conduit +adding the conduitSwapBase helper function (name up for debate). Let's start +by seeing how it solves our present problem:

import           Control.Monad.ST            (ST)
+import           Control.Monad.Trans.Class   (lift)
+import           Data.ByteString             (ByteString)
+import           Data.Conduit                (Consumer, ($$))
+import qualified Data.Conduit.Binary         as CB
+import           Data.Conduit.Util           (conduitSwapBase)
+import qualified Data.Vector.Unboxed         as V
+import qualified Data.Vector.Unboxed.Mutable as VM
+import           System.IO                   (stdin)
+
+freqSinkST :: Consumer ByteString (ST s) (V.Vector Int)
+freqSinkST = do
+    freq <- lift $ VM.replicate 256 0
+    CB.mapM_ $ \w -> do
+        let index = fromIntegral w
+        oldCount <- VM.read freq index
+        VM.write freq index (oldCount + 1)
+    lift $ V.freeze freq
+
+freqSink :: Monad m => Consumer ByteString m (V.Vector Int)
+freqSink = conduitSwapBase freqSinkST
+
+main :: IO ()
+main = (CB.sourceHandle stdin $$ freqSink) >>= print

I renamed the previous function to freqSinkST, leaving its type exactly as it +was. In addition, we now have a new freqSink, which can live in any base +monad. The type signature makes it completely clear that this function is +referentially transparent. And all we needed to do was use conduitSwapBase to +perform this conversion.

Once that conversion is performed, we can easily combine freqSink with our +IO-based sourceHandle. Or for that matter, it could be combined with a +completely pure source, or a source living in the Maybe monad.

I believe this function could be used to clean up the compression/decompression +functions in +zlib-conduit +and (at least some of) the functions in +blaze-builder-conduit.

As it stands right now, conduitSwapBase will allow the following base transformations to be applied:

  • ST can be converted to ~~any other monad~~. EDIT See update below.
  • Identity can be converted to any other monad.
  • IO can be converted to any instance of MonadIO.
  • For many transformers (all instances of MonadTransControl actually), if the base monad m1 can be converted to m2, then the transformer t m1 can be converted to t m2.

This addition allows us to keep more type safety in our codebase, while still +allowing safe interleaving of IO actions with pure code. I'm happy with the +addition so far, I'm curious to hear further ideas from the community.

UPDATE: As pointed out on +Reddit, +a backtracking base monad can break refential transparency for ST. I've +pushed a new +commit +that constrains the types of monads that can be converted to. In particular, it +works for monads which are processed in a linear/non branching manner. This +includes Identity, IO and Maybe, and transformers like ReaderT and ErrorT.

I'm currently calling this concept MonadLinear, but I have a strong feeling that there's a better abstraction already in existence.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/01/update-st-monad-conduit.html b/public/blog/2014/01/update-st-monad-conduit.html new file mode 100644 index 00000000..8fdd7950 --- /dev/null +++ b/public/blog/2014/01/update-st-monad-conduit.html @@ -0,0 +1,27 @@ + Update: The ST monad and conduit +

Update: The ST monad and conduit

January 17, 2014

GravatarBy Michael Snoyman

I wanted to post an update on yesterday's blog +post about using the ST +monad in conduit. On Reddit, gereeter pointed +out +some serious problems with the approach I laid out. I think the problems come +down to two categories:

  • Users can (ab)use the conduitSwapBase abstraction to perform arbitrary ST actions anywhere in their code, which is clearly wrong:

    unsafePerform = runPipe . conduitSwapBase . lift
  • If a Conduit gets run multiple times from some point in the middle of its computation, the abstraction breaks. This can occur in at least two circumstances:

    • Using a backtracking base monad such as list.
    • Using connect-and-resume and then connecting the same intermediate ResumableSource multiple times.

So it's quite clear at this point that the ST instance used by conduitSwapBase is wrong and should be removed. For now, I think I'm going to remove the entire conduitSwapBase machinery since, while it may be somewhat useful for lifting IO to MonadIO and other such things, such functionality is already present in conduit.

All that said... what about my original problem? Are we really going to give up on the dream of having a referentially transparent signature for freqSink? Actually, I realized I was being incredibly dense yesterday. Let's look again at the initial freqSink I provided:

freqSink :: Consumer ByteString IO (V.Vector Int)
+freqSink = do
+    freq <- lift $ VM.replicate 256 0
+    CB.mapM_ $ \w -> do
+        let index = fromIntegral w
+        oldCount <- VM.read freq index
+        VM.write freq index (oldCount + 1)
+    lift $ V.freeze freq

My complaint about this was that the type signature says IO, when in reality it works for both IO and ST. However, I'd simply forgotten what the actual type signatures in the vector package look like, e.g.:

new :: (PrimMonad m, Unbox a) => Int -> m (MVector (PrimState m) a)

In other words, none of the code I wrote above ever implied the IO constraint. So I can trivially generalize the type signature of freqSink above to:

freqSink :: PrimMonad m => Consumer ByteString m (V.Vector Int)

This now gives exactly the right implication about the code: it runs in any prim monad (which happens to be IO or ST), and while it is externally referentially transparent, it makes use of some state transformer operations under the surface. Looking at the critiques of the conduitSwapBase approach above, let's see if they're resolved:

  • There's definitely no way to implement unsafePerform, since we haven't added any ST-specific code to conduit!
  • Putting a backtracking monad as the base monad is now impossible, since it wouldn't be an instance of PrimMonad.
  • Connect-and-resume will maintain the intermediate state for each time the ResumableSource is reused. However, that's always the case when using IO or ST as a base monad.

So overall, this seems like a much saner approach to the problem.

Thanks again to gereeter for the feedback on the previous blog post and catching my mistakes!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/02/announce-mono-chunked.html b/public/blog/2014/02/announce-mono-chunked.html new file mode 100644 index 00000000..085cd748 --- /dev/null +++ b/public/blog/2014/02/announce-mono-chunked.html @@ -0,0 +1,84 @@ + Announcing mono-traversable, chunked-data, and conduit-combinators +

Announcing mono-traversable, chunked-data, and conduit-combinators

February 16, 2014

GravatarBy Michael Snoyman

I'm happy to announce the release of a number of packages today, in particular:

I want to discuss the purpose of these packages and recent changes. I'm very +excited about some of the opportunities these packages are presenting for +future growth and use.

mono-traversable 0.3

mono-traversable's core concept is abstracting over a common pattern: data +structures that look like containers, but which may be monomorphic. The primary +example of this is ByteString and Text, though this abstraction has proven +useful for other cases as well, such as unboxed and storable Vectors, where +some typeclass constraint limits the allowed value type.

In my experience so far, the most powerful abstraction has been MonoFoldable. +Unlike MonoFunctor and MonoTraversable, you don't end up losing any +flexibility in data types, since we do not need to retain the original data +structure. As such, I've switched over to using MonoFoldable variants of +functions in place of Foldable ones in all of my new code.

In addition to this core abstraction, mono-traversable provides abstractions +for Sets and Maps, non-empty data types, and sequences. This last point can +serve as a replacement for a number of list-like abstractions which have been +implemented separately over the years.

Since the 0.2 release, Greg and I have worked on the following improvements:

  • Additional instances for Either (thanks to João Cristóvão).
  • An optimized mapM_ implementation for ByteString.
  • Greatly improved test coverage.
  • Remove a number of type classes. As an example, we used to have a separate typeclass for non-empty types which had elements which could be ordered, which allowed for a total and optimized implementation of maximum and minimum. Instead, in version 0.3, MonoFoldable provides a maximumEx function, which can be optimized for an individual datatype, but may throw an exception, and then any non-empty data type may use maximum as a total function. We've done similar things for head, tail and others.
  • Added unsafe functions like unsafeHead when you need more performance (thanks to John Lato for the idea).
  • Greatly expanded the collection of Map and Set functions.
  • A new Data.MinLen module (still experimental), which extends the concept of non-null. Instead of simply encoding size as a boolean, we use type-level naturals to encode the minimum known length of a value. This allows, for example, such total functions as head $ tail $ mlappend listLen1 listLen1. Greg and I have decided to release this initially as an experimental module and, if it works well, replace the current Data.NonNull with it.

The core functionality of mono-traversable is pretty stable, and I highly +encourage people to give it a shot.

chunked-data

mono-traversable started as extracting some of the typeclass-based +functionality from classy-prelude in a principled, law-abiding manner. About +half of that functionality fell under the rubrik of monomorphic containers. The +other half was an abstraction of different kinds of chunked data: reading a +chunk of data from a Handle, textual encoding/decoding, lazy sequences, +builders, and zipping. chunked-data contains the remainder of this +functionality.

This package should be considered somewhat experimental. However, most of the +code is simply extracted from classy-prelude, where it's already had quite a +bit of testing. So those of you who are somewhat adventurous should definitely +jump in and play with it now. If you're more conservative, give in another +month or so before experimenting.

conduit-combinators

This is the release I'm most excited about. There's quite a lot going on here, +and I'll probably write a separate blog post going into the details of this +package. But here's the basic idea.

Until now, the primary combinator collection for conduit was the +Data.Conduit.List module. This module contains many commonly used functions, +like map and isolate. However, there are two things I'm unhappy with in +the current module:

  • The API only works on non-chunked data. For chunked data (like Text and +ByteString), you need to use Data.Conduit.Text and Data.Conduit.Binary, +respectively. John Lato and I have discussed this a bit in the past, and he +made a very convincing argument to me that providing a unified chunked data API +is superior.

  • The naming scheme of Data.Conduit.List does not always encourage best +practices. For example, take consumes all incoming values into memory. This +was a decision inherited from enumerator, and made sense back when the library +was first created. But common usage has changed.

This doesn't mean that there's anything broken in Data.Conduit.List, and +therefore I have no intention of breaking backwards compatibility by changing +the functions there. However, I think it's time to introduce a new, more modern +module providing even more combinators, chunked variations, and a more +consistent naming scheme.

That's where conduit-combinators comes into play. As a quick rundown:

  • provides generalized versions of all (unless I missed something) functions from Data.Conduit.List, Data.Conduit.Binary, and Data.Conduit.Text
    • The package makes heavy use of mono-traversable and chunked-data to let this happen, which is what originally instigated my work on those two packages in this development cycle.
  • since it's higher on the dependency chain, it can depend on packages like vector, and provide specific functions for it (like sinkVector)
  • adds a lot of missing functions (like takeWhile and mapWhile)
  • exports a module intended for qualified import: Data.Conduit.Combinators
  • exports a new module, Conduit, which provides a one-stop-shop for all commonly needed conduit functionality. Combinator names are munged by appending a C.

I know many people out there are quite happy with qualified imports, but for +those of you like me who enjoy writing a short import list and then having all +of the functionality you need available, I think the new Conduit module will +be a real pleasure.

Since I've been giving stability forecasts, I'll do the same thing here. For +the basic functions like map and sinkList, it's highly unlikely that there +will ever be a change. However, as I get feedback on the library, I'm likely +to make some tweaks to behavior of other functions. One example: there are two +sets of behavior that the drop function could reasonably have:

  • Drop the given number of items, and let the next monadically composed component consume the rest. In this case, drop would be a Consumer.
  • Drop the given number of items, and then yield the rest of the items downstream. In this case, drop would be a Conduit.

I've chosen the former, but I'm open to discussion on the topic.

So if you're using conduit in any significant way in your codebase, it's +definitely worth checking out this new package. I wouldn't port large codebases +over to use it yet unless you're okay needing to refactor again in the next month +or so. But I anticipate this package reaching a stable point in the very near +future (less than 3 months from now), as it will be a vital component of some +work I'm doing at FP Complete.

One nice fact about this library: it is currently sitting at 100% HPC test +coverage, and I intend to maintain that level going forward.

classy-prelude 0.8

The most exciting thing about the 0.8 release of classy-prelude is what's not +there: any significant code! After the work I've mentioned above, +classy-prelude (and -conduit and -yesod) has now become a simple re-export +module for functionality defined elsewhere. This is great: the core +generalizations have been made available for wider usage, and classy-prelude is +merely a convenience for getting at all of that functionality at once.

Since classy-prelude is based on the packages above, the same stability +guidelines apply. In particular, classy-prelude-conduit may see quite a bit of +turbulence as conduit-combinators changes. And if you used the chunked-data or +Data.MinLen re-exports from ClassyPrelude, those may be updated in the future. +But the vast majority of classy-prelude has remained stable for the past number +of versions (accounting for well over a year), and will continue to do so in +the future.

One question that I've raised and am looking for feedback on: should +ClassyPrelude export mtl data types and functions? I opened a Github issue for +discussion, and am +interested if others have any thoughts on the matter.

Look forward to some blog posts demonstrating usage of these libraries over the +next few weeks. And if you have any recommendations for improvement, please +bring them up!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/02/conduit-zip-source.html b/public/blog/2014/02/conduit-zip-source.html new file mode 100644 index 00000000..390d33ad --- /dev/null +++ b/public/blog/2014/02/conduit-zip-source.html @@ -0,0 +1,60 @@ + conduit 1.0.13 and ZipSource +

conduit 1.0.13 and ZipSource

February 4, 2014

GravatarBy Michael Snoyman

A few months ago, Petr Pudlák kicked off the conduit-extra +package with the +addition of ZipSink. As a refresher, ZipSink provides an alternative +Applicative instance for Sink which allows multiple Sinks to consume the +same stream.

I've just release version 1.0.13 of conduit which promotes ZipSink from +conduit-extra into conduit itself. This abstraction has proven to be generally +useful, and I hope others enjoy it as well. If you want a more in-depth review +of it, please see the original blog post. The only change since then is +renaming broadcast to sequenceSinks.

Along with this change, version 1.0.13 adds a new, similar concept: ZipSource +and sequenceSources. The idea here is to combine together multiple streams, +instead of sequencing one stream after another.

As a simple motivating example, let's say we have some files on the filesystem, +where each file contains a list of student test scores. We want to combine +together the test scores for all students for each of the tests. The following +program does the job:

import           Control.Monad.Trans.Class (lift)
+import qualified Data.ByteString.Char8     as S8
+import           Data.Conduit
+import qualified Data.Conduit.Binary       as CB
+import qualified Data.Conduit.List         as CL
+import qualified Data.Map                  as Map
+
+type Name = String
+
+people :: [Name]
+people = words "alice bob charlie"
+
+files :: Map.Map Name FilePath
+files = Map.fromList $ map (\name -> (name, name ++ ".txt")) people
+
+scores :: MonadResource m => FilePath -> Source m Int
+scores fp
+    = CB.sourceFile fp
+   $= CB.lines
+   $= CL.map (read . S8.unpack)
+
+sources :: MonadResource m => Map.Map Name (Source m Int)
+sources = fmap scores files
+
+sources' :: MonadResource m => Source m (Map.Map Name Int)
+sources' = sequenceSources sources
+
+main :: IO ()
+main = runResourceT $ sources' $$ CL.mapM_ (lift . print)

The important bit is the definition of sources'. We use sequenceSources to +combine together each of the individual test scores into a single test score +map. With some basic input files, the output looks like:

fromList [("alice",1),("bob",3),("charlie",2)]
+fromList [("alice",2),("bob",2),("charlie",2)]
+fromList [("alice",3),("bob",1),("charlie",2)]

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/02/ideas-pipes-parse.html b/public/blog/2014/02/ideas-pipes-parse.html new file mode 100644 index 00000000..f3088164 --- /dev/null +++ b/public/blog/2014/02/ideas-pipes-parse.html @@ -0,0 +1,309 @@ + Some ideas for pipes-parse +

Some ideas for pipes-parse

February 11, 2014

GravatarBy Michael Snoyman

I haven't mentioned them before, but many times in the past, I've played around +with from-scratch implementations of conduit to test out new theories. I've +tried having versions without automatic termination of any sort, different +orderings of finalizers, various different ways of handling leftovers, and +strict adherence to the category laws. Each one of them has taught me +something, but since the 1.0 release, none of them were enough of an +improvement on what exists already to warrant the pain in switching. And +frankly, most of them were far worse than what we already have.

A few days ago, Gabriel Gonzalez wrote a blog +post +about the newest release of pipes-parse. Given that we've had many +conversations over the years- both publicly and privately- about the directions +of our libraries, I wasn't surprised that the core idea behind this library is +something we'd discussed once before, and in fact was an idea I'd even tried +out for conduit once. Based on that experience, I'd like to share some ideas on +how Gabriel (or others) could approach the problem a bit differently, and +possibly avoid some user-facing issues.

Proliferation of APIs

Before discussing the details of leftovers themselves, let mention what I +consider the primary issue. It seems that each new feature added to pipes is +introducing a completely separate API. Consider, for example, a simple +question: how do you get the next value in a stream? In the conduit world, the +answer is await. There are also some convenience functions built on +top of await: awaitForever, peek, mapM_, etc. But there is just one +primitive for awaiting values, and it has the same type everywhere:

await :: Monad m => Consumer i m (Maybe i)

In the pipes world, there are now (to my knowledge) three different "get the +next value" primitives:

await :: Monad m => Consumer' a m a
+next :: Monad m => Producer a m r -> m (Either r (a, Producer a m r))
+draw :: Monad m => Parser a m (Maybe a)

This in turn means that utility functions need to be written multiple times, in +different ways, depending on the context in which they're needed. For example, +takeWhile in Pipes.Prelude works on a "normal" Pipe, but will silently +discard one extra value from the stream since a normal Pipe does not support +leftovers. span from Pipes.Parse performs essentially the same +functionality, but works in the Parser/leftover aware realm.

One of the biggest changes to come to the conduit library was the unification +of the Source, Sink, and Conduit datatypes into a single datatype, called +Pipe. And the reason it's called Pipe is, as you might guess, because it +was inspired from the pipes world (via Twan van +Laarhoven). Though I was +skeptical at first about the confusion in error messages and type signatures +which may have ensued, the net result was, in my mind, undeniably positive.

It seems to me like pipes is now at that same kind of crossroads. There are a +large number of different data types and type synonyms, different ways of +composing things together, and different functionality depending on what type +you're using. I'd recommend standardizing on one as the canonical entry point +to pipes, and make the standard libraries all use that API.

It seems like the Parser API is the best suited for this task. Unless I'm +mistaken, all of the folding functions in Pipes.Prelude (e.g., toList) can +be implemented in terms of Parser, and it adds the capability of leftovers. +If this change happened, then functions like takeWhile would no longer have +to silently discard data from the data stream either.

Side note: you might think that conduit has lots of different kinds of +composition due to the three different fusion operators, $=, =$, and =$=. +In reality, they're all type-restricted aliases for the same function. The +question of whether they should be type restricted at all is a good debate to +have, but that's not my topic here.

Overview of approaches

With that more general comment out of the way, let's get into the details of +Parser. Leftovers may seem like a really complicated concept, and (at least +to me) lens-based parsing sounds pretty intimidating. However, the core +concepts here are pretty trivial, and I think most people faced with the same +constraints would end up with similar solutions to what the major streaming +data libraries have. To motivate this, let's consider the simplest of all +streaming data: pure lists.

In our case, we'll store our "data producer" (i.e., a list) in a State monad. +Getting another value from the stream (a.k.a., awaiting, drawing) means getting +the list, popping an element off the top, putting back the smaller list, and +returning the popped element. Putting a value back in the stream (a.k.a., +undrawing, leftover) means getting the list, sticking an element at the +beginning, and putting it back. This can all be embodied in very little +Haskell code:

import Control.Monad.Trans.State.Strict
+
+type Parser a r = State [a] r
+
+-- In conduit, this is await
+draw :: Parser a (Maybe a)
+draw = do
+    list <- get
+    case list of
+        [] -> return Nothing
+        x:xs -> do
+            put xs
+            return (Just x)
+
+-- In conduit, this is leftover
+unDraw :: a -> Parser a ()
+unDraw a = do
+    list <- get
+    put $ a:list

At its core, this is what pipes-parse is doing. Instead of a pure list, it's +using a Producer, which is really just a list transformer. But there's +another, slightly less obvious approach that we could take instead. Right now, +we're sticking leftovers right back on the same stack, making it impossible to +distinguish between values taken from the original stream, and values leftover +from the Parser. Instead, we could store a tuple in the State monad: the +original list, and the leftovers. This is also pretty easy to code:

import Control.Monad.Trans.State.Strict
+
+type Parser a r = State ([a], [a]) r
+
+-- In conduit, this is await
+draw :: Parser a (Maybe a)
+draw = do
+    (list, leftovers) <- get
+    case leftovers of
+        x:leftovers' -> do
+            put (list, leftovers')
+            return (Just x)
+        [] ->
+            case list of
+                [] -> return Nothing
+                x:list' -> do
+                    put (list', leftovers)
+                    return (Just x)
+
+-- In conduit, this is leftover
+unDraw :: a -> Parser a ()
+unDraw a = do
+    (list, leftovers) <- get
+    put (list, a:leftovers)

While this certainly works, it seems a little overkill: what possible benefit +is there in having this separation? Well, it would allow us to distinguish +between "completely unparsed values" and "parsed and leftovered" values. In our +discussion so far, and in the documentation for pipes-parse, I see absolutely +no reason why this feature would be relevant. However, let me introduce a +non-trivial parsing example to motivate things a bit further.

An archive file format

Let's say for some (probably bad) reason we've decided that the tar file format +is unsuitable for our purposes. Instead, we've come up with a new file format +that works as follows:

  • Each file consists of a textual filename and binary contents.
  • The filename will be UTF8 encoded.
  • We will encode lengths using a variation on netstrings: the decimal representation of the length followed by a colon.
  • Each file will be encoded as the length of the textual filename, its UTF-8 representation, the length of its binary contents, and the contents.

Yes, this example is ridiculous, but I wanted to find something that would +demonstrate pipes-parse's ability to handle leftover preserving. To make the +above description a bit easier to understand, here's the Haskell code to encode +a list of these files:

data File = File
+    { fileName     :: !Text
+    , fileContents :: !ByteString
+    }
+    deriving Show
+
+encodeFile :: File -> Builder
+encodeFile (File name contents) =
+    tellLen (T.length name) <>
+    fromByteString (TEE.encodeUtf8 name) <>
+    tellLen (S.length contents) <>
+    fromByteString contents
+  where
+    tellLen i = fromByteString $ TEE.encodeUtf8 $ T.pack $ shows i ":"
+
+encodeFiles :: [File] -> Builder
+encodeFiles = mconcat . map encodeFile

What's important for the parsing is that we will need to switch back and forth +between a binary and a textual stream of data in order to handle this +correctly. (Note: in reality, if for some terrible reason you decide to +actually implement this format, you should encode the filename length using the +byte count, not the character count. I specifically used the character +count to force this awkward kind of stream switching.)

I've implemented the parser in conduit if anyone's interested in checking it out. The magic leftover-preserving occurs in the withUtf8 function:

withUtf8 :: MonadThrow m
+         => ConduitM Text o m r
+         -> ConduitM ByteString o m r
+withUtf8 =
+    fuseLeftovers toBS (CT.decode CT.utf8)
+  where
+    toBS = L.toChunks . TLE.encodeUtf8 . TL.fromChunks

We're saying to convert the stream to text assuming a UTF8 encoding. We'll +generate chunks of text on demand (i.e., lazily), and will stop once we hit an +invalid UTF8 sequence (that's the behavior of Data.Conduit.Text). Then, after +downstream is done, collect all of the leftovers that it generated, and convert +them back to their binary representation.

Obviously, in order to do this, we need to be able to distinguish between the +consumed upstream and the leftovers from downstream. If we cannot make that +distinction, we'd be forced to encode the entire stream into text, take out +the text that we need, and then convert the rest of the stream back to +binary. Moreover, we'd have to perform the conversion back and forth for each +time we call withUtf8. And even worse than the performance hit is the fact +that it may not work: if the byte stream contains invalid UTF8 character +sequences, this may break, depending on how your parser handles such invalid +sequences.

I'm fairly certain that pipes-parse falls prey to this issue. (If I've +misunderstood the library, please correct me, and I'll include the correction +here.) conduit handles the issue differently: the "parser" (a.k.a., Sink) +has an explicit command for reporting leftovers, and it's up to the composition +operator to decide how to handle the leftovers. The standard operators- $=, +=$ and =$=- use a similar trick to pipes-parse, and stick the leftovers +onto the upstream Source. And that's precisely why they have the behavior of +discarding downstream leftovers. However, this is just a sensible default, not +a requirement of conduit. It took me under five minutes to write an +alternative composition +function +that had leftover preserving behavior instead.

Simpler example

I tried to make the above example somewhat realistic, but the details may be a +bit overwhelming. Instead, let's consider something much simpler: an +isomorphism between two data types. We want to convert the stream from type A +to type B, perform some peeking, and then deal with the rest of the stream. +An example of doing this with conduit would be:

import           Control.Applicative ((<$>), (<*>))
+import           Data.Conduit        (yield, ($$), (=$=))
+import           Data.Conduit.Extra  (fuseLeftovers)
+import qualified Data.Conduit.List   as CL
+import           Debug.Trace         (traceShow)
+
+newtype A = A Int
+    deriving Show
+newtype B = B Int
+    deriving Show
+
+atob (A i) = traceShow ("atob", i) (B i)
+btoa (B i) = traceShow ("btoa", i) (A i)
+
+main :: IO ()
+main = do
+    let src = mapM_ (yield . A) [1..10]
+    res <- src $$ (,,,)
+        <$> fuseLeftovers (map btoa) (CL.map atob) CL.peek
+        <*> CL.take 3
+        <*> (CL.map atob =$= CL.take 3)
+        <*> CL.consume
+    print res

We have the numbers 1 through 10 as type A. In our Sink, we first convert +the stream to type B, peek, and then return the leftovers upstream. Then we +take three As, again convert to B and take three more elements, and finally +consume the remainder of the stream. I've added trace statements to demonstrate +exactly how many conversions are performed:

("atob",1)
+("btoa",1)
+("atob",4)
+("atob",5)
+("atob",6)
+(Just (B 1),[A 1,A 2,A 3],[B 4,B 5,B 6],[A 7,A 8,A 9,A 10])

The conversions that occur are the absolute bare minimum than could occur: the +first element has to be converted to B in order to be peeked at, and then +converted back to A to return to the original stream. Then, when we later +take three more elements of type B, they obviously need to be converted.

Let's look at the equivalent in pipes-parse, courtesy of Joseph Abrahamson:

{-# LANGUAGE RankNTypes #-}
+import           Control.Applicative
+import           Control.Lens               (Iso', from, iso, view, zoom)
+import           Control.Monad.State.Strict (evalState)
+import           Debug.Trace
+import           Pipes
+import           Pipes.Core                 as Pc
+import qualified Pipes.Parse                as Pp
+import qualified Pipes.Prelude              as P
+
+newtype A = A Int
+    deriving Show
+newtype B = B Int
+    deriving Show
+
+atob (A i) = traceShow ("atob", i) (B i)
+btoa (B i) = traceShow ("btoa", i) (A i)
+
+ab :: Iso' A B
+ab = iso atob btoa
+
+piso :: Monad m => Iso' a b -> Iso' (Producer a m r) (Producer b m r)
+piso i = iso (P.map (view i) <-<) (>-> P.map (view $ from i))
+
+main :: IO ()
+main = do
+  let src = P.map A <-< each [1..10]
+  let parser = (,,,) <$> zoom (piso ab) Pp.peek
+                     <*> zoom (Pp.splitAt 3) Pp.drawAll
+                     <*> zoom (Pp.splitAt 3 . piso ab) Pp.drawAll
+                     <*> Pp.drawAll
+  let res = evalState parser src
+  print res

The result is the same, but look at the traces:

("atob",1)
+("btoa",1)
+("atob",2)
+("btoa",2)
+("atob",3)
+("btoa",3)
+("atob",4)
+("btoa",4)
+("atob",4)
+("atob",5)
+("btoa",5)
+("atob",5)
+("atob",6)
+("btoa",6)
+("atob",6)
+("atob",7)
+("btoa",7)
+("atob",8)
+("btoa",8)
+("atob",9)
+("btoa",9)
+("atob",10)
+("btoa",10)
+(Just (B 1),[A 1,A 2,A 3],[B 4,B 5,B 6],[A 7,A 8,A 9,A 10])

As described above, in pipes-parse you need to convert the entire stream. In +our example, the conversion is trivial, and therefore not too worrying. But in +the case of either an expensive conversion, or a possibly-failing conversion, +this behavior would be incredibly problematic.

UPDATE: Davorak on Reddit helped me come up with a better +example +which demonstrates not just doubled encoding, but a program not completing +correctly. The conduit/pipes comparison code is available as a +Gist.

So my second recommendation would be to tweak Parser to have a stack of +leftovers in addition to a Producer, which would allow for more powerful +leftover preserving

Drop lenses

If you take the two recommendations above, you'll end up with a large +collection of pipes-parse-ready functions, like take and takeWhile. And +each of these functions will maintain a stack of leftovers, either to be used +by the next monadically-composed Parser, or to perhaps propagate upstream. At +this point, the lens-based approach to leftovers is overkill.

The problem with the lens approach is twofold. Firstly, it's difficult for +people to understand. The core concept is not complex, but the machinery around +it obscures its simplicity. But more importantly, it does not seem to compose +well. I don't mean that in the sense of lens or function composition: clearly +those work as expected. What I mean is that you won't be able to take the +existing utility functions I mentioned above and automatically use them in a +leftover-propagating manner.

I'd recommend borrowing from the conduit solution here directly: just have a +separate composition function or operator that returns the downstream +leftovers. It's a simple concept, it's easy to show in a type signature, and +it's easy to build more complex solutions on top of it.

I'll even argue that Gabriel's announcement post is in line with this +recommendation: as pointed out, the lens laws are being bent by pipes-parse. If +there's a separate solution that requires no law bending, why not use that?

Let me be clear: I'm not at all implying that lenses themselves are the problem +here. I think the problem is treating leftover propagation as an isomorphism +between two producers. I think lenses can actually play very nicely with a +streaming data package. I've been working on some conduit data analysis +libraries that make heavy usage of lens to let users write much simpler code +(things like filterField stockDate (< today)).

So to reiterate my third recommendation: provide a composition operator for +dealing with leftovers explicitly, let users reuse your existing library of +functions, and don't force them to try to write isomorphisms on entire streams +where a simple predicate function will suffice.

I hope these ideas come across the right way: ideas that I think would improve +the pipes ecosystem. These ideas should be taken with a large grain of salt. +They are strongly inspired by my experience with conduit, and with the kinds of +work I tend to see conduit applied to.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/02/module-name-conflicts.html b/public/blog/2014/02/module-name-conflicts.html new file mode 100644 index 00000000..e6435102 --- /dev/null +++ b/public/blog/2014/02/module-name-conflicts.html @@ -0,0 +1,37 @@ + Module name conflicts on Hackage +

Module name conflicts on Hackage

February 27, 2014

GravatarBy Michael Snoyman

For some upcoming improvements to FP Haskell Center, I recently added a new +feature to Stackage: the ability to detect module name conflicts. This is where +two different packages both export a module of the same name.

You can see the full module name conflict +list for my most recent build. The +file format is fairly dumb: one line lists all of the packages using a common +module name, and the following line contains all of the module names shared. +(JSON, YAML, or CSV would have been better file formats for this, but one of +the goals of the Stackage codebase is to avoid extra package dependencies +wherever possible.)

Most of these conflicts don't seem problematic at all. The fact that base, +haskell98, haskell2010, and base-compat share a lot of the same module names, +for example, should be expected, and users really do need to choose just one of +those packages to depend on.

Some other cases, on the other hand, might cause issues. For example, both +hashmap and unordered-containers export the Data.HashSet module. This can +negatively affect users of GHCi who have both packages installed and try to +import Data.HashSet. Also, if for some reason a cabal package depended on both, +you'd need to use package imports to disambiguate. There can also be an issue +of confusion: if I see Data.HashSet at the top of a module, it would be nice +to know which package it comes from without having to check a cabal file or +running ghc-pkg.

I'm mostly writing this blog post as I think it's the first time we've had any +kind of collection of this information, and I don't think we've had a community +discussion about conflicting module names. I don't know if the problem is +significant enough to even warrant further analysis, or how have thoughts on +how to proceed if we do want to try and disambiguate module names.

Here are some of the conflicting module names, and the packages using them:

  • System.FilePath.Glob
    • Glob
    • filemanip
  • Test.Framework
    • HTF
    • test-framework
  • Control.Monad.Trans.List
    • List
    • transformers
  • Control.Monad.CatchIO
    • MonadCatchIO-mtl
    • MonadCatchIO-transformers
  • Test.QuickCheck.Instances
    • checkers
    • quickcheck-instances
  • Crypto.Random
    • crypto-api
    • crypto-random
  • Data.HashSet
    • hashmap
    • unordered-containers
  • Language.Haskell.Lexer
    • haskell-lexer
    • haskell-src
  • Test.Hspec
    • hspec
    • nanospec
  • Data.String.UTF8
    • hxt-unicode
    • utf8-string

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/02/new-warp.html b/public/blog/2014/02/new-warp.html new file mode 100644 index 00000000..84861bd6 --- /dev/null +++ b/public/blog/2014/02/new-warp.html @@ -0,0 +1,126 @@ + Improving the performance of Warp again +

Improving the performance of Warp again

February 3, 2014

GravatarBy Kazu Yamamoto

Improving the performance of Warp again

As you may remember, I improved the performance of Warp in 2012 and wrote some blog articles on this web site. Based on these articles, Michael and I wrote an article "Warp" for POSA (Performance of Open Source Applications).

After working with Andreas last year, I came up with some new ideas to yet again improve Warp's performance, and have implemented them in Warp 2.0. In this article, I will explain these new techniques. If you have not read the POSA article, I recommend to give a look at it beforehand.

Here are the high level ideas to be covered:

  • Better thread scheduling
  • Buffer allocation to receive HTTP requests
  • Buffer allocation to send HTTP responses
  • HTTP request parser

witty

When I was testing multicore IO manager with Mighty (on Warp), I noticed bottlenecks of network programs complied by GHC. To show such bottlenecks actually exist, I wrote a server program called witty based on Andreas's "SimpleServer".

"witty" provides seven command line options to switch standard methods to alternatives. My experiment with "witty" disclosed the following bottlenecks:

  • Thread scheduling (the "-y" option)
  • Network.Socket.ByteString.recv (the "-r" option)

Better thread scheduling

GHC's I/O functions are optimistic. +For instance, recv tries to read incoming data immediately. +If there is data available, recv succeeds. +Otherwise, EAGAIN is returned. +In this case, recv asks the IO manager to notify it when data is available.

If a network server repeats receive/send actions, +recv just after send will usually fail because +there is a time lag for the next request from the client. +Thus the IO manager is often required to do notification work. +Here is a log of "strace":

recvfrom(13, )                -- Haskell thread A
+sendto(13, )                  -- Haskell thread A
+recvfrom(13, ) = -1 EAGAIN    -- Haskell thread A
+epoll_ctl(3, )                -- Haskell thread A (a job for the IO manager)
+recvfrom(14, )                -- Haskell thread B
+sendto(14, )                  -- Haskell thread B
+recvfrom(14, ) = -1 EAGAIN    -- Haskell thread B
+epoll_ctl(3, )                -- Haskell thread B (a job for the IO manager)

To achieve better scheduling, we need to call yield after send. +yield pushes its Haskell thread onto the end of thread queue. +In the meanwhile, other threads are able to work. +During the work of other threads, a new request message can arrive, +and therefore the next call to recv will succeed. +With the yield hack, a log of "strace" becames as follows:

recvfrom(13, )                -- Haskell thread A
+sendto(13, )                  -- Haskell thread A
+recvfrom(14, )                -- Haskell thread B
+sendto(14, )                  -- Haskell thread B
+recvfrom(13, )                -- Haskell thread A
+sendto(13, )                  -- Haskell thread A

In other words, yield makes the IO manager work less frequently. +This magically improves throughput! +This means that even multicore IO manager still has unignorable overhead. +It uses MVar to notify data availability to Haskell threads. +Since MVar is a lock, it may be slow. +Or perhaps, allocation of the MVar is slow.

Unfortunately when I first added yield to Warp, no performance improvement were +gained. It seems to me that Monad stack (ResourceT) handcuffs the yield +hack. So, Michael removed ResourceT from WAI. This is why the type of +Application change from:

type Application = Request -> ResourceT IO Response

to:

type Application = Request -> IO Response

This change itself improved the performance and enables the yield hack resulting in drastic performance improvement of Warp at least on small numbers of cores.

Michael has added an explanation of the removal of ResourceT to the end of this post.

Buffer allocation to receive HTTP requests

Warp used Network.Socket.ByteString.recv to receive HTTP requests. It appeared that this function has significant overhead. Let's dig its definition deeply:

recv :: Socket -> Int -> IO ByteString
+recv sock nbytes = createAndTrim nbytes $ recvInner sock nbytes

As you can see, recv calls createAndTrim to create ByteString. Its definition is:

createAndTrim :: Int -> (Ptr Word8 -> IO Int) -> IO ByteString
+createAndTrim l f = do
+    fp <- mallocByteString l
+    withForeignPtr fp $ \p -> do
+        l' <- f p
+        if assert (l' <= l) $ l' >= l
+            then return $! PS fp 0 l
+            else create l' $ \p' -> memcpy p' p l'

Suppose we specify 4,096 as a buffer size to recv. First a ByteString of 4,096 bytes is created by mallocByteString. If the size of a received request is not 4,096, another ByteString is created by create and memcpy. create also calls mallocByteString as follows:

create :: Int -> (Ptr Word8 -> IO ()) -> IO ByteString
+create l f = do
+    fp <- mallocByteString l
+    withForeignPtr fp $ \p -> f p
+    return $! PS fp 0 l

So, let's understand what happens when mallocByteString is called. Its definition is as follows:

mallocByteString :: Int -> IO (ForeignPtr a)
+mallocByteString = mallocPlainForeignPtrBytes

GHC.GHC.ForeignPtr provides mallocPlainForeignPtrBytes:

mallocPlainForeignPtrBytes :: Int -> IO (ForeignPtr a)
+mallocPlainForeignPtrBytes (I# size) = IO $ \s ->
+    case newPinnedByteArray# size s      of { (# s', mbarr# #) ->
+       (# s', ForeignPtr (byteArrayContents# (unsafeCoerce# mbarr#))
+                         (PlainPtr mbarr#) #)
+     }

As you can see, mallocPlainForeignPtrBytes calls newPinnedByteArray# which allocates a pinned object. Pinned objects are not moved by GHC's copy GC. That's why they are called pinned.

Substance of newPinnedByteArray# is rts/PrimOps.cmm:stg_newPinnedByteArrayzh which calls allocatePinned. allocatePinned is implemented in C in the file of "rts/sm/Storage.c":

StgPtr
+allocatePinned (Capability *cap, W_ n)
+{
+    StgPtr p;
+    bdescr *bd;
+
+    // If the request is for a large object, then allocate()
+    // will give us a pinned object anyway.
+    if (n >= LARGE_OBJECT_THRESHOLD/sizeof(W_)) {
+        p = allocate(cap, n);
+        Bdescr(p)->flags |= BF_PINNED;
+        return p;
+    }
+    ...

allocatePinned calls allocate if "n >= LARGE_OBJECT_THRESHOLD/sizeof(W_)" is met. Here is a part of the definition of allocate:

StgPtr
+allocate (Capability *cap, W_ n) {
+...
+        ACQUIRE_SM_LOCK
+        bd = allocGroup(req_blocks);
+        dbl_link_onto(bd, &g0->large_objects);
+        g0->n_large_blocks += bd->blocks; // might be larger than req_blocks
+        g0->n_new_large_words += n;
+        RELEASE_SM_LOCK;
+...

So, allocate uses a global lock. To my calculation, LARGE_OBJECT_THRESHOLD/sizeof(W_) is:

  • 3,276 bytes (819 words) on 32 bit machines
  • 3,272 bytes (409 words) on 64 bit machines

Caution: the numbers above were changed according to a comment from Simon Marlow.

Since old Warp specified 4,096 to recv, a global lock is acquired for every HTTP request. If the size of an HTTP request is some between 3,272(3,276) and 4,095, another global lock is obtained.

To avoid contention, I modified Warp so that a buffer of 4,096 bytes is allocated by malloc() for every HTTP connection. Sophisticated malloc() implementations have arena to avoid global contention. Also, since we repeat malloc() and free() for the same size, we can take advantage of the free list in malloc().

The buffer is passed to the recv() system call. After an HTTP request is received, a ByteString is allocated by mallocByteString and data is copied by memcpy. This tuning also improved the throughput of Warp drastically.

Buffer allocation to send HTTP response

Michael and I noticed that the buffer for receiving can also be used for sending. Let's recall that Response has three constructors:

data Response
+    = ResponseFile H.Status H.ResponseHeaders FilePath (Maybe FilePart)
+    | ResponseBuilder H.Status H.ResponseHeaders Builder
+    | ResponseSource H.Status H.ResponseHeaders (forall b. WithSource IO (C.Flush Builder) b)

We changed that the buffer is also used for ResponseBuilder and ResponseSource to avoid extra buffer allocations. (In the case of ResponseFile, the zero copy system call, sendfile(), ensures no extra buffer is allocated.)

HTTP request parser

At this stage, I took profiles of Mighty. Here is a result:

sendfileloop                    Network.Sendfile.Linux                    7.5    0.0
+parseRequestLine                Network.Wai.Handler.Warp.RequestHeader    3.6    5.8
+sendloop                        Network.Sendfile.Linux                    3.6    0.0
+serveConnection.recvSendLoop    Network.Wai.Handler.Warp.Run              3.1    1.9
+>>=                             Data.Conduit.Internal                     2.9    3.9

I persuaded my self that I/O functions are slow. But I could not be satisfied with the poor performance of the HTTP request parser. parseRequestLine was implemented by using the utility functions of ByteString. Since they have overhead, I re-wrote it with Ptr-related functions. After writing the low-level parser, the profiling became:

sendfileloop                  Network.Sendfile.Linux                    8.3    0.0
+sendResponse                  Network.Wai.Handler.Warp.Response         3.7    3.1
+sendloop                      Network.Sendfile.Linux                    3.2    0.0
+>>=                           Data.Conduit.Internal                     2.9    4.0
+serveConnection.recvSendLoop  Network.Wai.Handler.Warp.Run              2.6    2.0

I was happy because parseRequestLine disappeared from here. One homework for me is to understand why sendfileloop is so slow. Probably I need to check if locks are used in sendfile(). If you have any ideas, please let me know.

Performance improvement

So, how fast Warp became actually? I show a chart to compare throughput among Mighty 2 complied by GHC 7.6.3, Mighty 3 compiled by coming GHC 7.8, and nginx 1.4.0. Note that only one core is used. I have two reasons for this: 1) since the change of data center of our company, I cannot use the environment described in the POSA article at this moment. So, I need to draw this chart based on my old memo. 2) nginx does not scale at all in my environment even if the deep sleep mode is disabled.

Anyway, here is the result measured by weighttp -n 100000 -c 1000 -k:

Fig1: Throughput on one core

Removing ResourceT from WAI

Before we can understand how we removed ResourceT from WAI, we need to understand its purpose. +The goal is to allow an Application to acquire a scarce resource, such as a database connection. +But let's have a closer look at the three response constructors used in WAI (in the 1.4 series):

data Response
+    = ResponseFile H.Status H.ResponseHeaders FilePath (Maybe FilePart)
+    | ResponseBuilder H.Status H.ResponseHeaders Builder
+    | ResponseSource H.Status H.ResponseHeaders (C.Source (C.ResourceT IO) (C.Flush Builder))

Both ResponseFile and ResponseBuilder return pure values which are unable to take +advantage of the ResourceT environment to allocate resources. +In other words, the only constructor which takes advantage of ResourceT is ResponseSource. +Our goal, therefore, was to limit the effect of resource allocation to just that constructor, +and even better to make the resource environment overhead optional in that case.

The first step is what Kazu already mentioned above: changing Application to live in IO instead of ResourceT IO. +We need to make the same change to the ResponseSource constructor. +But now we're left with a question: how would we acquire a resource inside a streaming response +in an exception safe manner?

Let's remember that an alternative to ResourceT is to use the bracket pattern. +And let's look at the structure of how a streaming response is processed in Warp. +(A similar process applies to CGI and other backends.)

  1. Parse the incoming request, generated a Request value.
  2. Pass the Request value to the application, getting a Response value.
  3. Run the Source provided by the application to get individual chunks from the Response and send them to the client.

In order to get exception safety, we need to account for three different issues:

  • Catch any exceptions thrown in the application itself while generating the +Response. This can already be handled by the application, since it simply +lives in the IO monad.

  • Mask asynchronous exceptions between steps 2 and 3. This second point +required a minor change to Warp to add async masking.

  • Allow the application to install an exception handler around step 3.

That third point is the bit that required a change in WAI. +The idea is to emulate the same kind of bracket API provided by functions like withFile. +Let's consider the signature for that function:

withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
+
+-- or partially applied
+withSomeFile "foo" ReadMode :: (Handle -> IO a) -> IO a

In our case, we don't want to get a Handle, but instead a Source of bytes. +Let's make a type signature or two to represent this idea:

type ByteSource = Source IO (Flush Builder)
+type WithByteSource a = (ByteSource -> IO a) -> IO a

And the resulting ResponseSource constructor is:

ResponseSource H.Status H.ResponseHeaders (forall b. WithByteSource b)

To deal with the common case of installing a cleanup function, WAI provides the responseSourceBracket function. +At a higher level, Yesod is able to build on top of this to provide the same ResourceT functionality we had +previously, so that a Yesod user can simply use allocate and friends and get full exception safety.

Acknowledgment

Michael and I thank Joey Hess for getting Warp to work well on Windows and +Gregory Collins for his discussions on performance.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/03/efficient-directory-traversals.html b/public/blog/2014/03/efficient-directory-traversals.html new file mode 100644 index 00000000..48c187a5 --- /dev/null +++ b/public/blog/2014/03/efficient-directory-traversals.html @@ -0,0 +1,40 @@ + Efficient directory traversals +

Efficient directory traversals

March 20, 2014

GravatarBy Michael Snoyman

I've just released a new version of +conduit-combinators +which adds two new functions: sourceDirectory and sourceDirectoryDeep. The +former lists all of the children of a given directory, and the latter traverses +deeply into a directory tree and lists all of the files present.

To see how this is used, consider the following simple example, which prints +out the total number of files in the current directory tree. (Note: the False +parameter means not to traverse symlinks to directories.)

{-# LANGUAGE OverloadedStrings #-}
+import Conduit
+
+main :: IO ()
+main = runResourceT (sourceDirectoryDeep False "." $$ lengthC) >>= print

Note that this is equivalent to running find . -type f | wc -l.

This new function supersedes Data.Conduit.Filesystem.traverse, which uses +more memory. To give you an idea of the difference, for a directory structure +with 3361 files, traverse uses a maximum residency of 957KB, whereas +sourceDirectoryDeep uses a maximum of 48KB.

In the implementation of traverse, the entire contents of a directory are +read into memory as a list, and then each entry is analyzed one at a time. If +the entry is a file, it is yielded, and then can be garbage collected. But if +the entry is a directory, that directory must then be traversed, at which point +both the remaining contents from the parent directory, and the contents of +the new directory, are in memory simultaneously. By contrast, in +sourceDirectoryDeep, only a single file path is read into memory at a given +time.

Even if you're just doing shallow traversals, you can get a memory improvement +by using sourceDirectory instead of getDirectoryContents.

Deprecating filesystem-conduit

At this point, I'm also deprecating filesystem-conduit, as all of its +functionality is represented in conduit-combinators. I'm actually hoping to +consolidate a few other packages over the coming weeks in an effort to simplify +dependency trees a bit. I'll post on the subject when I have some more concrete +plans.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/03/gsoc-proposals.html b/public/blog/2014/03/gsoc-proposals.html new file mode 100644 index 00000000..ac4eeb36 --- /dev/null +++ b/public/blog/2014/03/gsoc-proposals.html @@ -0,0 +1,32 @@ + Google Summer of Code proposals +

Google Summer of Code proposals

March 15, 2014

GravatarBy Greg Weber

Here are some ideas for GSoC proposals that I think are good matches for the constraints and goals of Google Summer of Code and Haskell.org

Sorry for making this post late, I just realized we were 5 days from the GSoc deadline!

Make Template Haskell visible

There are critics of Tempalte Haskell, but it is often difficult to find constructive critiques from them. +In a conversation with a new team member we were able to figure out that a big problem with +Template Haskell for him is that it invisibly generates code. +If the template Haskell generates identifiers then you can no longer just grep for those identifiers. +You can turn on -ddump-splices, but viewing code generated that way is generally a horrible process. +I started a ghc proposal to simply instead output the code to a file.

So if you have Foo.hs it would generate a file Foo.th.hs

Now when you grep for identifiers, they will show up, and you can see all the generated code.

My estimation may be wildly inaccurate, but I think the just above will not take that long. +But that is a good thing for GSoC, to have an easy to achieve first goal that will immediately benefit the community. +I think there are enough directions to take this project to keep the summer busy.

  • There are other sources of code generation such as deriving: these could also be added.
  • Making sure IDEs, and text editors can find the generated code and match it to their source location
  • Shipping the generated code and using it is an interesting idea to explore. This could help with cross-compiling and marking packages as Safe.

Improve the Persistent (Database serialization) library

Persistent is relied on by a lot of application developers, and it could definitely be improved. +We already started work on Persistent 2.0, which will finally have flexible key support. +There are so many things that could be improved with persistent, please talk with us to nail down a more concrete proposal.

Make a Haskell development mode

This is a continuation of a successful GSoC project from 2 years ago which created the fsnotify library now used by the community. +The idea has always been to take things further and provide the ability to automatically recompile Haskell projects as files are edited, +and figuring out how to do this as quickly possible through plugins or the GHC API. +We already have yesod devel for yesod, but we want to improve this process and generalize it so it can be used effectively on any Haskell project.

I also want to give a shout out to an existing proposal for faster Cabal/GHC parallel builds +A development mode helps manage the development compilation cycle to keep that process efficient, but ultimately the compilcation process needs to be as fast as possible.

Notes on applying to GSoC

The best thing you can do is find your project mentors ahead of time. +This requires starting your application process today rather than the night before the hard deadline. +Right now there are 3 possible proposals here, but only 2 mentors available. +I will help look for more mentors in the community to match interest from students. +A single student can make multiple proposals.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/03/network-conduit-async.html b/public/blog/2014/03/network-conduit-async.html new file mode 100644 index 00000000..4da9fede --- /dev/null +++ b/public/blog/2014/03/network-conduit-async.html @@ -0,0 +1,174 @@ + network-conduit, async, and conduit-combinators +

network-conduit, async, and conduit-combinators

March 5, 2014

GravatarBy Michael Snoyman

I got a request to write up some examples of using +network-conduit for +server and client apps. I'm going to try to cover a few of the examples +requested. I'm also going to be demonstrating some usage of the new +conduit-combinators +library, as well as Simon Marlow's +async package. If you want to test +this out locally, start off by running:

cabal install conduit-combinators network-conduit async word8

network-conduit provides some functions for writing network servers and clients +using conduit. There's support for TCP, UDP, and Unix sockets, and with +network-conduit-tls there's support for SSL/TLS connections. We'll just use +plain TCP in this post.

Server 1: ALL CAPS

The first thing we'll do is write a server that listens on port 4000, and echos +back to the client whatever it transmitted, but upper-cased. The code is +incredibly short, let's just jump into it and then hit the explanations:

{-# LANGUAGE OverloadedStrings #-}
+import           Conduit
+import           Data.Conduit.Network
+import           Data.Word8           (toUpper)
+
+main :: IO ()
+main = runTCPServer (serverSettings 4000 "*") $ \appData ->
+    appSource appData $$ omapCE toUpper =$ appSink appData

runTCPServer takes two parameters. The first is the server settings, which +indicates how to listen for incoming connections. Our two parameters say to +listen on port 4000 and that the server should answer on all network +interfaces. The second parameter is an Application, which takes some +AppData and runs some action. Importantly, our app data provides a Source +to read data from the client, and a Sink to write data to the client. +(There's also information available such as the SockAddr of the client.)

The next line is a very trivial conduit pipeline: we take all data from the +source, pump it through omapCE toUpper, and send it back to the client. +omapCE is our first taste of conduit-combinators: omap means we're doing a +monomorphic map (on a ByteString), and C means conduit, and E means "do +it to each element in the container."

Go ahead and run that code and telnet to port 4000. Everything you type in +should COME BACK LOOKING ANGRY.

Server 2: doubled characters

Let's try another simple server (mostly because we need two servers to try out +a later example). Again, let's just jump in.

{-# LANGUAGE OverloadedStrings #-}
+import           Conduit
+import           Data.ByteString      (pack)
+import           Data.Conduit.Network
+
+main :: IO ()
+main = runTCPServer (serverSettings 4001 "*") $ \appData ->
+    appSource appData
+        $$ concatMapCE (\w -> pack [w, w])
+        =$ appSink appData

The only changes here are (1) listen on port 4001 instead of 4000, and (2) +instead of upper casing, we want to duplicate each incoming byte. Again, we're +following the same naming scheme of CE to indicate "conduit, element-wise."

Client: telnet

Now let's write a client. We want our client to perform the same job as our +telnet client: connect to the server we just wrote, pipe all input from stdin +to the server, and send all output to stdout:

{-# LANGUAGE OverloadedStrings #-}
+import           Conduit
+import           Control.Concurrent.Async (concurrently)
+import           Control.Monad            (void)
+import           Data.Conduit.Network
+
+main :: IO ()
+main =
+    runTCPClient (clientSettings 4000 "localhost") $ \server ->
+        void $ concurrently
+            (stdinC $$ appSink server)
+            (appSource server $$ stdoutC)

Instead of runTCPServer and serverSettings, we're now using runTCPClient +and clientSettings. But we're still dealing with the same Application type, +and therefore the same ability to get access to our source and sink separately.

To get our standard input, we'll use stdinC, which is equivalent to +sourceHandle stdin. We want to connect that to appSink server. Similarly, +we need to connect appSource server to stdoutC. But the important bit is +doing this in two separate threads. Remember that it's entirely possible that a +server could generate output without corresponding input from our application.

To handle these semantics correctly, we pull in the async package, and in +particular, the concurrently function. This function forks each child action +into a separate thread, and blocks until both actions complete. If either +thread throws an exception, then the other is terminated and the exception is +rethrown. This provides exactly the behavior we need.

Client: data pipeline

Now let's get a bit more complicated. We still want to get input from the user, +but now send the output from the first server to a second server, and then send +output from that server to standard output. This is actually not much worse +than what we had previously:

{-# LANGUAGE OverloadedStrings #-}
+import           Conduit
+import           Control.Applicative      ((*>))
+import           Control.Concurrent.Async (Concurrently (..))
+import           Data.Conduit.Network
+
+main :: IO ()
+main =
+    runTCPClient (clientSettings 4000 "localhost") $ \server1 ->
+    runTCPClient (clientSettings 4001 "localhost") $ \server2 ->
+        runConcurrently $
+            Concurrently (stdinC $$ appSink server1) *>
+            Concurrently (appSource server1 $$ appSink server2) *>
+            Concurrently (appSource server2 $$ stdoutC)

We now call runTCPClient twice, once for each server. And we need three +threads instead of two, since there are three different connections going on. +We could just make two calls to concurrently, but async provides a very +convenient newtype wrapper- Concurrently- which lets us use its applicative +instance to run all three of our threads at the same time.

Server and client: proxy

Now our final example. We'd like to run a proxy server. It will accept incoming +connections, connect to a server, transmit all input from the client to the +server, and all output from the server to the client. (Bonus points: try to +implement this without looking at the example below.)

{-# LANGUAGE OverloadedStrings #-}
+import           Conduit
+import           Control.Concurrent.Async (concurrently)
+import           Control.Monad            (void)
+import           Data.Conduit.Network
+
+main :: IO ()
+main =
+    runTCPServer (serverSettings 4002 "*") $ \client ->
+    runTCPClient (clientSettings 4000 "localhost") $ \server -> void $ concurrently
+        (appSource server $$ appSink client)
+        (appSource client $$ appSink server)

One important thing here is the order of the calls to runTCPServer and +runTCPClient. The way we've set it up, we first start listening for incoming +connections. Then, for each new incoming connection, we create a new connection +to the server. In other words, there will be as many connections to the server +as incoming client connections. This is the right way to implement a proxy, +though there are possibly other use cases where you'd want to share server +connections across multiple incoming clients.

Other than that bit, everything else should be familiar. You should be able to +connect to 4002 and talk to OUR ANGRY MAKING SERVER.

Proxy with authentication

All of the examples so far have involved a single operation on our streams of +data: sending all of it to a server, upper casing every byte, etc. Let's look +at something a bit more complicated: an authenticating proxy. In this case, the +proxy will challenge the user for a username and password, the user will enter +them, and if they are valid, the proxy session will begin.

{-# LANGUAGE OverloadedStrings #-}
+import           Conduit
+import           Control.Concurrent.Async (concurrently)
+import           Control.Monad            (void)
+import           Data.ByteString          (ByteString)
+import           Data.Conduit.Network
+import           Data.Word8               (_cr)
+
+creds :: [(ByteString, ByteString)]
+creds =
+    [ ("spaceballs", "12345")
+    ]
+
+checkAuth :: Conduit ByteString IO ByteString
+checkAuth = do
+    yield "Username: "
+    username <- lineAsciiC $ takeCE 80 =$= filterCE (/= _cr) =$= foldC
+    yield "Password: "
+    password <- lineAsciiC $ takeCE 80 =$= filterCE (/= _cr) =$= foldC
+    if ((username, password) `elem` creds)
+        then do
+            yield "Successfully authenticated.\n"
+        else do
+            yield "Invalid username/password.\n"
+            error "Invalid authentication, please log somewhere..."
+
+main :: IO ()
+main =
+    runTCPServer (serverSettings 4003 "*") $ \client -> do
+        (fromClient, ()) <- appSource client $$+ checkAuth =$ appSink client
+        runTCPClient (clientSettings 4000 "localhost") $ \server ->
+            void $ concurrently
+                (appSource server $$ appSink client)
+                (fromClient $$+- appSink server)

creds is just a simple collection of valid username/password combos. +checkAuth is where most of our magic happens. First notice its type +signature: it's a Conduit from ByteStrings (client input) to ByteStrings +(output to client). We could alternatively pass around the client Sink +explicitly, but this is more convenient. To say something to the client, we +simply yield.

We want to get a line of input data from the user. Let's look at the code more +closely:

username <- lineAsciiC $ takeCE 80 =$= filterCE (/= _cr) =$= foldC

There are a few things we're doing here:

  • The lineAsciiC combinator streams an entire line to the provided consumer. It ensures that, even if the consumer doesn't take all of the bytes, they will be flushed.
  • To prevent a memory exhaustion attack, we only keep up to 80 bytes of input.
  • lineAsciiC automatically strips out trailing line feeds, but does not strip out carriage returns. (Note: I might change that in a future release of conduit-combinators.) So we use filterCE to drop it.
  • foldC consumes the incoming stream of ByteStrings and folds them together into a single ByteString.

We use the same logic for getting the password, and then test if the +username/password is in creds. If it is, we give a successful message. +Otherwise, we give the user an error message and throw an exception to close +the connection.

The important change to main is the usage of connect-and-resume (the $$+ +operator):

(fromClient, ()) <- appSource client $$+ checkAuth =$ appSink client

This allows us to consume some of the input from the client, and then resume +the consumption later with a totally different consumer. This is highly +convenient: instead of needing to put our authentication logic into the same +consumer as the proxy, we can keep things nicely seperated. In order to resume +consumption, we need to use the $$+- operator:

(fromClient $$+- appSink server)

That's all I've got for the moment. If there are points that are unclear, +please let me know. I'd like to make this blog post the official tutorial for +network-conduit.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/03/package-consolidation.html b/public/blog/2014/03/package-consolidation.html new file mode 100644 index 00000000..bab10df4 --- /dev/null +++ b/public/blog/2014/03/package-consolidation.html @@ -0,0 +1,105 @@ + Package consolidation +

Package consolidation

March 25, 2014

GravatarBy Michael Snoyman

A few weeks ago, there was a pretty heated debate about the PVP on the +libraries mailing +list. +I've seen a few different outcomes from that discussion. One is to reduce +dependency footprints to try and avoid some of the dependency problems. +(Another one is concrete proposals for changes to the PVP; I intend to post a +proposal as well in the coming days, but wanted to get the easier stuff out of +the way first.)

This blog post is about some plans I have for consolidating multiple packages +together, hopefully to result in simpler dependency trees without causing users +to have unneeded package dependencies- at least not too often. The reason I'm +writing up this blog post is to let the community know about these changes in +advance, and let me know if any of these changes will cause them problems. +Also, if I list a package as deprecated, and you'd like to take over +maintainership, please let me know.

One of the guiding principles I've used in setting this up is that I belive +some dependencies should be considered incredibly cheap. Depending on +process, for example, is a cheap dependency, since it comes with GHC. (Unless +you place restrictive bounds on process, which can instead cause dependency +problems.) For more information on these principles, please read my +description.

I'll start at the bottom of the dependency tree and build up.

streaming-commons, zlib-bindings, text-stream-decode, conduit and pipes

Currently, there are about six core conduit packages: conduit, zlib-conduit, +attoparsec-conduit, etc. In addition, for some of these packages, I've split +off helper packages providing functionality to be used by other streaming data +libraries as well, such as zlib-bindings and text-stream-decode.

I want to collapse that into just three packages. All of the streaming helpers +will end up in a new package, +streaming-commons. I've talked +with Gabriel Gonzalez about this, and we'll be collaborating together on +creating a high-quality, low-dependency library. This library will also include +some features currently baked into conduit itself, like lock-free Windows file +reading and directory traversals.

All of the conduit core packages on top of that would then be merged into a new +package, conduit-extra. So we'd end up with conduit, conduit-extra, and +streaming-commons. The only downside is that, if you only needed zlib support, +you'll now get a few extra packages as well. However, following the principle I +listed above, these extra dependencies should all be coming from the "basically +free" dependency category.

Crazier ideas for streaming-commons

This may be taking the idea too far, but we could include some even more +advanced tooling in streaming-commons. This could include not only the data +types from xml-types and json-types- which provide both streaming and tree +based data structures for those data formats- but also attoparsec parsers and +blaze-builder renderers. This could allow quite a bit of the xml-conduit +codebase to be shared by the pipes world, for example.

I'm curious if people think this is a cool idea, or too radical (or both!).

Deprecate failure, attempt, and safe-failure

This one's been on my list for a while, pending some details being worked out +with Edward Kmett. The goal is to completely deprecate failure and attempt in +favor of the exceptions package, and within exceptions split out MonadThrow +from MonadCatch.

This will also mean removing some redundant functionality from resourcet. It +will be nice to be rid of the custom MonadThrow and MonadUnsafeIO defined +there.

http-client-*

A few simple moves: merge http-client-multipart into http-client, and merge +http-client-conduit into http-conduit. The latter change will mean that it's a +bit more difficult to use http-client in conduit without depending on tls, but +that's a use case anyone has expressed interest to me in.

Another change I'm planning to do at the same time is to add a new module to +http-conduit, with an alternate API. There are a few places where I'm +dissatisfied with the current API, and this module will work as an experimental +next-gen http-conduit. I'm planning on keeping both versions of the API around +for quite a while for backwards compatibility, however. The changes I'm looking +at are:

  • Avoiding ResumableSource
  • Using with-semantics (like http-client) to avoid accidentally keeping connections open.
  • Don't bake in the need for ResourceT
  • Possibly use lenses for field modifiers on the Request data type.

shakespeare

This change is pretty simple: collapse shakespeare, shakespeare-css, +shakespeare-js, shakespeare-text, shakespeare-i18n, and hamlet into a single +package. It made sense to keep these separate when APIs were changing rapidly, +but things are basically stable now.

wai

Since they don't add any extra dependencies, I'd like to merge wai-test and +wai-eventsource into wai-extra. Once again, since we're dealing with stable +APIs, this shouldn't cause too much trouble.

I'm also considering deprecating wai-handler-devel, since it's no longer used +at all by yesod devel.

Deprecate pool-conduit

pool-conduit used to be a resource pool implementation based on code in conduit. Since then:

  • The actual resource pool implementation is provided by resource-pool.
  • The code I used from conduit has moved to resourcet.

At this point, pool-conduit doesn't really have much in it. If there's code +that people are using from it, I'd like to get it merged into resource-pool +itself.

yesod

The next iteration of Yesod will have a significantly simpler dispatch +system. +This new code doesn't really make sense as a general-purpose routing tool, so +I'm planning on moving that code into yesod-core itself and deprecate +yesod-routes. I know there are other users of yesod-routes; I think it makes +sense to rename yesod-routes to do something like merging yesod-routes into +wai-routes, as yesod-routes has no Yesod-specifics in it.

Another minor change: merge yesod-eventsource into yesod-core. No extra dep, +and a stable API.

Finally, the big (and somewhat controversial) one: merge most of the yesod-* +core packages into the yesod package itself. A number of year ago, we did +precisely the opposite. However, given API stability, I think the time has come +to simplify our dependency list again here. This will have the added benefit +that when a user reports "I'm using Yesod version 1.2.3", it will give us more +information.

xml

I'm planning on deprecating dtd, uri-conduit, and xml-catalog, as I don't use +them any more and have no time to maintain them.

Another idea I'm playing with is merging html-conduit into xml-conduit. This +would add a tagstream-conduit dependency. Alternatively, perhaps +tagstream-conduit could be merged into xml-conduit as well.

classy-prelude

Merge classy-prelude-conduit in with classy-prelude. Downside: classy-prelude +will depend on conduit-combinators, but that doesn't actually add more than 3 +extra packages.

Next step: trimming dependencies in general

I'm not planning on rushing any of these changes. The goal is to take them +slowly and methodically, and release changes incrementally. For example, after +the conduit changes are done, I'd release new versions of wai, yesod, etc, that +are compatible with both the new and old versions. Hopefully the user facing +changes will simply be tweaking import lists and cabal files, but users will be +able to take their time on this.

Ultimately, I'm planning on releasing new version of persistent (2.0) and Yesod +(1.4). You can see the Persistent 2.0 +goals. The +Yesod 1.4 release doesn't actually have any breaking changes planned, aside +from upgrading its dependencies.

One other thing I'm going to be doing in this process is a more general +trimming down of dependencies. I'll be going through yesod-platform to find +packages we don't need. I also want to avoid redundant packages if possible +(e.g., we only need one cryptographic hash packages). In many cases, as a +community, we have multiple package options available. I think a great move for +the community would be to start consolidating those options as well where it +makes sense. If I have concrete ideas on that, I'll certainly share them.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/03/wai-yesod-websockets.html b/public/blog/2014/03/wai-yesod-websockets.html new file mode 100644 index 00000000..b7946d87 --- /dev/null +++ b/public/blog/2014/03/wai-yesod-websockets.html @@ -0,0 +1,98 @@ + WAI 2.1 and yesod-websockets +

WAI 2.1 and yesod-websockets

March 9, 2014

GravatarBy Michael Snoyman

I'm happy to announce a new 2.1 release of +WAI and +Warp, version 0.3.1 of +http-reverse-proxy, +and the release of a brand new package: +yesod-websockets. These +releases are all connected, so let's start off by describing the major addition +to WAI 2.1.

Primary change: responseRaw

HTTP is built around a request/response pair, and WAI has until now followed +that strictly. The three response types allowed (builder, file, and source) all +worked around the idea of first consuming a request, then returning a response. +For normal HTTP requests, this is adequate. But there are two areas (that I'm +aware of at least) where this falls short:

  • Implementing proxy servers, where the CONNECT request method requires +upgrading to full-duplex communication.
  • Using WebSockets, which again use full duplex communication.

To work around this limitation, Warp provides a settingsIntercept, which +allows a special handler to intercept some requests and take control away from +the normal WAI request/response pairs. I took this approach initially because +many WAI handlers have no means of properly supporting full duplex +communications (e.g., CGI). However, this split between normal and non-normal +handling makes it very awkward to actually use WebSockets.

So starting with WAI 2.1, we have a fourth response type: responseRaw. Its type is:

responseRaw
+    :: (C.Source IO B.ByteString -> C.Sink B.ByteString IO () -> IO ())
+    -> Response
+    -> Response

The first parameter is a "raw application:" it takes a Source of all data +coming from the client, a Sink for sending data to the client, and performs +an action with them. The second parameter is a backup response for WAI handlers +which do not support responseRaw; usually this will just be a message like +"Your server doesn't support WebSockets."

With this change the old settingsIntercept from Warp is no longer necessary, +and the behavior can be achieved from inside normal WAI applications. This has +resulted in a number of changes.

wai-websockets 2.1

The wai-websockets package has been available for over two years now, thanks to +Jasper Van der Jeugt's websockets library. However, until now, it's provided a +slightly strange API to work with settingsIntercept:

intercept :: ConnectionOptions -> ServerApp -> Request -> Maybe (Source IO ByteString -> Connection -> IO ())

ConnectionOptions and ServerApp are both part of the websockets library +itself. Request is a WAI request, and it returns a Just value if the +request represents a proper WebSockets request. Starting with version 2.1, +there's a much simpler API:

websocketsApp :: ConnectionOptions -> ServerApp -> Request -> Maybe Response

Now, instead of getting back something to tie into Warp's intercept handler, we +get back a Response. If there was no WebSockets request, we get Nothing, +which allows us to write normal, non-WebSockets responses.

yesod-websockets

This is also the first release of the yesod-websockets package. Previously, +there was no good story for how to tie WebSockets into an existing Yesod +application. Now, integration is trivial. Let's say we want to write a very +basic chat server. We can describe this as a WebSockets application with the +following:

chatApp :: WebSocketsT Handler ()
+chatApp = do
+    sendTextData ("Welcome to the chat server, please enter your name." :: Text)
+    name <- receiveData
+    sendTextData $ "Welcome, " <> name
+    App writeChan <- getYesod
+    readChan <- atomically $ do
+        writeTChan writeChan $ name <> " has joined the chat"
+        dupTChan writeChan
+    race_
+        (forever $ atomically (readTChan readChan) >>= sendTextData)
+        (sourceWS $$ mapM_C (\msg ->
+            atomically $ writeTChan writeChan $ name <> ": " <> msg))

sendTextData and receiveData are the core functions. There's also a +conduit-based API using sourceWS and sinkWSText, as well as some +convenience asynchronous helpers (race and concurrently). But more +interesting is the integration with the existing handler infrastructure:

getHomeR :: Handler Html
+getHomeR = do
+    webSockets chatApp
+    defaultLayout ...

The webSockets function takes a WebSockets application and tries to run it. +If the client sent a WebSockets request, then the app will be run, and no +further Handler actions will be taken. Otherwise, normal operations will +continue. (You can see the full chat +example +in the Github repo.)

As this is a first release, things are still evolving, so it's not a good idea +to base your entire app off of this yet. But if you've been interested in +playing with WebSockets, now's a good time to get started. If you end up +working on anything, please let me know!

http-reverse-proxy

This is actually the project that kicked off my endeavors into responseRaw in +the first place. I wanted to add support for reverse proxying WebSockets +requests, and initially did so without WAI +support +using settingsIntercept. But the entire approach felt like a hack. Now with +responseRaw support, all users of http-reverse-proxy automatically get +WebSockets support. (This includes yesod devel and keter, by the way.)

I also put together an experimental forward proxy server a few weeks ago, and +responseRaw made that nicer +too.

Additional change: settings

Kazu and I changed a few settings since Warp 2.0, which required a major +version bump. We didn't like that the 2.0 settings infrastructure required a +major version bump for minor tweaks to settings, so we've introduced a new +system for modifying Warp settings. You still start with the defaultSettings +value, but to modify, for example, the port, you use the new setter function:

setPort 8080 defaultSettings

The old record-based accessors have been deprecated. In a future release, we'll +be moving them entirely to an internal module.

The concrete settings change was providing the socket address to the onOpen +and onClose settings. This +allows you to write logging functions when connections are opened and closed, +and indicate where the connections come from. In addition, you can inspect the +SockAddr in onOpen and reject a connection immediately. Previously, this +needed to be done from the application itself, which meant that more processing +was performed before terminating the connection. +onOpen/onClose.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/04/consolidation-progress.html b/public/blog/2014/04/consolidation-progress.html new file mode 100644 index 00000000..2d110978 --- /dev/null +++ b/public/blog/2014/04/consolidation-progress.html @@ -0,0 +1,35 @@ + Consolidation progress: shakespeare, conduit, http-client +

Consolidation progress: shakespeare, conduit, http-client

April 3, 2014

GravatarBy Michael Snoyman

My last blog post detailed a number of changes I was going to be making for package consolidation. A number of those have gone through already, this blog post is just a quick summary of the changes.

shakespeare

shakespeare is now a single package. hamlet, shakespeare-css, shakespeare-js, +shakespeare-i18n, shakespeare-text, and servius have all been merged in and +marked as deprecated. I've also uploaded new, empty versions of those +deprecated packages. This means that, in order to support both the old and new +versions of shakespeare, you just need to ensure that you have both the +shakespeare and deprecated packages listed in your cabal file. In other words, +if previously you depended on hamlet, now you should depend on hamlet and +shakespeare. When you're ready to drop backwards compatibility, simply put a +lower bound of >= 2.0 on shakespeare and remove the deprecated packages.

(Note: this method for dealing with deprecated packages is identical for all +future deprecations, I won't detail the steps in the rest of this blog post.)

conduit

conduit-extra now subsumes attoparsec-conduit, blaze-builder-conduit, +network-conduit, and zlib-conduit. It also includes three modules that used to +be in conduit itself: .Text, .Binary, and .Lazy. To deal with this change, +simply adding conduit-extra to your dependencies should be sufficient.

The other changes have to do with resourcet. In particular:

  • Data.Conduit no longer reexports identifiers from resourcet and +monad-control. These should be imported directly from their sources.
  • Instead of defining its own MonadThrow typeclass, resourcet now uses the +MonadThrow typeclass from the exceptions package. For backwards +compatibility, Control.Monad.Trans.Resource provides monadThrow as an alias +for the new throwM function.
  • The Resource monad had a confusing name, in that it wasn't directly related to the ResourceT transformer. I've renamed it to Acquire, and put it in its own module (Data.Acquire).
    • I'm actually very happy with Acquire, and think it's a great alternative to hard-coding either the bracket pattern or resourcet into libraries. I'm hoping to add better support to WAI for Acquire, and blog a bit more about the usage of Acquire.
  • MonadUnsafeIO has been removed entirely. All of its functionality can be replaced with MonadPrim and MonadBase (for example, see the changes to blaze-builder-conduit).
  • MonadActive, which is only needed for Data.Conduit.Lazy, has been moved to that module.

http-client

http-client-multipart has been merged into http-client. In addition, instead of using the failure package, http-client now uses the exceptions package.

http-client-conduit has been merged into http-conduit. I've also greatly expanded the Network.HTTP.Client.Conduit module to contain what I consider its next-gen API. In particular:

  • No usage of ResumableSource.
  • Instead of explicit ResourceT usage, it uses the Acquire monad and bracket pattern (acquireResponse, withResponse).
  • Instead of explicitly passing around a Manager, it uses MonadReader and the HasHttpManager typeclass.

I'm curious how people like the new API. I have no plans on removing or changing the current Network.HTTP.Conduit module, this is merely an alternative approach.

Updated yesod-platform

I've also released a new version of yesod-platform that uses the new versions +of the packages above. A number of packages on Hackage still depend on conduit +1.0, but I've sent quite a few pull requests in the past few days to get things +up-to-date. Thankfully, maintaining compatibility with both 1.0 and 1.1 is +pretty trivial.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/04/disemboweling-wai.html b/public/blog/2014/04/disemboweling-wai.html new file mode 100644 index 00000000..e1fddbe2 --- /dev/null +++ b/public/blog/2014/04/disemboweling-wai.html @@ -0,0 +1,71 @@ + Disemboweling WAI (aka gutting out conduit) +

Disemboweling WAI (aka gutting out conduit)

April 28, 2014

GravatarBy Michael Snoyman

The Haskell Web Application Interface- or WAI- serves as a low-level interface +between web applications and servers. In order to do this in a +resource-efficient manner, it avoids lazy I/O and instead uses explicit +streaming. The story of how it does that streaming has evolved over time: it +initially used its own +home-brewed streaming abstraction. +Later, Gregory Collins +convinced me to switch over to enumerator. In the 1.0 +release, it switched to conduit, +which was essentially designed for the very purpose of supporting WAI's use +cases. While I'm very happy with conduit, baking conduit into WAI makes the +barrier to using a different streaming framework (like pipes) higher.

So today, I'm proposing that we go all the way back to the beginning, and +remove dependencies on external streaming frameworks from WAI, making it a +completely universal web interface for Haskell.

I've been hesitant about doing this in the past due to two different reasons:

  1. Making this change makes it more difficult to write handlers and middleware +for WAI.
  2. It's not really possible to get rid of a streaming abstraction entirely; +instead, we end up just having a locally baked abstraction, which is less +thoroughly tested than existing abstractions.

On the first point, most middlewares and handlers which modify request and +response bodies are already maintained by the WAI team (and mostly by me +personally), so I'm less concerned about pushing such a burden out onto the +community. Thankfully, applications and frameworks can be completely insulated +by this change by providing a wai-conduit adapter package.

Regarding the second point, I've recently had some experience with this: +refactoring http-client out +of http-conduit. It turned +out to be relatively painless, though I did end up having to reimplement a +number of combinators from conduit (especially in the test suite). Nonetheless, +the codebase is about the same level of complexity, given the low-level nature +of the http-client library, and there's been an overwhelmingly positive +response to the splitting up of those two packages, so I want to try it out +with WAI as well.

I've created a no-conduit +branch in the WAI repo. +Currently, I've converted the wai and warp repos over to be conduit-free (with +a few helper functions stubbed out). And Warp passes its full test suite, which +is rather promising.

The streaming interface is relatively simple. In order to consume a request body, you use the function:

requestBody :: Request -> IO ByteString

This function returns the next strict ByteString from the request body in the +stream, or an empty ByteString if the body has been fully consumed. On the +response side, you write an application with the type:

(Maybe Builder -> IO ()) -> IO ()

The argument function is used for emitting data to the user. If you provide a +Just value, it sends the data in, and a Nothing value flushes the stream. +(Currently, WAI uses the Flush data type from conduit for this purpose.)

The code is not yet ready to be released, but it is ready for some review and +discussion. I'm hoping to hear community feedback, both from current users of +WAI, and those who are considering using it (either directly in an application, +or as part of a framework).


A separate breaking change that came up when working on this feature has +to do with safe resource allocation and the definition of Application. +In WAI 2.0, we introduced the function responseSourceBracket, which is a +version of bracket that works for WAI applications. Internally to Warp +(and every other WAI handler), we had to jump through some hoops to make sure +async exceptions were all masked and restored properly. We also had to make our +definition of ResponseSource a bit ugly to make this all possible.

Now with the move away from Sources, we have two choices: either perpetuate the strangeness in the ResponseStream and ResponseRaw constructor, or add bracket semantics to the entirety of a WAI application. In terms of code, I'm talking about replacing:

type Application = Request -> IO Response

with

type Application = Request -> (forall b. (Response -> IO b) -> IO b)

I've implemented this idea as +well. +It certainly makes the Warp codebase easier to follow, and allows for some +slightly more powerful applications (e.g., acquiring a resource for use in some +lazy I/O performed by ResponseBuilder). The downsides are:

  • Yet another breaking change.
  • It's harder to explain the intuition versus the dead simple Application we have now.
  • It will likely make middlewares significantly harder to write, though I'll admit that I haven't tried that yet.

When I discussed this change with Gabriel, he pointed out his Managed data +type +as a possible approach to make the signatures and usage slightly less scary. +That would mean:

newtype Managed r = Managed { _bind :: forall x . (r -> IO x) -> IO x }
+type Application = Request -> Managed Response

Managed would be nice in that it provides a number of instances (including +Monad), but it makes the WAI API a little bit denser to grok. I think I'm +leaning against that direction, but wanted to raise this in the discussion as +well.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/04/proposal-changes-pvp.html b/public/blog/2014/04/proposal-changes-pvp.html new file mode 100644 index 00000000..ae2a16ee --- /dev/null +++ b/public/blog/2014/04/proposal-changes-pvp.html @@ -0,0 +1,123 @@ + Proposal: Changes to the PVP +

Proposal: Changes to the PVP

April 8, 2014

GravatarBy Michael Snoyman

As I mentioned two posts +ago, there was a +serious discussion on the libraries mailing +list +about the Package Versioning +Policy (PVP).

This blog post presents some concrete changes I'd like to see to the PVP to +make it better for both general consumers of Hackage, and for library authors +as well. I'll start off with a summary of the changes, and then give the +explanations:

  1. The goal of the PVP needs to be clarified. Its purpose is not to ensure +reproducible builds of non-published software, but rather to provide for +more reliable builds of libraries on Hackage. Reproducible builds should be +handled exclusively through version freezing, the only known technique to +actually give the necessary guarantees.

  2. Upper bounds should not be included on non-upgradeable packages, such as +base and template-haskell (are there others?). Alternatively, we should +establish some accepted upper bound on these packages, e.g. many people place +base < 5 on their code.

  3. We should be distinguishing between mostly-stable packages and unstable +packages. For a package like text, if you simply import Data.Text (Text, +pack, reverse), or some other sane subset, there's no need for upper bounds.

    Note that this doesn't provide a hard-and-fast rule like the current PVP, but is +rather a matter of discretion. Communication between library authors and users (via +documentation or other means) would be vital to making this work well.

  4. For a package version A.B.C, a bump in A or B indicates some level of +breaking change. As an opt-in approach, package authors are free to +associated meaning to A and B beyond what the PVP requires. Libraries which use these +packages are free to rely on the guarantees provided by package authors when +placing upper bounds.

    Note that this is very related to point (3).

While I (Michael Snoyman) am the author of this proposal, the following people +have reviewed the proposal and support it:

  • Bryan O'Sullivan
  • Felipe Lessa
  • Roman Cheplyaka
  • Vincent Hanquez

Reproducible builds

There are a number of simple cases that can result in PVP-compliant code not +being buildable. These aren't just hypothetical cases; in my experience as both +a package author and Stackage maintainer, I've seen these come up.

  • Package foo version 1.0 provides an instance for MonadFoo for IO and +Identity. Version 1.1 removes the IO instance for some reason. Package bar +provides a function:

    bar :: MonadFoo m => Int -> m Double

    Package bar compiles with both version 1.0 and 1.1 of foo, and therefore +(following the PVP) adds a constraint to its cabal file foo >= 1.0 && < 1.2.

    Now a user decides to use the bar package. The user never imports anything from +foo, and therefore has no listing for foo in the cabal file. The user code +depends on the IO instance for MonadFoo. When compiled with foo 1.0, everything +works fine. However, when compiled with foo 1.1, the code no longer compiles.

  • Similarly, instead of typeclass instances, the same situation can occur +with module export lists. Consider version 1.0 of foo which provides:

    module Foo (foo1, foo2) where

    Version 1.1 removes the foo2 export. The bar package reexports the entire Foo +module, and then a user package imports the module from bar. If the user +package uses the foo2 function, it will compile when foo-1.0 is used, but not +when foo-1.1 is used.

In both of these cases, the issue is the same: transitive dependencies are not +being clamped down. The PVP makes an assumption that the entire interface for a +package can be expressed in its version number, which is not true. I see three +possible solutions to this:

  1. Try to push even more of a burden onto package authors, and somehow make +them guarantee that their interface is completely immune to changes +elsewhere in the stack. This kind of change was proposed on the libraries list. +I'm strongly opposed to some kind of change like this: it makes authors' lives +harder, and makes it very difficult to provide backwards compatibility in +libraries. Imagine if transformers 0.4 adds a new MonadIO instance; the logical +extreme of this position would be to disallow a library from working with both +transformers 0.3 and 0.4, which will split Hackage in two.

  2. Modify the PVP so that instead of listing just direct dependencies, authors +are required to list all transitive dependencies as well. So it would be a +violation to depend on bar without explicitly listing foo in the dependency +list. This will work, and be incredibly difficult to maintain. It will also +greatly increase the time it takes for a new version of a deep dependency to be +usable due to the number of authors who will have to bump version bounds.

  3. Transfer responsibility for this to package users: if you first built your +code against foo 1.0, you should freeze that information and continue +building against foo 1.0, regardless of the presence of new versions of foo. +Not only does this increase reproducibility, it's just common sense: it's +entirely possible that new versions of a library will introduce a runtime bug, +performance regression, or even fix a bug that your code depends on. Why should +the reliability of my code base be dependent on the actions of some third party +that I have no control over?

Non-upgradeable packages

There are some packages which ship with GHC and cannot be upgraded. I'm aware +of at least base and template-haskell, though perhaps there are others +(haskell98 and haskell2010?). In the past, there was good reason to place upper +bounds on base, specifically with the base 3/4 split. However, we haven't had +that experience in a while, and don't seem to be moving towards doing that +again. In today's world, we end up with the following options:

  • Place upper bounds on base to indicate "I haven't tested this with newer +versions of GHC." This then makes it difficult for users to test out that +package with newer versions of GHC.
  • Leave off upper bounds on base. Users may then try to install a package onto +a version of GHC on which the package hasn't been tested, which will result +in either (1) everything working (definitely the common case based on my +Stackage maintenance), or (2) getting a compilation error.

I've heard two arguments to push us in the direction of keeping the upper +bounds in this case, so I'd like to address them both:

  • cabal error messages are easier to understand than GHC error messages. I have two problems with that:
    • I disagree: cabal error messages are terrible. (I'm told this will be fixed in the next version of cabal.) Take the following output as a sample:

      cabal: Could not resolve dependencies:
      +trying: 4Blocks-0.2
      +rejecting: base-4.6.0.1/installed-8aa... (conflict: 4Blocks => base>=2 && <=4)
      +rejecting: base-4.6.0.1, 4.6.0.0, 4.5.1.0, 4.5.0.0, 4.4.1.0, 4.4.0.0, 4.3.1.0,
      +4.3.0.0, 4.2.0.2, 4.2.0.1, 4.2.0.0, 4.1.0.0, 4.0.0.0, 3.0.3.2, 3.0.3.1 (global
      +constraint requires installed instance)

      I've seen a number of users file bug reports not understanding that +this message means "you have the wrong version of GHC."

    • Even if the error messages were more user-friendly, they make it more +difficult to fix the actual problem: the code doesn't compile with the +new version of GHC. Often times, I've been able to report an error message to a +library author and, without necessarily even downloading the new version of +GHC, he/she has been able to fix the problem.

  • Using upper bounds in theory means that cabal will be able to revert to an +older version of the library that is compatible with the new version of +GHC. However, I find it highly unlikely that there's often- if ever- a case +where an older version of a library is compatible with a later version of GHC.

Mostly-stable, and finer-grained versioning

I'll combine the discussion of the last two points. I think the heart of the +PVP debates really comes from mostly-stable packages. Let's contrast with the +extremes. Consider a library which is completely stable, never has a breaking +change, and has stated with absolute certainty that it never will again. Does +anyone care about upper bounds on this library? They're irrelevant! I'd have +no problem with including an upper bound, and I doubt even the staunchest PVP +advocates would really claim it's a problem to leave it off.

On the other hand, consider an extremely unstable library, which is releasing +massively breaking changes on a weekly basis. I would certainly agree in that +case that an upper bound on that library is highly prudent.

The sticking point is the middle ground. Consider the following code snippet:

import Data.Text (Text, pack)
+
+foo :: Text
+foo = pack "foo"

According to the PVP as it stands today, this snippet requires an upper bound +of < 1.2 on the text package. But let's just play the odds here: does anyone +actually believe there's a real chance that the next iteration of text will +break this code snippet? I highly doubt it; this is a stable subset of the text +API, and I doubt it will ever be changing. The same can be said of large +subsets of many other packages.

By putting in upper bounds in these cases, we run a very real risk of +bifurcating Hackage into "those demanding the new text version for some new +feature" vs "those who haven't yet updated their upper bounds to allow the new +version of text."

The PVP currently takes an extremely conservative viewpoint on this, with the +goal of solving just one problem: making sure code that compiles now continues +to compile. As I demonstrated above, it doesn't actually solve that problem +completely. And in addition, in this process, it has created other problems, +such as this bifurcation.

So my proposal is that, instead of creating rigid rules like "always put an +upper bound no matter what," we allow some common sense into the process, and +also let package authors explicitly say "you can rely on this API not +changing."

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/05/exceptions-cont-monads.html b/public/blog/2014/05/exceptions-cont-monads.html new file mode 100644 index 00000000..0a5f1bce --- /dev/null +++ b/public/blog/2014/05/exceptions-cont-monads.html @@ -0,0 +1,110 @@ + Exceptions in continuation-based monads +

Exceptions in continuation-based monads

May 30, 2014

GravatarBy Michael Snoyman

I've been meaning to write a blog post for a few weeks now about exception +handling in Haskell, especially with regards to monad transformer stacks. +Unfortunately, this is not that blog post, just one small aspect of it: +exception handling in continuation-base monads.

I've seen many people at different times advocate having some kind of exception +handling in continuation-based monads. Without calling out individual +instances, I'll sum up a lot of what I've discussed as, "Why can't +conduit/enumerator/pipes have a bracket function?"

After noticing yet another request for such a thing this morning, I decided to +write up a quick demonstration of what happens when you create a bracket +function for the enumerator package. I'll be using MonadCatchIO-transformers +(which is thankfully deprecated in favor of the exceptions +package), and snap-core's +orphan +instance.

Let's start off by noticing something interesting: enumerator provides an +enumFile function (for reading the contents of a file), but no iterFile +equivalent to write data back. Using a bracket, it's actually really easy to +write up such a function (including some debug output to make sure we're being +safe):

iterFile :: (MonadCatchIO m, MonadIO m, Functor m)
+         => FilePath -> Iteratee ByteString m ()
+iterFile fp = bracket
+    (liftIO $ do
+        putStrLn $ "opening file for writing: " ++ fp
+        IO.openFile fp IO.WriteMode)
+    (\h -> liftIO $ do
+        putStrLn $ "closing file for writing: " ++ fp
+        IO.hClose h)
+    iterHandle

There shouldn't be any surprises in this implementation: we open a file handle +in the acquire argument, close that handle in the release argument, and then +use the handle in the inner argument. All is well in the world. Now let's try +actually using this function, both with and without exceptions being thrown:

main :: IO ()
+main = do
+    writeFile "exists.txt" "this file exists"
+    run (enumFile "exists.txt" $$ iterFile "output1.txt") >>= print
+    run (enumFile "does-not-exist.txt" $$ iterFile "output2.txt") >>= print

Or you can try running the code +yourself. Let's look at the output:

opening file for writing: output1.txt
+closing file for writing: output1.txt
+Right ()
+opening file for writing: output2.txt
+Left does-not-exist.txt: openBinaryFile: does not exist (No such file or directory)

Notice that the output2.txt Handle is never closed. This is inherent to +working with any continuation based monad, since there are no guarantees that +the continuation will be called at all. It's also impossible to know if your +continuation will be called only once. With something like ContT, it's +possible to have the continuation run multiple times, in which case your +cleanup actions can run multiple times, which can be really +bad.

The exceptions package handles this in the right way. There are two different +type classes: +MonadCatch +allows for catching exceptions (which a continuation based monad does allow +for), whereas +MonadMask +gives guarantees about bracket/finally semantics, which is what a +continuation-based monad cannot do. Another valid approach is +monad-control, which makes +it (I believe) impossible to write invalid instances.

(I want to get into more of the details of the trade-offs between exceptions and +monad-control, but that will have to wait for another blog post. For now, I +just wanted to address immediate continuation based concern.)

If you're in a continuation based monad and you need exception safe resource +handling, there is a solution: +resourcet. +resourcet hoists the exception safety outside of the realm of the continuation +based code, and maintainers finalizer functions via mutable variables. Note +that this isn't just useful for continuation based monads, but for any +situation where you don't have full control over the flow of execution of the +program. For example, you'd use the same technique for an io-streams directory +traversal.

One last caveat. There is one case where a continuation based monad could in +theory have a valid bracket function, which is where you have full knowledge +of the code which will run the continuation, and can guarantee that all +continuations will always be executed. So if you hide constructors and only +expose such run functions, you might be safe. But the burden of proof is on +you.


Note: I'm purposely not linking to any of the conversations I've referred to +about getting a bracket function for continuation based monads, I don't feel +like calling people out here. Also, in case you don't feel like loading up the +School of Haskell page, here's the full source code for my example above:

{-# LANGUAGE PackageImports #-}
+import           "MonadCatchIO-transformers" Control.Monad.CatchIO  (MonadCatchIO,
+                                                                     bracket)
+import           Control.Monad.IO.Class (MonadIO, liftIO)
+import           Data.ByteString        (ByteString, hPut)
+import           Data.Enumerator        (Iteratee, run, ($$))
+import           Data.Enumerator.Binary (enumFile, iterHandle)
+import           Snap.Iteratee          () -- orphan instance
+import qualified System.IO              as IO
+
+iterFile :: (MonadCatchIO m, MonadIO m, Functor m)
+         => FilePath -> Iteratee ByteString m ()
+iterFile fp = bracket
+    (liftIO $ do
+        putStrLn $ "opening file for writing: " ++ fp
+        IO.openFile fp IO.WriteMode)
+    (\h -> liftIO $ do
+        putStrLn $ "closing file for writing: " ++ fp
+        IO.hClose h)
+    iterHandle
+
+main :: IO ()
+main = do
+    writeFile "exists.txt" "this file exists"
+    run (enumFile "exists.txt" $$ iterFile "output1.txt") >>= print
+    run (enumFile "does-not-exist.txt" $$ iterFile "output2.txt") >>= print

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/05/foldable-mapm-maybe-recursive.html b/public/blog/2014/05/foldable-mapm-maybe-recursive.html new file mode 100644 index 00000000..6b5748f5 --- /dev/null +++ b/public/blog/2014/05/foldable-mapm-maybe-recursive.html @@ -0,0 +1,102 @@ + Foldable.mapM_, Maybe, and recursive functions +

Foldable.mapM_, Maybe, and recursive functions

May 5, 2014

GravatarBy Michael Snoyman

This blog post is also available as a School of Haskell tutorial. I recommend reading the content there, as you can use active code.


I've run into this issue myself, and seen others hit it too. Let's start off with some very simple code:

sayHi :: Maybe String -> IO ()
+sayHi mname =
+    case mname of
+        Nothing -> return ()
+        Just name -> putStrLn $ "Hello, " ++ name
+
+main :: IO ()
+main = sayHi $ Just "Alice"

There's nothing amazing about this code, it's pretty straight-forward pattern matching Haskell. And at some point, many Haskellers end up deciding that they don't like the explicit pattern matching, and instead want to use a combinator. So the code above might get turned into one of the following:

import qualified Data.Foldable as F
+hiHelper :: String -> IO ()
+hiHelper name = putStrLn $ "Hello, " ++ name
+
+sayHi1 :: Maybe String -> IO ()
+sayHi1 = maybe (return ()) hiHelper
+
+sayHi2 :: Maybe String -> IO ()
+sayHi2 = F.mapM_ hiHelper
+
+main :: IO ()
+main = do
+    sayHi1 $ Just "Alice"
+    sayHi2 $ Just "Bob"
+    -- or often times this:
+    F.forM_ (Just "Charlie") hiHelper

The theory is that all three approaches (maybe, mapM_, and forM_) will end up being identical. We can fairly conclusively state that forM_ will be the exact same thing as mapM_, since it's just mapM_ flipped. So the question is: will the maybe and mapM_ approaches do the same thing? In this case, the answer is yes, but let's spice it up a bit more. First, the maybe version:

import qualified Data.Text.Lazy as T
+import qualified Data.Foldable as F
+import Control.Monad (when)
+
+printChars :: Int -> T.Text -> IO ()
+printChars idx t = maybe (return ()) (\(c, t') -> do
+    when (idx `mod` 100000 == 0)
+        $ putStrLn $ "Character #" ++ show idx ++ ": " ++ show c
+    printChars (idx + 1) t') (T.uncons t)
+
+main :: IO ()
+main = printChars 1 $ T.replicate 5000000 $ T.singleton 'x'

The code above works correctly in constant space. However, the usage of maybe makes this a bit ugly. This is a common time to use forM_ to syntactically clean things up. So let's give that a shot:

import qualified Data.Text.Lazy as T
+import qualified Data.Foldable as F
+import Control.Monad (when)
+
+printChars :: Int -> T.Text -> IO ()
+printChars idx t = F.forM_ (T.uncons t) $ \(c, t') -> do
+    when (idx `mod` 100000 == 0)
+        $ putStrLn $ "Character #" ++ show idx ++ ": " ++ show c
+    printChars (idx + 1) t'
+
+main :: IO ()
+main = printChars 1 $ T.replicate 5000000 $ T.singleton 'x'

The code is certainly cleaner and easier to follow. However, try running it: you'll get a stack overflow. The issue is that the implementation of mapM_ in Data.Foldable is not tail recursive. As a result, each recursive call ends up accumulating a bunch of "do nothing" actions to perform after completing the recursive call, which wipes out the stack.

Fortunately, solving this issue is pretty easy: write a tail-recursive version of forM_ for Maybe:

import qualified Data.Text.Lazy as T
+import qualified Data.Foldable as F
+import Control.Monad (when)
+
+forM_Maybe :: Monad m => Maybe a -> (a -> m ()) -> m ()
+forM_Maybe Nothing _ = return ()
+forM_Maybe (Just x) f = f x
+
+printChars :: Int -> T.Text -> IO ()
+printChars idx t = forM_Maybe (T.uncons t) $ \(c, t') -> do
+    when (idx `mod` 100000 == 0)
+        $ putStrLn $ "Character #" ++ show idx ++ ": " ++ show c
+    printChars (idx + 1) t'
+
+main :: IO ()
+main = printChars 1 $ T.replicate 5000000 $ T.singleton 'x'

There's one slight difference in the type of forM_Maybe and forM_ specialized to Maybe. The former takes a second argument of type a -> m (), while the latter takes a second argument of type a -> m b. This difference is unfortunately necessary; if we try to get back the original type signature, we have to add an extra action to wipe out the return value, which again reintroduces the stack overflow:

import qualified Data.Text.Lazy as T
+import qualified Data.Foldable as F
+import Control.Monad (when)
+
+forM_Maybe :: Monad m => Maybe a -> (a -> m b) -> m ()
+forM_Maybe Nothing _ = return ()
+-- show
+forM_Maybe (Just x) f = f x {-hi-}>> return (){-/hi-}
+-- /show
+
+printChars :: Int -> T.Text -> IO ()
+printChars idx t = forM_Maybe (T.uncons t) $ \(c, t') -> do
+    when (idx `mod` 100000 == 0)
+        $ putStrLn $ "Character #" ++ show idx ++ ": " ++ show c
+    printChars (idx + 1) t'
+
+main :: IO ()
+main = printChars 1 $ T.replicate 5000000 $ T.singleton 'x'

mono-traversable

I'd like to address this issue in mono-traversable, but it would require changing the type of mapM_ and forM_. I'm tempted to do so, but am interested if it causes breakage for anyone. If you have an opinion on this, please comment on the Github issue. For the record, here's the same stack overflow with mono-traversable.

import qualified Data.Text.Lazy as T
+import Data.MonoTraversable (oforM_)
+import Control.Monad (when)
+
+printChars :: Int -> T.Text -> IO ()
+printChars idx t = oforM_ (T.uncons t) $ \(c, t') -> do
+    when (idx `mod` 100000 == 0)
+        $ putStrLn $ "Character #" ++ show idx ++ ": " ++ show c
+    printChars (idx + 1) t'
+
+main :: IO ()
+main = printChars 1 $ T.replicate 5000000 $ T.singleton 'x'

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/05/wai-3-0-alpha.html b/public/blog/2014/05/wai-3-0-alpha.html new file mode 100644 index 00000000..9917db66 --- /dev/null +++ b/public/blog/2014/05/wai-3-0-alpha.html @@ -0,0 +1,64 @@ + WAI 3.0 alpha release +

WAI 3.0 alpha release

May 27, 2014

GravatarBy Michael Snoyman

Following up on last month's blog +post, I'm happy to +announce that a version of WAI without any conduit dependency is available for +testing. If you'd like to test this the easy way, please add the following line +to your Cabal config file (on a Linux system: $HOME/.cabal/config):

remote-repo: wai3:http://www.stackage.org/stackage/79eaf0520967e1e63e61b39e15f914a8971052c5

Then, run cabal update and you should be able to install wai-3.0. This +snapshot also includes updated versions of all the yesod packages, which are +compatible with wai 2.0 and 3.0 (and, I believe, 1.4).

I think this version of the code is pretty stable, and just about ready for +release. If you're a user of WAI, please take some time to check out the new +version and get in your feedback before the 3.0 release.

Decisions

There were two issues left unresolved after the last public discussion: the +streaming interface, and how we'd provide exception safety. After quite a bit +of offline discussion, we came to the following decisions.

The streaming interface looks like the following:

type StreamingBody = (Builder -> IO ()) -> IO () -> IO ()
+data Response
+    = ...
+    | ResponseStream H.Status H.ResponseHeaders StreamingBody
+    | ...

StreamingBody is a function which takes two arguments: a function for sending +another chunk of data to the client, and a function to flush data to the +client. This is more efficient than the previous Maybe Builder -> IO () +approach as it avoids the need to construct/destruct a Maybe value.

Regarding exception safety, we ended up with:

type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived

where the constructor for ResponseReceived is only exposed from an internal +module. The idea is to follow the bracket pattern, but also avoid any +RankNTypes/ImpredicativeTypes, as their presence made it much harder to write +middlewares. There is a known flaw with this approach: a poorly written +application can run the supplied callback multiple times. This is not stopped +by the type system, but is a violation of the WAI interface.

I'd recommend that anyone building higher-level frameworks on top of WAI use +the type system to ensure that such an abuse can't occur. However, as is +becoming the mantra of WAI, we want to keep the interface low-level and simple, +and therefore we're willing to expose a few rough edges like this.

WebSockets

The entire wai codebase is now devoid of streaming data libraries... except for +wai-websockets, which depends on the websockets library, which in turn depends +on io-streams. As I think most of my readers know, I'm very much in favor of +sharing libraries across the Haskell ecosystem wherever possible, and +therefore- until now- have had no problem with adding a dependency on a +different streaming data abstraction. However, given that WAI is attempting to +be completely devoid of a streaming data framework, this dependency no longer +makes sense.

There seem to be a few approaches forward here. One would be to simply keep the +status quo, and depend on websockets/io-streams. Another would be to try and +remove the io-streams dependency from websockets. I'm not sure if that's +something Jasper's interested in or not. Another would be to have a +WAI-specific implementation of websockets, which initially doesn't look too +difficult.

Note that this issue came to a head earlier this week when a major performance +problem was +uncovered +in yesod-websockets. We haven't yet done enough investigation to determine the +real culprit, but it appears to be io-streams based on the profiling output.

In any event, it's very likely the WAI 3.0 will ship with wai-websockets still +depending on websockets as-is, and we can figure out any changes to be made for +a later wai-websockets-3.1 release.

Reflections

Overall, the move away from conduit was not too difficult. Let me address three +separate components:

  • Writing applications is not any more difficult, simply because it's trivial to wrap up the new streaming interface inside a conduit interface. I avoided doing so in wai itself, but did exactly that when working on Yesod.
  • Writing handlers (e.g. Warp) turned out to not be too bad. With all of the performance optimizations we've put into Warp over the years, it frankly was no longer taking real advantage of the beauty of a streaming data framework, so cutting out conduit didn't really make the codebase any worse (and, in some places, made it a bit cleaner, especially where we were already doing ugly IORef tricks).
  • The tough one was writing middlewares. conduit simply provides a really easy way to say "take this stream of Builders, convert it to a stream of ByteStrings, apply a simple transformation on the HTML, and GZIP compress the whole thing." Doing this with lower-level streaming primitives is far more clumsy. For a good example, see wai-handler-launch.

My feeling now is the same as a month ago: if we were in a world where lots of +people were writing WAI middlewares regularly, this change would be a bad idea. +But it's become quite clear that most people are writing applications, where +having a low-level streaming interface is not a big problem.

Timing

If I hear no feedback on the changes in WAI by June 3, I'll assume everyone's +OK with the changes, and will start planning an official release. So if you +have some feedback you'd like to get in, please do so in the next week.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/06/evil-conditional-compile.html b/public/blog/2014/06/evil-conditional-compile.html new file mode 100644 index 00000000..be153074 --- /dev/null +++ b/public/blog/2014/06/evil-conditional-compile.html @@ -0,0 +1,95 @@ + Three evil ways to avoid conditional compilation +

Three evil ways to avoid conditional compilation

June 15, 2014

GravatarBy Michael Snoyman

Let's suppose that you're using a library at version 1. It exposes a function:

someFunc :: Int -> String
+someFunc i = 'x' : show i

In version 2 of the library, someone realizes that this library isn't +general-purpose enough: why is the 'x' character hardcoded? So version 2 +exposes a more powerful version of the function:

someFunc :: Int -> Char -> String
+someFunc i c = c : show i

In your current codebase, you have:

main = putStrLn $ someFunc 5

Changing that code to work with version 2 is trivial:

main = putStrLn $ someFunc 5 'x'

But what if you want to make your code work with both versions? The real, proper answer, that I hope everyone actually uses, is to use Cabal CPP macros:

#if MIN_VERSION_somelib(2, 0, 0)
+main = putStrLn $ someFunc 5 'x'
+#else
+main = putStrLn $ someFunc 5
+#endif

And sure, you should do that... but let's have some fun. I'm going to present +three evil techniques to accomplish the same conditional compilation result, +and try to point out their relative merits. I encourage others to come up with +other ridiculous ideas of their own.

If anyone's curious where I came up with the idea to do this, it was thinking +about prerelease GHC patch level releases that break backwards +compatibility. +And if you want to play around with the code, either open it in FP Haskell +Center +or clone the Github +repo. In all the +examples, simply switch whether V1 or V2 is imported to simulated +upgrading/downgrading dependencies.

Typeclasses

A well known truth in Haskell is, "for every problem, there's an abuse of +typeclasses waiting to be discovered." As far as typeclass abuses go, this one +is pretty benign.

{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+module Typeclass where
+
+import V1
+-- import V2
+
+class Evil a where
+    evil :: a -> String
+instance Evil String where
+    evil = id
+instance Evil a => Evil (Char -> a) where
+    evil f = evil (f 'x')
+
+main :: IO ()
+main = putStrLn $ evil $ someFunc 5

What's "nice" about this approach (if anything here is nice) is that everything +is compile-time checked, and the code is actually pretty readable. However, as +the different cases you want to support get more complicated, you'll need to +add in ever harrier language extensions.

Typeable

This approach is for the Pythonista in you. Next time someone proudly states +that Haskell is a statically typed language, just pull this one out:

module Typeable where
+
+import Data.Typeable
+-- import V1
+import V2
+
+evil :: Typeable a => a -> String
+evil a
+    | Just s <- cast a = s
+    | Just f <- cast a = f 'x'
+    | otherwise = error "Yay, runtime type errors!"
+
+main :: IO ()
+main = putStrLn $ evil $ someFunc 5

Advantage: it's so incredibly trivial to add more cases. Downsides: it's +runtime type checking, and the dispatch is performed at runtime, not compile +time.

Template Haskell

Of course, no blog post about Haskell abuse would be complete without some +usage of Template Haskell. Due to the stage restriction, we have to split this +into two files. First, THHelper:

{-# LANGUAGE TemplateHaskell #-}
+module THHelper where
+
+import Language.Haskell.TH
+
+evil :: Name -> Q Exp
+evil name = do
+    VarI _ typ _ _ <- reify name
+    case typ of
+        AppT (AppT ArrowT (ConT _)) (ConT _) ->
+            return $ VarE name
+        AppT (AppT ArrowT (ConT _)) (AppT (AppT ArrowT (ConT _)) (ConT _)) ->
+            [|flip $(return $ VarE name) 'x'|]
+        _ -> error $ "Unknown type: " ++ show typ

Notice how beautiful our pattern matching is. This combines the best (worst) of +both worlds from above: we get full compile time checking, and can easily +(hah!) pattern match on all possible signatures for the function at hand.

And calling this beast is equally elegant:

{-# LANGUAGE TemplateHaskell #-}
+module TH where
+
+import THHelper
+import V1
+-- import V2
+
+main :: IO ()
+main = putStrLn $ $(evil 'someFunc) 5

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/06/exceptions-transformers.html b/public/blog/2014/06/exceptions-transformers.html new file mode 100644 index 00000000..3148bbf1 --- /dev/null +++ b/public/blog/2014/06/exceptions-transformers.html @@ -0,0 +1,78 @@ + Exceptions and monad transformers +

Exceptions and monad transformers

June 10, 2014

GravatarBy Michael Snoyman

Duncan Coutts kicked off a discussion on the core libraries mailing list in +April about exception handling in monad transformers. We made a lot of headway +in that discussion, and agreed to bring up the topic again on the libraries +mailing list, but unfortunately none of us ever got around to that. So I'm +posting a summary of that initial discussion (plus some of my own added +thoughts) to hopefully get a broader discussion going.

The initial thread kicked off with a link to ghc's +ExceptionMonad +typeclass, which encapsulates the ability to catch exceptions, mask async +exceptions, and guarantee cleanup actions (a.k.a. bracket/finally). The +question is: is there a canonical way to do the same thing, without depending +on the ghc library?

As is usually the case in the Haskell ecosystem, the answer is that there are +about five different packages providing this functionality. I'd break them down +into two categories:

  • Packages which define a typeclass (or set of typeclasses) specifically for +exception handling. Such examples include MonadCatchIO-mtl, +MonadCatchIO-transformers, and exceptions.
  • Packages which define a generic way to embed a monad transformer inside the value, and thereby perform any control operation in the base monad. Examples are monad-peel and monad-control (or if you want to go really far in time, neither).

Fortunately, most of those options have been deprecated in favor of +alternatives. Today, there are really two choices: exceptions and +monad-control. I'd like to describe these in a bit more detail, and contrast +some of the pluses and minuses of both approaches. My overall goals are +twofold:

  • Get more knowledge out there about the advantages of the two libraries.
  • Work towards a community consensus on when each library should be used.

I'm interested in the latter point, since having a consistent usage of the +MonadBaseControl vs MonadMask typeclasses in various packages makes it +easier to reuse code.

Note: I don't mean to take credit for the ideas that are expressed in this blog +post. As I said, it's a combination of summary of a previous discussion (mostly +amongst Duncan, Edward, and myself) and a few new thoughts from me.

exceptions

The exceptions package exposes three typeclasses, all specifically geared at +exception handling, and each one incrementally more powerful than the previous +one. MonadThrow is for any monad which can throw exceptions. Some examples of +instances are:

  • IO, where the exception becomes a runtime exception.
  • Either, where the exception becomes the Left value.
  • Maybe, where an exception results in Nothing.
  • Any monad transformer built on top of one of those. (Note also that there's a special CatchT transformer, which keeps the exception in the transformer itself.)

In addition to just throwing exceptions, you often want to be able to catch +exceptions as well. For that, the MonadCatch typeclass is available. However, +some monads (in particular, Maybe) cannot be MonadCatch instances, since +there's no way to recover the thrown exception from a Nothing.

The final typeclass is MonadMask, which allows you to guarantee that certain actions are run, even in the presence of exceptions (both synchronous and asynchronous). In order to provide that guarantee, the monad stack must be able to control its flow of execution. In particular, this excludes instances for two categories of monads:

  • Continuation based monads, since the flow of execution may ignore a callback entirely, or call it multiple times. (For more information, see my previous blog post.)
  • Monads with multiple exit points, such as ErrorT over IO.

And this is the big advantage of the exceptions package vs MonadCatchIO: by +making this distinction between catching and masking, we end up with instances +that are well behaved, and finally functions that guarantee cleanup happens +once, and only once.

One design tradeoff is that all exceptions are implicitly converted to SomeException. An alternate approach is possible, but ends up causing many more problems.

monad-control

monad-control takes a completely different approach. Let's consider the StateT +Int IO monad stack, and consider a function

foo :: StateT String IO Int

Now suppose that I'd like to catch any exceptions thrown by foo, using the +standard try function (specialized to IOException for simplicity):

tryIO :: IO a -> IO (Either IOException a)

Obviously these two functions don't mix together directly. But can we coerce +them into working together somehow? The answer lies in exposing the fact that, +under the surface, StateT just becomes a function in IO returning a tuple. +Working that way, we can write a tryState function:

tryState :: StateT String IO a -> StateT String IO (Either IOException a)
+tryState (StateT f) = StateT $ \s0 -> do
+    eres <- tryIO $ f s0
+    return $ case eres of
+        Left e -> (Left e, s0)
+        Right (x, s1) -> (Right x, s1)

(Full example on School of Haskell.) The technique here is to:

  1. Capture the initial state.
  2. Use tryIO on the raw IO function.
  3. Case analyze the result, either getting an exception or a successful result and new state. Either way, we need to reconstitute the internal state of the transformer, in the former by using the initial state, in the latter, using the newly generated state.

It turns out that this embedding technique can be generalized in two different ways:

  • It applies to just about any IO function, not just exception functions.
  • It applies to many different transformers, and to arbitrarily deep layerings of these transformers.

For examples of the power this provides, check out the lifted-base +package, which includes such +things as thread forking, timeouts, FFI marshaling.

This embedding technique does not work for all transformers. As you've +probably guessed, it does not work for continuation-based monads.

Compare/contrast

Even though these libraries are both being proposed for solving the same +problem (exception handling in transformer stacks), that's actually just a +narrow overlap between the two. Let's look at some of the things each library +handles that the other does not:

  • exceptions allows throwing exceptions in many more monads than monad-control works with. In particular, monad-control can only handle throwing exceptions in IO-based stacks. (Note that this actually has nothing to do with monad-control.)
  • exceptions allows catching exceptions in many more monads as well, in particular continuation based monads.
  • monad-control allows us to do far more than exception handling.

So the overlap squarely falls into: throwing, catching, and cleaning up from +exceptions in a monad transformer stack which can be an instance of +MonadMask/MonadBaseControl, which is virtually any stack without +short-circuiting or continuations, and is based on IO.

Given that the overlap is relatively narrow, the next question is- if you have +a situation that could use either approach- which one should you use? I think +this is something that as a community, we should really try to standardize on. +It would be beneficial from a library user standpoint if it was well accepted +that "oh, I'm going to need a bracket here, so I should use MonadXXX as the +constraint," since it will make library building more easily composable.

To help kick off that discussion, let me throw in my more subjective opinions +on the topic:

  • exceptions is an easier library for people to understand. I've been using +monad-control for a while, and frankly I still find it intimidating.
  • If you're writing a function that might fail, using MonadThrow as the constraint can lead to much better code composability and more informative error messages. (I'm referring back to 8 ways to report errors in Haskell.)
  • exceptions allows for more fine-grained constraints via MonadThrow/MonadCatch/MonadMask.
  • monad-control makes it virtually impossible to write an incorrect instance. It's fairly easy to end up writing an invalid MonadMask instance, however, which could easily lead to broken code. This will hopefully be addressed this documentation and education, but it's still a concern of mine.
  • monad-control requires more language extensions.
  • While there are things that exceptions does that monad-control does not, +those are relatively less common.

Overall, I'm leaning in the direction that we should recommend exceptions as +the standard, and reserve monad-control as a library to be used for the cases +that exceptions doesn't handle at all (like arbitrary control functions). This +is despite the fact that, until now, all of my libraries have used +monad-control. If the community ends up deciding on exceptions, I agree to +(over time) move my libraries in that direction.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/06/google-email2.html b/public/blog/2014/06/google-email2.html new file mode 100644 index 00000000..b050464c --- /dev/null +++ b/public/blog/2014/06/google-email2.html @@ -0,0 +1,26 @@ + GoogleEmail2 auth plugin +

GoogleEmail2 auth plugin

June 5, 2014

GravatarBy Michael Snoyman

Just a short PSA. yesod-auth has shipped with a GoogleEmail module for a while, +which used Google's OpenID system to authenticate users via their email +addresses. It had the nice property of requiring no configuration to get +started, and was therefore included with the scaffolded site as one of two +default authentication plugins (Mozilla Persona/BrowserID being the second).

However, Google has begun deprecating their OpenID services. In particular, new +sites will no longer be able to use OpenID login already, and all OpenID +services will be removed some time next year (IIRC). There are three results of +this that Yesod users should be aware of:

  1. GoogleEmail will no longer be one of the plugins included with the +scaffolded site.
  2. I've released a new version of yesod-auth which includes a new module: GoogleEmail2. This module utilizes the new Google+ login system. The user facing interface is almost identical to what GoogleEmail provides, but you must follow more configuration steps, like getting OAuth credentials from Google. The module documentation contains a list of steps necessary.
  3. If you have a site that uses GoogleEmail, I recommend you immediately begin testing GoogleEmail2, and role it out ASAP.

GoogleEmail2 has not been very thoroughly tested, so please QA your site +before releasing to production. I've already switched over some of my codebases +to using it, but that code is not in production yet, so I don't have detailed +real-world experience to share.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/07/reading-files-proc-filesystem.html b/public/blog/2014/07/reading-files-proc-filesystem.html new file mode 100644 index 00000000..2b2d4c65 --- /dev/null +++ b/public/blog/2014/07/reading-files-proc-filesystem.html @@ -0,0 +1,61 @@ + Reading files from the proc filesystem +

Reading files from the proc filesystem

July 27, 2014

GravatarBy Michael Snoyman

I was stumped by this one myself for a bit today, so I thought writing it up in +a blog post would be a good way to make sure (1) I don't forget this little +fact, and (2) hopefully the next person doesn't need to puzzle over this as +long as I did. Let's say you want to read the contents of a file in the proc +filesystem, such as /proc/uptime. There are many ways to do that in Haskell. +Let's ignore any streaming data framework for the moment, and instead focus on +just the "string-like" types: String and strict/lazy ByteString/Text. +Here's a little program that tries all of them out:

import qualified Data.ByteString      as S
+import qualified Data.ByteString.Lazy as L
+import qualified Data.Text.IO         as T
+import qualified Data.Text.Lazy.IO    as TL
+
+test :: Show a => String -> (FilePath -> IO a) -> IO ()
+test name reader = do
+    contents <- reader "/proc/uptime"
+    putStrLn $ name ++ ": " ++ show contents
+
+main :: IO ()
+main = do
+    test "String           " readFile
+    test "strict ByteString" S.readFile
+    test "lazy ByteString  " L.readFile
+    test "strict Text      " T.readFile
+    test "lazy Text        " TL.readFile

Given that the uptime file is just simple ASCII data, you'd probably assume +(like I did) that all of these will produce the same result. In fact, that's +not the case. On my system, the results are:

String           : "60740.70 136144.86\n"
+strict ByteString: ""
+lazy ByteString  : "60740.70 136144.86\n"
+strict Text      : "60740.70 136144.86\n"
+lazy Text        : "60740.70 136144.86\n"

Strict ByteString reading is returning an empty value! Why is this happening? +It's actually quite easy to see once you throw in two new pieces of +information. First, let's look at the implementation of +Data.ByteString.readFile:

readFile :: FilePath -> IO ByteString
+readFile f = bracket (openBinaryFile f ReadMode) hClose
+    (\h -> hFileSize h >>= hGet h . fromIntegral)

Notice how we allocate a buffer exactly the right size to read in the entire +contents of the file. We don't do this with any of the file reading functions. +For the lazy variants, we don't want to read the entire file into memory at +once. And for strict Text, knowing the size of the file doesn't tell us the +size of the buffer we need to allocate, due to variable length encoding. So +this nifty optimization only applies to strict ByteStrings.

Now piece of data number two:

$ ls -l /proc/uptime 
+-r--r--r-- 1 root root 0 Jul 27 13:56 /proc/uptime

Huh, the file is empty! As is well +documented, +virtually every file in the proc filesystem is listed as empty, and the +contents are generated on demand by the kernel.

So how do you read the file contents into a strict ByteString? There are actually plenty of approaches that work. In my case, I ended up just writing a helper function using conduit:

    localReadFile fp =
+         IO.withBinaryFile fp IO.ReadMode $ \h ->
+         sourceHandle h $$ foldC

But probably the simplest thing to do is to just convert a lazy ByteString +into a strict ByteString, e.g. fmap L.toStrict . L.readFile.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/07/rfc-new-data-conduit-process.html b/public/blog/2014/07/rfc-new-data-conduit-process.html new file mode 100644 index 00000000..c5a8863d --- /dev/null +++ b/public/blog/2014/07/rfc-new-data-conduit-process.html @@ -0,0 +1,162 @@ + RFC: New Data.Conduit.Process +

RFC: New Data.Conduit.Process

July 10, 2014

GravatarBy Michael Snoyman

I've been working on a new iteration of the Data.Conduit.Process API over the +past few days. The current API (provided by the process-conduit package), has +some +issues. +So I'm starting over with a new API, and will be including this in +conduit-extra's next release.

Before releasing, I'd like to get some feedback on the new API. I've put +together a School of Haskell +tutorial, +which will ultimately become the real documentation for this module. It +describes usage of the library, as well as why the library looks the way it +does. You can view the +source +on the process branch of conduit.

In case anyone's wondering, the "well known bug" in waitForProcess may +actually not be very well known yet. There's a race condition documented in +the source code, but it's not nearly as narrow a window as implied there. For +example, the following code will reliably throw an exception:

import System.Process
+import Control.Concurrent.Async
+
+main :: IO ()
+main = do
+    (_, _, _, ph) <- createProcess $ shell "sleep 1"
+    let helper i = do
+            ec <- waitForProcess ph
+            print (i :: Int, ec)
+    ((), ()) <- concurrently (helper 1) (helper 2)
+    return ()

Thus the motivation for fixing the problem in Data.Conduit.Process. Thanks go +to Michael Sloan for discovering the severity of this race condition. In fact, +the bug he ran into, combined with a separate process-conduit bug I ran up +against, were the impetus for me getting this library written now.

For the lazy, here's a copy of the content from School of Haskell:


NOTE: This tutorial documents a not-yet-released version of conduit-extra's Data.Conduit.Process module. Currently, that module name is provided by process-conduit, which provides a completely different API. This tutorial is present now for early feedback. If you'd like to experiment, this code is available on the process branch of the conduit repo.

Introduction

Whenever you run an external process, there are four ways to interact with it post-creation:

  • Write to its standard input
  • Read from its standard output
  • Read from its standard error
  • Check its exit code

The standard System.Process module provides means for all of these interactions. However, there are some downsides with using them:

  • Many of the function in System.Process rely on lazy I/O.
  • There is a subtle race condition when checking for exit codes.
  • Dealing with Handles directly is relatively low-level.

Data.Conduit.Process provides a higher-level interface for these four interactions, based on conduit. It additionally leverages type classes to provide more static type safety than dealing directly with System.Process, as will be described below. The library is also designed to work with the wonderful async library, providing for easy, high-quality concurrency.

Note that providing general parameters for creating a process, such as its working directory or modified environment variables, are not addressed by this module; you should instead use the standard facilities from System.Process.

Synopsis

{-# LANGUAGE OverloadedStrings #-}
+import           Control.Applicative      ((*>))
+import           Control.Concurrent.Async (Concurrently (..))
+import           Control.Concurrent.Async (Concurrently (..))
+import           Data.Conduit             (await, yield, ($$), (=$))
+import qualified Data.Conduit.Binary      as CB
+import qualified Data.Conduit.List        as CL
+import           Data.Conduit.Process     (ClosedStream (..), conduitProcess,
+                                           proc, waitForConduitProcess)
+import           System.IO                (stdin)
+
+main :: IO ()
+main = do
+    putStrLn "Enter lines of data. I'll base64-encode it."
+    putStrLn "Enter \"quit\" to exit."
+
+    ((toProcess, close), fromProcess, ClosedStream, cph) <-
+        conduitProcess (proc "base64" [])
+
+    let input = CB.sourceHandle stdin
+             $$ CB.lines
+             =$ inputLoop
+             =$ toProcess
+
+        inputLoop = do
+            mbs <- await
+            case mbs of
+                Nothing -> close
+                Just "quit" -> close
+                Just bs -> do
+                    yield bs
+                    inputLoop
+
+        output = fromProcess $$ CL.mapM_
+            (\bs -> putStrLn $ "from process: " ++ show bs)
+
+    ec <- runConcurrently $
+        Concurrently input *>
+        Concurrently output *>
+        Concurrently (waitForConduitProcess cph)
+
+    putStrLn $ "Process exit code: " ++ show ec

Exit codes

There's a well documented corner case in waitForProcess whereby multiple calls can end up in a race condition, and therefore a deadlock. Data.Conduit.Process works around this issue by not providing direct access to a ProcessHandle. Instead, it wraps this with a ConduitProcessHandle, which uses an STM TMVar under the surface. This allows you to either poll to check if a process has exited, or block and wait for the process to exit. As a minimal example (ignore the streaming bits for now, they'll be explained shortly).

import Data.Conduit.Process
+
+main :: IO ()
+main = do
+    (Inherited, Inherited, Inherited, cph) <-
+        conduitProcess (shell "sleep 2")
+
+    -- non-blocking
+    getConduitProcessExitCode cph >>= print
+
+    -- blocking
+    waitForConduitProcess cph >>= print

If you need direct access to the ProcessHandle (e.g., to terminate a process), you can use conduitProcessHandleRaw.

Streaming

Now to the main event: streaming data. There are multiple ways you can interact with streams with an external process:

  • Let the child process inherit the stream from the parent process
  • Provide a pre-existing Handle.
  • Create a new Handle to allow more control of the interaction.

One downside of the System.Process API is that there is no static type safety to ensure that the std_out parameter in fact matches up with the value produced by createProcess for the standard output handle. To overcome this, Data.Conduit.Process makes use of type classes to represent the different ways to create a stream. This isn't entirely intuitive from the Haddocks, but once you see the concept used, it's easy to use yourself.

Inherited and ClosedStream

Let's start with an example of using the simplest instances of our typeclasses. Inherited says to inherit the Handle from the parent process, while ClosedStream says to close the stream to the child process. For example, the next snippet will inherit stdin and stdout from the parent process and close standard error.

import Data.Conduit.Process
+
+main :: IO ()
+main = do
+    putStrLn "Just wrapping cat. Use Ctrl-D to exit."
+
+    (Inherited, Inherited, ClosedStream, cph) <-
+        conduitProcess (shell "cat")
+
+    waitForConduitProcess cph >>= print

Note that there's no way to send an EOF in School of Haskell, so the above active code will never terminate.

Conduit

It would be pretty strange to have a library in conduit-extra that didn't provide some conduit capabilities. You can additionally get a Sink to be used to feed data into the process via standard input, and Sources for consuming standard output and error.

This next example reads standard input from the console, process standard output with a conduit, and closes standard error.

import           Data.Conduit         (($$))
+import qualified Data.Conduit.List    as CL
+import           Data.Conduit.Process
+
+main :: IO ()
+main = do
+    putStrLn "Just wrapping cat. Use Ctrl-D to exit."
+
+    (Inherited, src, ClosedStream, cph) <-
+        conduitProcess (shell "cat")
+
+    src $$ CL.mapM_ print
+
+    waitForConduitProcess cph >>= print

Note that these Sources and Sinks will never close their Handles. This is done on purpose, to allow them to be used multiple times without accidentally closing their streams. In many cases, you'll need to close the streams manually, which brings us to our next section.

Conduit + close

Let's say we'd like to close our input stream whenever the user types in "quit". To do that, we need to get an action to close the standard input Handle. This is simple: instead of just returning a Source or Sink, we ask for a tuple of a Source/Sink together with an IO () action to close the handle.

{-# LANGUAGE OverloadedStrings #-}
+import           Data.ByteString      (ByteString)
+import           Data.Conduit         (Source, await, yield, ($$), ($=))
+import qualified Data.Conduit.Binary  as CB
+import           Data.Conduit.Process
+import           System.IO            (stdin)
+
+userInput :: Source IO ByteString
+userInput =
+       CB.sourceHandle stdin
+    $= CB.lines
+    $= loop
+  where
+    loop = do
+        mbs <- await
+        case mbs of
+            Nothing -> return ()
+            Just "quit" -> return ()
+            Just bs -> do
+                yield bs
+                yield "\n"
+                loop
+
+main :: IO ()
+main = do
+    putStrLn "Just wrapping cat. Type \"quit\" to exit."
+
+    ((sink, close), Inherited, ClosedStream, cph) <-
+        conduitProcess (shell "cat")
+
+    userInput $$ sink
+    close
+
+    waitForConduitProcess cph >>= print

UseProvidedHandle

Let's take a quick detour from our running example to talk about the last special type: UseProvidedHandle. This says to conduitProcess: use the example value of UseHandle provided in std_in/std_out/std_err. We can use this to redirect output directly to a file:

import Data.Conduit.Process
+import System.IO (withFile, IOMode (..))
+
+main :: IO ()
+main = do
+    let fp = "date.txt"
+    withFile fp WriteMode $ \h -> do
+        (ClosedStream, UseProvidedHandle, ClosedStream, cph) <-
+            conduitProcess (shell "date")
+                { std_out = UseHandle h
+                }
+        waitForConduitProcess cph >>= print
+    readFile fp >>= print

Use with async

In our examples above, we only ever used a single Source or Sink at a time. There's a good reason for this: we can easily run into deadlocks if we don't properly handle concurrency. There are multiple ways to do this, but I'm going to strongly recommend using the async package, which handles many corner cases automatically. In particular, the Concurrently data type and its Applicative instance make it easy and safe to handle multiple streams at once.

Instead of duplicating it here, I'll ask the reader to please refer back to the synopsis example, which ties this all together with two threads for handling streams, and another thread which blocks waiting for the process to exit. That style of concurrency is very idiomatic usage of this library.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/08/announcing-auto-update.html b/public/blog/2014/08/announcing-auto-update.html new file mode 100644 index 00000000..779cc0f8 --- /dev/null +++ b/public/blog/2014/08/announcing-auto-update.html @@ -0,0 +1,116 @@ + Announcing auto-update +

Announcing auto-update

August 6, 2014

GravatarBy Michael Snoyman

Kazu and I are happy to announce the first release of +auto-update, a +library to run update actions on a given schedule. To make it more concrete, +let's start with a motivating example.

Suppose you're writing a web service which will return the current time. This +is simple enough with WAI and Warp, e.g.:

{-# LANGUAGE OverloadedStrings #-}
+import           Data.ByteString.Lazy.Char8 (pack)
+import           Data.Time                  (formatTime, getCurrentTime)
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (responseLBS)
+import           Network.Wai.Handler.Warp   (run)
+import           System.Locale              (defaultTimeLocale)
+
+main :: IO ()
+main =
+    run 3000 app
+  where
+    app _ respond = do
+        now <- getCurrentTime
+        respond $ responseLBS status200 [("Content-Type", "text/plain")]
+                $ pack $ formatTime defaultTimeLocale "%c" now

This is all well and good, but it's a bit inefficient. Imagine if you have a +thousand requests per second (some people really like do know what time it +is). We will end up recalculating the string representation of the time a 999 +extra times than is necessary! To work around this, we have a simple solution: +spawn a worker thread to calculate the time once per second. (Note: it will +actually calculate it slightly less than once per second due to the way +threadDelay works; we're assuming we have a little bit of latitude in +returning a value thats a few milliseconds off.)

{-# LANGUAGE OverloadedStrings #-}
+import           Control.Concurrent         (forkIO, threadDelay)
+import           Control.Monad              (forever)
+import           Data.ByteString.Lazy.Char8 (ByteString, pack)
+import           Data.IORef                 (newIORef, readIORef, writeIORef)
+import           Data.Time                  (formatTime, getCurrentTime)
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (responseLBS)
+import           Network.Wai.Handler.Warp   (run)
+import           System.Locale              (defaultTimeLocale)
+
+getCurrentTimeString :: IO ByteString
+getCurrentTimeString = do
+    now <- getCurrentTime
+    return $ pack $ formatTime defaultTimeLocale "%c" now
+
+main :: IO ()
+main = do
+    timeRef <- getCurrentTimeString >>= newIORef
+    _ <- forkIO $ forever $ do
+        threadDelay 1000000
+        getCurrentTimeString >>= writeIORef timeRef
+    run 3000 (app timeRef)
+  where
+    app timeRef _ respond = do
+        time <- readIORef timeRef
+        respond $ responseLBS status200 [("Content-Type", "text/plain")] time

Now we will calculate the current time once per second, which is far more +efficient... right? Well, it depends on server load. Previously, we talked +about a server getting a thousand requests per second. Let's instead reverse +it: a server that gets one request every thousand seconds. In that case, our +optimization turns into a pessimization.

This problem doesn't just affect getting the current time. Another example is +flushing logs. A hot web server could be crippled by flushing logs to disk on +every request, whereas flushing once a second on a less popular server simply +keeps the process running for no reason. One option is to put the power in the +hands of users of a library to decide how often to flush. But often times, we +won't know until runtime how frequently a service will be requested. Or even +more complicated: traffic will come in spikes, with both busy and idle times.

(Note that I've only given examples of running web servers, though I'm certain +there are plenty of other examples out there to draw from.)

This is the problem that auto-update comes to solve. With auto-update, you +declare an update function, a frequency with which it should run, and a +threshold at which it should "daemonize". The first few times you request a +value, it's calculated in the main thread. Once you cross the daemonize +threshold, a dedicated worker thread is spawned to recalculate the value. If +the value is not requested during an update period, the worker thread is shut +down, and we go back to the beginning.

Let's see how our running example works out with this:

{-# LANGUAGE OverloadedStrings #-}
+import           Control.AutoUpdate         (defaultUpdateSettings,
+                                             mkAutoUpdate, updateAction)
+import           Data.ByteString.Lazy.Char8 (ByteString, pack)
+import           Data.Time                  (formatTime, getCurrentTime)
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (responseLBS)
+import           Network.Wai.Handler.Warp   (run)
+import           System.Locale              (defaultTimeLocale)
+
+getCurrentTimeString :: IO ByteString
+getCurrentTimeString = do
+    now <- getCurrentTime
+    return $ pack $ formatTime defaultTimeLocale "%c" now
+
+main :: IO ()
+main = do
+    getTime <- mkAutoUpdate defaultUpdateSettings
+        { updateAction = getCurrentTimeString
+        }
+    run 3000 (app getTime)
+  where
+    app getTime _ respond = do
+        time <- getTime
+        respond $ responseLBS status200 [("Content-Type", "text/plain")] time

If you want to see the impact of this change, add a putStrLn call to +getCurrentTimeString and make a bunch of requests to the service. You should +see just one request per second, once you get past that initial threshold +period (default of 3).

Kazu and I have started using this library in a few places:

  • fast-logger no longer requires explicit flushing; it's handled for you automatically.
  • wai-logger and wai-extra's request logger, by extension, inherit this functionality.
  • Warp no longer has a dedicated thread for getting the current time.
  • The Yesod scaffolding was able to get rid of an annoying bit of commentary.

Hopefully others will enjoy and use this library as well.

Control.Reaper

The second module in auto-update is Control.Reaper. This provides something +similar, but slightly different, from Control.AutoUpdate. The goal is to +spawn reaper/cleanup threads on demand. These threads can handle such things +as:

  • Recycling resources in a resource pool.
  • Closing out unused connections in a connection pool.
  • Terminating threads that have overstayed a timeout.

This module is currently being used in Warp for slowloris timeouts and file +descriptor cache management, though I will likely use it in http-client in the +near future as well for its connection manager management.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/08/announcing-persistent-2.html b/public/blog/2014/08/announcing-persistent-2.html new file mode 100644 index 00000000..f75e2f25 --- /dev/null +++ b/public/blog/2014/08/announcing-persistent-2.html @@ -0,0 +1,44 @@ + Announcing Persistent 2 +

Announcing Persistent 2

August 29, 2014

GravatarBy Greg Weber

We are happy to announce the release of persistent 2.0

persistent 2.0 adds a flexible key type and makes some breaking changes. 2.0 is an unstable release that we want your feedback on for the soon to follow stable 2.1 release.

New Features

  • type-safe composite primary and foreign keys
  • added an upsert operation (update or insert)
  • added an insertMany_ operation

Fixes

  • An Id suffix is no longer automatically assumed to be a Persistent type
  • JSON serialization +* MongoDB ids no longer have a prefix 'o' character.

Breaking changes

  • Use a simple ReaderT for the underlying connection
  • fix postgreSQL timezone storage
  • remove the type parameter from EntityDef and FieldDef

In depth

Composite keys

The biggest limitation of data modeling with persistent is an assumption of a simple (for current SQL backends an auto-increment) primary key. +We learned from Groundhog that a more flexible primary key type is possible. Persistent adds a similar flexible key type while maintaining its existing invariant that a Key is tied to a particular table.

To understand the changes to the Key data type, lets look at a change in the test suite for persistent 2.

       i <- liftIO $ randomRIO (0, 10000)
+-      let k = Key $ PersistInt64 $ abs i
++      let k = PersonKey $ SqlBackendKey $ abs i

Previously Key contained a PersistValue. This was not type safe. PersistValue is meant to serialize any basic Haskell type to the database, but a given table only allows specific values as the key. +Now we generate the PersonKey data constructor which specifies the Haskell key types. +SqlBackendKey is the default key type for SQL backends.

Now lets look at code from CompositeTest.hs

mkPersist sqlSettings [persistLowerCase|
+  Parent
+      name  String maxlen=20
+      name2 String maxlen=20
+      age Int
+      Primary name name2 age
+      deriving Show Eq
+  Child
+      name  String maxlen=20
+      name2 String maxlen=20
+      age Int
+      Foreign Parent fkparent name name2 age
+      deriving Show Eq
+|]
+

Here Parent has a composite primary key made up of 3 fields. +Child uses that as a foreign key. +The primary key of Child is the default key for the backend.

let parent = Parent "a1" "b1" 11
+let child = Child "a1" "b1" 11
+kp <- insert parent
+_ <- insert child
+testChildFkparent child @== parent

Future changes

Short-term improvements

Before the 2.1 release I would like to look at doing some simple things to speed up model compilation a little bit.

  • Speed up some of the compile-time persistent code (there is a lot of obviously naive code).
  • Reduce the size of Template Haskell generation (create a reference for each EntityDef and some other things rather than potentially repeatedly inlining it)

Medium-term improvement: better support for Haskell data types

We want to add better support for modeling ADTs, particularly for MongoDB where this is actually very easy to do in the database itself. Persistent already support a top-level entity Sum Type and a simple field ADT that is just an enumeration.

Another pain point is serializing types not declared in the schema. The declaration syntax in groundhog is very verbose but allows for this. So one possibility would be to allow the current DRY persistent declaration style and also a groundhog declaration style.

Long-term improvements: Projections

It would be possible to add projections now as groundhog or esqueleto have done. However, the result is not as end-user friendly as we would like. +When the record namespace issue is dealt with in the GHC 7.10 release we plan on adding projections to persistent.

Ongoing: Database specific functionality

We always look forward to see more databases adapters for persistent. +In the last year, a Redis and ODBC adapter were added.

Every database is different though, and you also want to take advantage of your database-specific features. +esqueleto and persistent-mongoDB have shown how to build database specific features in a type-safe way on top of persistent.

Organization

Although the persistent code has no dependency on Yesod, I would like to make the infrastructure a little more independent of yesod. The first steps would be

  • putting it under a different organization on github.
  • having a separate mail list (should stackoverflow be prioritized over e-mail?)

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/08/deprecating-yesod-platform.html b/public/blog/2014/08/deprecating-yesod-platform.html new file mode 100644 index 00000000..882b9c25 --- /dev/null +++ b/public/blog/2014/08/deprecating-yesod-platform.html @@ -0,0 +1,53 @@ + Deprecating yesod-platform +

Deprecating yesod-platform

August 9, 2014

GravatarBy Michael Snoyman

I want to deprecate the yesod-platform, and instead switch to Stackage server +as the recommended installation method for Yesod for end users. To explain why, +let me explain the purpose of yesod-platform, the problems I've encountered +maintaining it, and how Stackage +Server can +fit in. I'll also explain some unfortunate complications with Stackage Server.

Why yesod-platform exists

Imagine a simpler Yesod installation path:

  1. cabal install yesod-bin, which provides the yesod executable.
  2. yesod init to create a scaffolding.
  3. cabal install inside that directory, which downloads and installs all of the necessary dependencies.

This in fact used to be the installation procedure, more or less. However, this +led to a number of user problems:

  • Back in the earlier days of cabal-install, it was difficult for the dependency solver to find a build plan in this situation. Fortunately, cabal-install has improved drastically since then.
    • This does still happen occasionally, especially with packages with restrictive upper bounds. Using --max-backjumps=-1 usually fixes that.
  • It sometimes happens that an upstream package from Yesod breaks Yesod, either by changing an API accidentally, or by introducing a runtime bug.

This is where yesod-platform comes into play. Instead of leaving it up to +cabal-install to track down a consistent build plan, it specifies exact +versions of all depedencies to ensure a consistent build plan.

Conflicts with GHC deps/Haskell Platform

Yesod depends on aeson. So logically, yesod-platform should have a strict +dependency on aeson. We try to always use the newest versions of dependencies, +so today, that would be aeson == 0.8.0.0. In turn, this demands text >= +1.1.1.0. However, if you look at the Haskell Platform +changelog, there's no version +of the platform that provides a new enough version of text to support that +constraint.

yesod-platform could instead specify an older version of aeson, but that would +unnecessarily constrain users who aren't sticking to the Haskell Platform +versions (which, in my experience, is the majority of users). This would also +cause more dependency headaches down the road, as you'd now also need to force +older versions of packages like criterion.

To avoid this conflict, yesod-platform has taken the approach of simply +omitting constraints on any packages in the platform, as well as any packages +with strict bounds on those packages. And if you look at yesod-platform today, +you'll that there is no mention of aeson or text.

A similar issue pops up for packages that are a dependency of the GHC package +(a.k.a., GHC-the-library). The primary problem there is the binary package. In +this case, the allowed version of the package depends on which version of GHC +is being used, not the presence or absence of the Haskell Platform.

This results in two problems:

  • It's very difficult to maintain this list of excluded packages correctly. I +get large number of bug reports about these kinds of build plan problems.

  • We're giving up quite a bit of the guaranteed buildability that +yesod-platform was supposed to provide. If aeson 0.7.0.4 (as an example) +doesn't work with yesod-form, yesod-platform won't be able to prevent such a +build plan from happening.

There's also an issue with the inability to specify dependencies on +executable-only packages, like alex, happy, and yesod-bin.

Stackage Server

Stackage Server solves exactly the same problem. It provides a consistent set +of packages that can be installed together. Unlike yesod-platform, it can be +distinguished based on GHC version. And it's far simpler to maintain. Firstly, +I'm already maintaining Stackage Server full time. And secondly, all of the +testing work is handled by a very automated process.

So here's what I'm proposing: I'll deprecate the yesod-platform package, and change the Yesod quickstart guide to have the following instructions:

  • Choose an appropriate Stackage snapshot from stackage.org
  • Modify your cabal config file appropriately
  • cabal install yesod-bin alex happy
  • Use yesod init to set up a scaffolding
  • cabal install --enable-tests in the new directory

For users wishing to live on more of a bleeding edge, the option is always +available to simply not use Stackage. Such a usage will give more control over +package versions, but will also lack some stability.

The problems

There are a few issues that need to be ironed out.

  • cabal sandbox does not allow changing the remote-repo. Fortunately, Luite seems to have this solved, so hopefully this won't be a problem for long. Until then, you can either use a single Stackage snapshot for all your development, or use a separate sandboxing technology like hsenv.

  • Haskell Platform conflicts still exist. The problem I mentioned above with aeson and text is a real problem. The theoretically correct solution is to create a Stackage snapshot for GHC 7.8 + Haskell Platform. And if there's demand for that, I'll bite the bullet and do it, but it's not an easy bullet to bite. But frankly, I'm not hearing a lot of users saying that they want to peg Haskell Platform versions specifically.

    In fact, the only users who really seem to want to stick to Haskell Platform versions are Windows users, and the main reason for this is the complexity in installing the network package on Windows. I think there are three possible solutions to this issue, without forcing Windows users onto old versions of packages:

    1. Modify the network package to be easier to install on Windows. I really hope this has some progress. If this is too unstable to be included in the official Hackage release, we could instead have an experimental Stackage snapshot for Windows with that modification applied.
    2. Tell Windows users to simply bypass Stackage and yesod-platform, with the possibility of more build problems on that platform.
      • We could similarly recommend Windows users develop in a Linux virtual machine/Docker image.
    3. Provide a Windows distribution of GHC + cabal-install + network. With the newly split network/network-uri, this is a serious possibility.

Despite these issues, I think Stackage Server is a definite improvement on +yesod-platform on Linux and Mac, and will likely still improve the situation on +Windows, once we figure out the Haskell Platform problems.

I'm not making any immediate changes. I'd very much like to hear people using +Yesod on various operating systems to see how these changes will affect them.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/09/announcing-yesod-1-4.html b/public/blog/2014/09/announcing-yesod-1-4.html new file mode 100644 index 00000000..384d61ae --- /dev/null +++ b/public/blog/2014/09/announcing-yesod-1-4.html @@ -0,0 +1,51 @@ + Announcing Yesod 1.4 +

Announcing Yesod 1.4

September 30, 2014

GravatarBy Greg Weber

We are happy to announce the release of Yesod 1.4. This includes:

  • Releases of all Yesod packages to support version 1.4.
  • The book content on yesodweb.com is completely updated for Yesod 1.4, with all snippets confirmed to compile and most of the text proofread from scratch for accuracy (in the next week the rest will be finished).
  • A new Stackage snapshot available for GHC 7.8.3.

Its worth mentioning that there have been a ton of improvements to Yesod since version 1.2, they just didn't need any breaking changes.

Thanks to everyone who provided code, feedback, and testing for this release, it +should be a very solid one!

Here's a collection of links that provide various other pieces of information about this release:

Changelog

What is most exciting to report is that this was a very minor change to Yesod, and +therefore most code should be upgradeable with minor changes. First, the +changelog of breaking changes:

New routing system with more overlap checking control

This requires OverloadedStrings and ViewPatterns. +The generated code is faster and much more readable.

Yesod routes are not just type-safe, they also check for overlapping that could cause ambiguity. This is a great feature, but sometimes it gets in your way. +Overlap checking can be turned off for multipieces, entire routes, and parent routes in a hierarchy. For more information, see the commit comment.

Dropped backwards compatibility with older versions of dependencies

In particular, persistent-1 and wai-2. We will talk more about persistent 2. +wai-3 uses a CPS style that will require some middleware to have an additional CPS parameter. +Looking at the wai-extra source code can help with upgrading, but it should just be adding an extra parameter.

yesod-auth works with your database and your JSON

There is better support for non-persistent backends in yesod-auth. See pull request 821 for details. For most users, you can fix this by adding instance YesodAuthPersist App to your Foundation.hs.

yesod-auth already released a breaking change to be able to accept JSON everywhere. +That bumped the version to 1.3 +We like to keep the yesod-* packages in sync, so now everything is getting bumped to 1.4 together.

In the 1.4 release, we also fixed requireAuth and and requireAuthId to return a 401 response when a JSON response is requested. See pull request 783.

yesod-test sends HTTP/1.1 as the version

This may require updating tests to expect 303 instead of 302 redirects.

Type-based caching with keys.

The Type-based caching code was moved into a separate module without Yesod dependencies and documented. If there is interest in seeing this as a separate package let us know, but it is also pretty easy to just copy the module.

To me, TypeCache is a beautiful demonstration of Haskell's advanced type system that shows how you can get the best of both worlds in a strongly typed language.

type TypeMap      = HashMap TypeRep Dynamic

Above we have the wonderful juxtaposition of Haskell's strong typing in the Key, and dynamic typing in the value. This HashMap is used to cache the result of a monadic action.

cached :: (Monad m, Typeable a) 
+       => TypeMap
+       -> m a                       -- ^ cache the result of this action
+       -> m (Either (TypeMap, a) a) -- ^ Left is a cache miss, Right is a hit

Dynamic is used to have a HashMap of arbitrary value types. +TypeRep is used to create a unique key for the cache. +Yesod uses this to cache the authentication lookup of the database for the duration of the request.

newtype CachedMaybeAuth val = CachedMaybeAuth { unCachedMaybeAuth :: Maybe val }
+    deriving Typeable
+
+cachedAuth
+    = fmap unCachedMaybeAuth
+    . cached
+    . fmap CachedMaybeAuth
+    . getAuthEntity

CachedMaybeAuth is a newtype that isn't exported. TypeRep is specific to a module, so this pattern guarantees that your cache key will not conflict outside of your module.

This functionality was in yesod-1.2 even though the code was not separated into a new module. +The 1.4 release adds the ability to cache multiple values per type

type KeyedTypeMap = HashMap (TypeRep, ByteString) Dynamic
+
+cachedBy :: (Monad m, Typeable a)
+         => KeyedTypeMap
+         -> ByteString                     -- ^ a cache key
+         -> m a                            -- ^ cache the result of this action
+         -> m (Either (KeyedTypeMap, a) a) -- ^ Left is a cache miss, Right is a hit

This is useful if your monadic action has inputs: if you serialize them to a ByteString you can use thm as a key.

Upgrade guide

The most significant set of changes in the Yesod ecosystem actually landed in +Persistent 2. However, these were mostly internal changes with new features that maintain backwards compatibility, so many users will be unaffected.

To kickoff the upgrade process, you need to change update your cabal file to allow yesod version 1.4. +If you had constraints on persistent, update them to > 2.1 +If you are using cabal freeze to peg your versions in the cabal.config file, cabal will provide you no assistance in making a smooth upgrae. +You are probably going to want to delete a whole lot of things in cabal.config (or possibley the entire file), and upgrade a lot of dependencies at once. +When you are done and things compile again, you will want to do a cabal freeze

As has become the custom for each major release, the upgrade +process is documented by the diff of the Haskellers code base upgrading to Yesod 1.4. +For Haskellers it was pretty simple.

In sum:

  • Replace type YesodPersistBackend App = SqlPersist with type YesodPersistBackend App = SqlBackend.
  • Add instance YesodAuthPersist App to Foundation.hs.
  • Add the ViewPatterns language extension.

If you have more complex persistent code you may have more to do. +Look at the previous post on persistent-2.1

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/09/clarification-previous-blog-post.html b/public/blog/2014/09/clarification-previous-blog-post.html new file mode 100644 index 00000000..dd2e4e0f --- /dev/null +++ b/public/blog/2014/09/clarification-previous-blog-post.html @@ -0,0 +1,28 @@ + Clarification of previous blog post +

Clarification of previous blog post

September 11, 2014

GravatarBy Michael Snoyman

I've heard that my previous blog +post has +caused a bit of confusion, as sarcasm doesn't really come across in text very +well. So let me elaborate (and of course, in the process, kill the joke):

Some years back, Erik found a case that was quite difficult to implement using +enumerator. After we cracked our heads on it for long enough, some of us (I +don't actually remember who was involved) decided to work on a new streaming +library. That library ended up being called conduit (thanks to Yitz for the +naming idea). It turns out that most people are unaware of that history, so +when at ICFP, I casually mentioned that Erik was the cause of conduit coming +into existence, some people were surprised. Erik jokingly chastised me for +not giving him enough credit. In response, I decided to write an over-the-top +post giving Erik all credit for conduit. I say over the top, since I made +it seem like there was some large amount of blame being heaped on as well.

So to be completely clear:

  • Erik and I are good friends, and this was just a bit of an inside joke turned public.
  • No one has said anything offensive to me at all about conduit. There are obviously differing opinions out there about the best library for a job, but there's nothing offensive about it, just healthy discussion around a complicated topic. My purpose in making a big deal about it was not to express frustration at anyone attacking me, but rather to just play up the joke a bit more.

My apologies to anyone who was confused, upset, or worried by the previous +post, it was completely unintentional.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/09/misassigned-credit-for-conduit.html b/public/blog/2014/09/misassigned-credit-for-conduit.html new file mode 100644 index 00000000..6999316c --- /dev/null +++ b/public/blog/2014/09/misassigned-credit-for-conduit.html @@ -0,0 +1,22 @@ + Misassigned credit for conduit +

Misassigned credit for conduit

September 9, 2014

GravatarBy Michael Snoyman

When I was at ICFP last week, it became clear that I had made a huge mistake in +the past three years. A few of us were talking, including Erik de Castro Lopo, +and when I mentioned that he was the original inspiration for creating the +conduit package, everyone else was surprised. So firstly: Erik, I apologize for +not making it clear that you initially kicked off development by finding some +fun corner cases in enumerator that were difficult to debug.

So to rectify that, I think it's only fair that I write the following:

  • conduit is entirely Erik's fault.
  • If you love conduit, write Erik a thank you email.
  • More importantly, if you hate conduit, there's no need to complain to me anymore. Erik presumably will be quite happy to receive all such further communications.
  • In other words, it's not my company, I just work here.

Thanks Erik :)

UPDATE Please also read my follow-up blog +post clarifying this one, just +in case you're confused.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/09/persistent-2.1-rc.html b/public/blog/2014/09/persistent-2.1-rc.html new file mode 100644 index 00000000..b2d67f2f --- /dev/null +++ b/public/blog/2014/09/persistent-2.1-rc.html @@ -0,0 +1,19 @@ + Persistent 2.1 Release Candidate +

Persistent 2.1 Release Candidate

September 16, 2014

GravatarBy Greg Weber

We are happy to announce a stable persistent 2 release candidate.

We previously announced an unstable release of persistent 2. It was a good idea to call it unstable because some commenters pointed out that we were not exposing the full power of the new flexible Key type. This lead to a couple of breaking releases that organizied the internal types of persistent. All of these are on the unstable 2.0.x series.

persistent-2.0.3.* is on hackage now. We consider this a release candidate that will be promoted to persistent-2.1. We may wait until the haddocks build on hackage before releasing.

Ongoing persistent maintainership

Persistent is of huge importance to the Hasekll community, playing the role of default ORM. +The project has benefited immensely from the community involvement. +We get a lot of prompt bug reports that often include fixes. +And there have been many great new features added by contributors.

However, persistent it is lacking in dedicated maintainers for each backend. We would like for Michael to keep doing a great job stewarding the project, but for others to step up and help own a SQL backend.

An extreme example of a lack of backend maitenance is when we received a pull request for a CouchDB backend. It was great to share the code as a starting point, but it was already using an older version of persistent and is now sitting in the experimental folder in a bit-rotted state.

In general a persistent backend can only be first class and in our tree with a dedicated maintainer.
Michael and I maintain persistent and persistent-template. I maintain the persitent-mongoDB backend. The issue now is more with the SQL backends, where the maitenance and development for them is being pushed on Michael. For example, I implemented custom Key types in persistent, persistent-template, and persistent-mongoDB. Michael and myself implemented them for persistent-sqlite, but it still needs to be implement for persistent-postgressql and persistent-mysql.

Maintaining persitent and persitent-template has had a questionable cost/benefit ratio for me. But I have personally found that maintaing the persistent-mongoDB backend has paid off well for me. +I need to have a good understanding of what is happening with my code that deals with the database. Rather than treating it as a black box I make continous incremental improvements to the library that I rely on, and I can smoothly get code onto hackage rather than having local modifications.

Let us know if you are interested in helping to maintain a backend.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/09/persistent-2.1-released.html b/public/blog/2014/09/persistent-2.1-released.html new file mode 100644 index 00000000..5fd3f91f --- /dev/null +++ b/public/blog/2014/09/persistent-2.1-released.html @@ -0,0 +1,46 @@ + Persistent 2.1 released +

Persistent 2.1 released

September 29, 2014

GravatarBy Greg Weber

Persistent 2.1, a stable release of the next generation of persistent is released to Hackage.

Persistent is an ORM for Haskell that keeps everything type-safe.

Persistent 2.1 features

  • a flexible, yet more type-safe Key type
  • a simplified monad stack

I already announced persistent 2 +and the 2.1 release candidate.

Everyone should set their persistent dependencies to > 2.1 && < 3. 2.0.x was the unstable release and is now deprecated.

I want to thank all the early persistent 2 adopters for putting up with a fast-moving, buggy code base. This was an experiment in shipping an unstable version, and what I learned from it is that it was a great process, but we need to make sure Travis CI is running properly, which it is now!

Persistent 2.1 library support

The persistent and persistent-template libraries should support any kind of primary key type that you need. The backends are still catching up to the new features

  • persistent-sqlite backend has fully implemented these features.
  • persistent-postgres and persitent-mysql don't yet support changing the type of the id field
  • persistent-mongoDB does not yet support composite primary keys

All of the above packages except persistent-mysql are being well maintained, but just developing new features at their own pace. persistent-mysql is in the need of a dedicated maintainer. There are some major defects in the migration code that have gone unresolved for a long time now.

  • persistent-redis is in the process of being upgraded to 2.1
  • persistent-zookeeper was just released, but it is on persistent 1.3.*
  • There are other persistent packages out there that I have not had the chance to check on yet, most noteably persistent-odbc. Feel free to ask for help with upgrading.

Persistent 2.1 upgrade guide

Simple persistent usage may not need any changes to upgrade.

The fact that the Key type is now flexible means it may need to be constrained. +So if you have functions that have Key in the type signature that are not specific to one PersistEntity, you may need to constrain them to the BackendKey type. +An easy way to do this is using ConstraintKinds.

type DBEntity record =
+    ( PersistEntityBackend record ~ MongoContext
+    , PersistEntity record
+    , ToBackendKey MongoContext record
+    )

A SQL user would use SqlBackend instead of MongoContext. So you can now change the type signature of your functions:

- PersistEntity record => Key record
++ DBEntity record => Key record

Depending on how you setup your monad stacks, you may need some changes. +Here is one possible approach to creating small but flexible Monad stack type signatures. +It requires Rank2Types, and the code show is specialized to MongoDB.

type ControlIO m = ( MonadIO m , MonadBaseControl IO m)
+type LogIO m = ( MonadLogger m , ControlIO m)
+
+-- these are actually types, not constraints
+-- with persistent-2 things work out a lot easier this way
+type DB    a =  LogIO m => ReaderT MongoContext m a
+type DBM m a =  LogIO m => ReaderT MongoContext m a

The basic type signature is just DB () (no constraints required). +For working with different monad stacks, you can use DBM. +If you are using conduits, you will have MonadResource m => DBM m (). +Here is another example:

class Monad m => HasApp m where
+    getApp :: m App 
+instance HasApp Handler where
+    getApp = getYesod
+instance HasApp hasApp => HasApp (ReaderT MongoContext hasApp) where
+    getApp = lift $ getApp
+instance MonadIO m => HasApp (ReaderT App m) where
+    getApp = ask 
+
+-- | synonym for DB plus HasApp operations
+type DBApp    a = HasApp m => DBM m a 
+type DBAppM m a = HasApp m => DBM m a 

With this pattern our return type signature is always ReaderT MongoContext m, and we are changing m as needed. A different approach is to have a return type signature of m and to place a MonadReader constraint on it.

type Mongo m = (LogIO m, MonadReader MongoContext m)

Right now this approach requires using a call to +Database.MongoDB.liftDB around each database call, but I am sure there are approaches to dealing with that. One approach would be to wrap every persistent "primitive" with liftDB.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/09/planning-yesod-1-4.html b/public/blog/2014/09/planning-yesod-1-4.html new file mode 100644 index 00000000..6df0cc8b --- /dev/null +++ b/public/blog/2014/09/planning-yesod-1-4.html @@ -0,0 +1,48 @@ + Planning Yesod 1.4 +

Planning Yesod 1.4

September 1, 2014

GravatarBy Michael Snoyman

Now that persistent 2.0 is out the +door, it's time +to start talking about Yesod version 1.4. First question you might ask: what +happened to Yesod 1.3? Answer: a few of the Yesod libraries (e.g., yesod-auth) +are already on version 1.3, so to avoid confusion, we're jumping straight to +1.4.

Second question: what are we planning on breaking this time? Answer: hopefully +nothing! The main purpose of this release is actually to just remove some +backwards-compatibility hacks in the Yesod codebase for older versions of +dependencies, like shakespeare pre-2.0, conduit pre-1.2, WAI pre-3.0, and +persistent pre-2.0.

There are few exceptions to this, which should hopefully have minimal impact on +users. You can see these in the detailed change +list. +One change I'd like to call out is the updated routing system. This is a +fundamental change to how yesod-routes works. The generated code is +drastically simpler as a result. Instead of constructing a data structure +that allows for efficient pattern matching of the request path and then +attempting to parse the resulting pieces, the new code simply generates a +series of clauses, one for each route, and ensures proper parsing using view +patterns. In my initial benchmarking, this made routing twice as fast as Yesod +1.2. I would release this as part of 1.2, but it introduces a new requirement +on the ViewPatterns language extension. So instead, I held it off for the 1.4 +release.

If there are other breaking changes that people would like to propose, now's +the time to do it. But be aware that I'll likely push back hard on any +breakage. If there's a very good reason for it, we can do it. But I'd rather +keep stability wherever possible.

There's one exception to that rule, which is the purpose of the rest of this +blog post: the scaffolded site. Making changes to the scaffolded site never +breaks existing application, and therefore we can be much more liberal about +changing things there. There is a downside in terms of education: all +existing tutorials on the scaffolding would need to be updated. But one of my +points below addresses that.

So here are my proposed scaffolding changes:

  • Let's move away from config files towards environment variables for configuration. A config file is still a convenient way to record configuration, but injecting that configuration through environment variables means configuration can also be stored in a database or elsewhere and injected through environment variables the same way.
  • Along the same lines, we would no longer need a command line argument to indicate which environment we're in (devel vs production, etc). All such settings would be controlled via environment variables.
  • To allow for easy development, we would have a single YESOD_DEVEL environment variables which would indicate if we're currently in development. If so, it would apply a number of default environment variable values to avoid the need to set these in your shell manually.
  • Finally, and I expect this to be controversial: let's use classy-prelude-yesod in the Import module, instead of just taking Prelude with a few objectionable functions filtered out.

This is just a first pass at a scaffolding cleanup, I'm sure there are other improvements that can be made as well.

I don't have a specific date on a Yesod 1.4 release, but I'm not expecting it +to be a long development process. The vast majority of the work is already done +(on the yesod-1.4 branch), and that codebase is already being used extensively +in a rather large Yesod application, so I'm not +too worried about regressions having slipped in.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/09/woes-multiple-package-versions.html b/public/blog/2014/09/woes-multiple-package-versions.html new file mode 100644 index 00000000..5e3cf752 --- /dev/null +++ b/public/blog/2014/09/woes-multiple-package-versions.html @@ -0,0 +1,114 @@ + The woes of multiple package versions +

The woes of multiple package versions

September 23, 2014

GravatarBy Michael Snoyman

When I've answered the same question more than three times, it's usually time +to write up a blog post explaining the situation in more detail, and just link +people to that in the future. This is such a blog post.

Many people working with Haskell end up with one of these two classes of +inexplicable GHC errors:

  1. Cannot match ByteString with ByteString, with a whole bunch of package +name and version junk as well.

  2. SomeTransformerT is not an instance of MonadTrans, when the +documentation clearly indicates that SomeTransformerT does define such +an instance.

How can a ByteString not be a ByteString? Well, there are two ways I can +think of. The first is that you're accidentally trying to mix up a strict +ByteString with a lazy ByteString, whose types clearly don't unify. While +that problem does pop up, in my experience most people figure that one out +pretty quickly. By the time someone's asking a question on Stack +Overflow/Reddit/haskell-cafe, it's usually much more insidious: there are two +copies of bytestring on their system.

Imagine this situation: you install GHC 7.6, which ships with +bytestring-0.10.0.2. You then install text via cabal install text. A few days +later, someone mentions that bytestring-0.10.4.0 is out, and it's all new and +shiny, so you go ahead and install it with cabal install bytestring. +Everything works wonderfully, and life is good. Then you decide to experiment +with text a bit, so you write the following program:

{-# LANGUAGE OverloadedStrings #-}
+import Data.Text.Encoding (encodeUtf8)
+import qualified Data.ByteString.Char8 as S8
+
+main :: IO ()
+main = S8.putStrLn $ encodeUtf8 "Hello World!"

Woe unto you! GHC rejects your program with:

foo.hs:6:22:
+    Couldn't match expected type `S8.ByteString'
+                with actual type `bytestring-0.10.0.2:Data.ByteString.Internal.ByteString'
+    In the return type of a call of `encodeUtf8'
+    In the second argument of `($)', namely `encodeUtf8 "Hello World!"'
+    In the expression: S8.putStrLn $ encodeUtf8 "Hello World!"

When is a ByteString not a ByteString? Here, apprently. Now it turns out +the GHC is actually giving you quite of bit of useful information, you just +need to know what to look for. It's expecting the type S8.ByteString, which +expands to Data.ByteString.Char8.ByteString, which in reality is just a type +synonym for Data.ByteString.Internal.ByteString. So what GHC really means is +that it can't unify the following two types:

expected:                     Data.ByteString.Internal.ByteString
+actual:   bytestring-0.10.0.2:Data.ByteString.Internal.ByteString

Now the difference just jumps out at you: the actual type comes from the +bytestring-0.10.0.2 package, whereas the first comes from... well, somewhere +else. As I'm sure you're guessing right now, that "somewhere else" is +bytestring-0.10.4.0, but GHC doesn't bother telling us that, since including +that level of information in every error message would be overwhelming. To +step through why this came up exactly:

  • text is installed against bytestring-0.10.0.2 (it was the only version of bytestring available at the time you installed text).
  • Therefore, encodeUtf8 will generate a ByteString value from version 0.10.0.2.
  • Your program imports Data.ByteString.Char8, which is provided by both bytestring-0.10.0.2 and bytestring-0.10.4.0.
  • GHC's default dependency resolution is: take the latest version of each package, in this case 0.10.4.0.
  • Now we have a S8.putStrLn function expecting a 0.10.4.0 ByteString, but an encodeUtf8 function returning a 0.10.0.2 ByteString.

So how do we work around this problem? I can think of three ways:

  1. Explicitly tell GHC which version of the bytestring package you want to use to force consistency, e.g. runghc -package=bytestring-0.10.0.2 foo.hs.
  2. Never use GHCi, runghc, or ghc directly from the command line. Instead, always create a cabal file first. cabal's default dependency resolution will force consistent package loading.
  3. Don't wind up in the situation in the first place, by ensuring you only have one version of each package installed.

That last point is what I strongly recommend to all users. And this is exactly +the design goal around Stackage, so it will +hopefully not come as a surprise that that's exactly what I recommend most +Haskell users use to get their packages installed.

Let's demonstrate that second case of MonadTrans. This time, let's try it +with GHC 7.8.3. GHC ships with +transformers-0.3.0.0. Next, we'll install the either package with cabal +install either. Once again, someone comes along and tells us about a shiny new +package, transformers-0.4.1.0. Dutifully, we upgrade with cabal install +transformers-0.4.1.0. And then we try to run the following simple program:

import Control.Monad.Trans.Class
+import Control.Monad.Trans.Either
+
+main :: IO ()
+main = do
+    x <- runEitherT $ lift $ putStrLn "hey there!"
+    print (x :: Either () ())

GHC mocks you with:

foo.hs:6:23:
+    No instance for (MonadTrans (EitherT ()))
+      arising from a use of ‘lift’
+    In the expression: lift
+    In the second argument of ‘($)’, namely
+      ‘lift $ putStrLn "hey there!"’
+    In a stmt of a 'do' block:
+      x <- runEitherT $ lift $ putStrLn "hey there!"

"But EitherT is an instance of MonadTrans!" you insist. That may be true, but it's an instance of the wrong MonadTrans. The either package is built against transformers-0.3.0.0, whereas you've imported lift from transformers-0.4.1.0. This can be worked around as above, with runghc -package=transformers-0.3.0.0 foo.hs. And yet again, my strong recommendation is: use Stackage.

There's one more particularly painful thing I need to point out. Some packages +are bundled with GHC, and are depended on by the ghc package. The special +thing about the ghc package is that it cannot be reinstalled without installing +a new version of GHC itself. Any packages depended on by the ghc package +cannot be unregistered without breaking ghc, which would in turn break +libraries like doctest and hint. If you follow these points to conclusion, +this means that you should never upgrade GHC-bundled libraries. I wrote a blog +post on this +topic, and the +takeaway is: please, always support older versions of packages like bytestring, +transformers, and- of course- base.

There's one final case I want to mention. Try running cabal install data-default-0.5.1 http-client, and then run the following program:

import Data.Default
+import Network.HTTP.Client
+
+main :: IO ()
+main = withManager defaultManagerSettings $ \man -> do
+    res <- httpLbs def man
+    print res

You'll get the irritating error message:

foo.hs:6:20:
+    No instance for (Default Request) arising from a use of ‘def’
+    In the first argument of ‘httpLbs’, namely ‘def’
+    In a stmt of a 'do' block: res <- httpLbs def man
+    In the expression:
+      do { res <- httpLbs def man;
+           print res }

But if you look at http-client, Request is in fact an instance of +Default. "Alright, I know what's going on here" you say. Certainly there are +two versions of data-default installed, right? Actually, no, that's not the +case. Have a look at the following:

$ ghc-pkg list | grep data-default
+    data-default-0.5.1
+    data-default-class-0.0.1

There's just a single version of each of these packages available. So why are +we getting our mysterious error message? Once again, it's because we have two +versions of the Default class. After data-default version 0.5.1, +data-default split into a number of packages, and the Default class +migrated into data-default-class. http-client defines an instance for +Default from data-default-class. And if you use data-default version +0.5.2 or higher, it will simply re-export that same class, and everything will +work.

However, our cabal install command forced the installation of the older +data-default (0.5.1) which defines its own Default typeclass. Therefore, we +end up with two separate Default classes that don't unify. This is a problem +that exists whenever packages are split or joined, which is why you should +embark on such refactorings with great care.

As it happens, this is yet another problem that is solved by using Stackage, +since it forces a consistent set of versions for data-default and +data-default-class.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/10/classy-base-prelude.html b/public/blog/2014/10/classy-base-prelude.html new file mode 100644 index 00000000..4db4e548 --- /dev/null +++ b/public/blog/2014/10/classy-base-prelude.html @@ -0,0 +1,63 @@ + Haskell's Prelude is adding Foldable/Traversable +

Haskell's Prelude is adding Foldable/Traversable

October 6, 2014

GravatarBy Greg Weber

Haskell's Prelude is changing to favor using Foldable/Traversable instead of just lists. +Many Haskellers are concerned that upcoming changes to the Prelude could

  • break existing code
  • make maintaining code more difficult
  • decrease beginner friendliness

Lets discuss these concerns

Stability and the Prelude design space

Neil Mitchell writes:

Step 3: Incorporate feedback
+
+I expect that will result in a significant number of revisions, and perhaps significant departures from the original design. Note that the classy-prelude package is following the steps above, and has had 38 revisions on Hackage so far.

As a contributor to and user of classy-prelude, I wanted to point out something about this statement. Most of these revisions are minor and backwards compatible consisting of bug-fixes or something like adding a non-default implementation of a typeclass method or an additional typeclass instance. A better data point is the number of major revision releases. classy-prelude is at release 0.10 now, so that would be 10.

Neil mentions classy-prelude a second time:

The classy-prelude work has gone in that direction, and I wish them luck, but the significant changes they've already iterated through suggest the design space is quite large.

Of these 10 changes, I would only consider one to be major and represent any kind of change in the design space: the 0.6 release basing classy-prelude on the mono-traversable package. +Some of the version bumps represent a change in code organization between mono-traversable and classy-prelude. One change that required a bump is a change to the type signature of mapM_ that should be made in the Prelude but probably never will because it will break existing code. +The major change in the 0.6 release is very similar to the upcoming changes in the Prelude, except that classy-prelude (via mono-traversable) works on monomorphic structures such as Text in addition to polymorphic structures. +So I would not consider the design space to be large for classy-prelude or for other prelude replacements. +classy-prelude before 0.6 and most other Prelude replacements or type-class conveniences have not worked out very well. +There is only 1 design that has worked well (incorporating Foldable and Traversable), and that is the design being incorporated into the new Prelude.

Neil also writes about other Haskell abstractions:

We already have that problem with the Control.Arrow module, which (as far as most people use it), is just a pile of tuple combinators. But unlike other tuple combinators, these are ones whose type signature can't be understood. When I want to use &&& or ***

I want to point out that classy-prelude solved some of this issue by exporting Data.BiFunctor instead of functions from Data.Arrow, which required a version bump from 0.9.x to 0.10. I also want to point out that these kinds of arguments are straw man arguments. Every proposed abstraction to Haskell has its own advantages and dis-advantages. Because of the need for backwards compatibility, we are going to be able to point to abstractions in the Prelude that are not being used in the right way for a long time. However, Foldable/Traversable is the only serious contender for abstracting over data structures in Haskell. It has stood the test of time so far, but it has not seen a lot of use yet because everyone is initially directed to just use lists for everything, and the next instinct when using other data structures in the current environment is to use qualified names.

Iterating the new design

One proposed remedy for dealing with change is trying to release the new Prelude in an iterative way. +This could be a good idea, but in practice it is very difficult to implement: most users are still going to import Prelude rather than trying out something different and giving their feedback. +A better approach than holding it back is to use a design that makes it easier for new releases to make backwards incompatible changes. +One approach to this could be at the package level the way that base-compat operates. +Another approach that could be useful to library authors is incorporate versioning at the module level.

Something to keep in mind though is that because the new Prelude needs to try to work with the old Prelude, +there are not that many options in the design space. +classy-prelude has had the luxury of being able to re-think every Haskell wart. +So it was able to remove all partial functions and use Text instead of String in many places. +But that process is very difficult for the actual Prelude, which is severly constrained.

Why change? Module qualified names and generic code.

The motivation for classy-prelude was to confront one of Haskell's most glaring warts: name-spacing and the need to qualify functions. +We could certainly have our IDE automatically write import statements, but we still end up with needing to use module qualified names. +This isn't really an acceptable way to program. +I have not seen another language where this extra line noise is considered good style. +For Haskell to move forward and be as convenient to use as other programming languages, there are 2 solutions I know of.

  1. change the language
  2. make it convenient to write generic code

Changing the language so that module qualification is not needed is arguably a much better approach. +This is the case in Object-Oriented languages, and possible in languages very similar to Haskell such as Frege that figure out how to disambiguate a function based on the data type being used. +I think this would be a great change to Haskell, but the idea was rejected by Simon Peyton Jones himself during the discussion on fixing Haskell records because it is not compatible with how Haskell's type system operates today. Simon did propose Type directed name resolution which I always though was a great idea, but that proposal was not able to get off the ground in part because changing Haskell's dot operator proved too controversial.

So the only practical option I know of is to focus on #2. +Being able to write generic code is an important issue in of itself. +Programmers in most other mainstream languages write code that operates on multiple data structures of a particular shape, +but Haskell programmers are still specializing a lot of their interfaces.

Lists are holding Haskell back

It is taken by many to be a truism that programming everything with lists makes things simpler or at least easier for new Haskell programmers. +I have found this statement to be no different than 99% of things given the glorious "simple" label: the "simplicity" is not extensible, does not even live up to its original use case, and ends up creating its own incidental complexity.

I used to frequently warp the functions I wrote to fit the mold of Haskell's list. +Now that I use classy-prelude I think about the data structure that is needed. Or often I start with a list, eventually discover that something such as appending is needed, and I am able to quickly change the function to operate on a different data structure.

Using an associative list is an extreme example of using the wrong data structure where lookup is O(n) instead of constant or O(log(n)). +But by warping a function I am really talking about writing a function in a way to reduce list appends or doing a double reverse instead of using a more natural DList or a Seq. +This warping process probably involves performing recursion by hand instead of re-using higher-order functions. +As a library developer, I would like to start exposing interfaces that allow my users to use different data structures, but I know that it is also going to cause some inconvenience because of the current state of the Prelude.

Neil writes that he had an opposite experience:

I have taken over a project which made extensive use of the generalised traverse and sequence functions. Yes, the code was concise, but it was read-only, and even then, required me to "trust" that the compiler and libraries snapped together properly.

This kind of report is very worrying and it is something we should take very seriously. +Any you certainly cannot tell someone that their actual experience was wrong. +However, it is human nature to over-generalize our experiences just as it was the nature of the code author in question to over-generalize functions. +In order to have a productive discussion about this, we need to see (at least a sample or example of) the code in question. +Otherwise we are only left guessing at what mistakes the author made.

In general I would suggest specializing your application code to lists or other specific structures (this can always be done with type signatures) until there is a need for more abstraction, and that could be a big part of the problem in this case.

It would be really great to start having these discussions now based off of actual code examples and to have a community guide that explains the missing common sense for how to use abstractions appropriately.

The uncertainty discussed here is the heart of the matter, and talking about what is good for beginners is largely a distraction.

Haskell is for programmers first, students in their first class in functional programming second

This might sound hostile to beginners. In fact, the opposite is the case! +The programming languages that are taught to beginners are the very same ones that are used in industry. +The first programming language taught to me at school was C, and it was not because it was optimized out of the box for beginners.

So the way to attract more beginners is simply to become more popular with industry. +Haskell has already reached the growth rate limit of a language that is popular for the sake of learning about programming.

That being said, we do need to do a lot of things to make the experience as nice as possible for beginners, and an alternative prelude for beginners could be a great idea. +But making this the default that holds back progress hurts everyone in the long-run.

It isn't Beginner vs. Expert anyways

The most up-voted comment on Reddit states:

What other languages have different standard libraries for people learning and people not learning? What could be a greater way to confuse learners, waste their time and make them think this language is a joke than presenting them with n different standard libraries?

I will add my own assertion here: Haskell is confusing today because the Prelude is in a backward state that no longer reflects several important best practices (for example, Neil had to create the Safe package!) and it does not hold up once you write more than a trivial amount of code in your module.

We also need to keep in mind that using Haskell can be difficult for beginners precisely for some of the same reasons that it is painful for experts. +And the same reason these new changes will be more difficult for beginners (mental overhead of using the Foldable/Traversable abstraction instead of just lists) will also create difficulties for non-beginners.

So the changes to the Prelude are going to make some aspects a better for beginners or existing users and others harder.

If we really want to improve Haskell for beginners we need to stop creating a false dichotomy between beginner and expert. +We also need to empower committees to make forward progress rather than letting minority objections stall all forward progress.

Improving the library process

Some have expressed being surprised to learn about what is going on in the Haskell libraries committee at a late stage. +On the other hand, I doubt that hearing more objections earlier would actually be helpful, because the libraries process has not learned from GHC.

Take a look at the extensive documentation around proposed changes to improve Haskell's record system. +Creating a solution to Haskell's problem with records was a very difficult process. +There were several designs that looked good in a rough sketch form, but that had issues when explored in thorough detail on the wiki. +More importantly, the wiki helped summarize and explain a discussion that was extremely laborious to read and impossible to understand by looking through a mail list.

Before creating a non-minor change to GHC, there is a convention of creating a wiki page (certainly it isn't always done). +At a minimum there is a Trac ticket that can serve a somewhat similar purpose.

My suggestion is that the libraries process use the existing GHC or Haskell wiki to create a page for every non-minor change. +The page for Foldable/Traversable would explain

  • what is being changed
  • which changes create a breakage
  • how type errors have changed
  • how library code is affected
  • how user code is affected
  • best practices for using Foldable/Traversable

Right now we are stuck in a loop of repeating the same points that were already made in the original discussion of the proposal. +Given a wiki page, Neil and others could point out the down-sides of the proposal with actual code and have their voice heard in a productive way that builds up our body of knowledge.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/10/slides-conduit-ghcjs.html b/public/blog/2014/10/slides-conduit-ghcjs.html new file mode 100644 index 00000000..5ac15ea0 --- /dev/null +++ b/public/blog/2014/10/slides-conduit-ghcjs.html @@ -0,0 +1,23 @@ + Slides on conduit and GHCJS +

Slides on conduit and GHCJS

October 31, 2014

GravatarBy Michael Snoyman

I'm currently sitting in a hotel room in Poznan, Poland, getting ready to +attend the second day of PolyConf. In the past two days, I've already had quite +some fun. Wednesday- thanks to Matthias Fischmann and Adam Drake- I spoke to +the Berlin Haskell users group about conduit. I had no idea that there were +this many Haskellers in Berlin, and it was awesome to meet all of you. +Yesterday I spoke about Haskell on the client side at PolyConf, mostly talking +about how awesome GHCJS is (after Luite indoctrinated me at ICFP).

For those interested, the slides are now available online at:

I've also learnt quite a number of cool client side tricks at PolyConf, +hopefully we'll get to take advantage of some of them in Yesod in the near +future :).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/10/updating-auto-update.html b/public/blog/2014/10/updating-auto-update.html new file mode 100644 index 00000000..50c0beab --- /dev/null +++ b/public/blog/2014/10/updating-auto-update.html @@ -0,0 +1,97 @@ + Updating auto-update +

Updating auto-update

October 2, 2014

GravatarBy Michael Snoyman

A few weeks back, Kazu and I received an email from Martin Bailey, who was +interested in working with us to further optimize Warp. The subject at the +time was reduced buffer copying and allocations. That subject is very +interesting in itself, and once finalized, will get its own blog post as well. But +that's not the subject of this blog post.

After working on some ideas, Kazu benchmarked Warp, and found a massive +performance degradation which had already slipped onto Hackage. The problem +only appears under highly concurrent requests (which is exactly what Kazu's +benchmark was testing). That bug has now been resolved, and users are +recommended to upgrade to the newest versions of auto-update and warp. (We also +included some other performance boosts here, care of Ryan Newton's +atomic-primops package.) This blog post covers the problem, and its solution.

It's about time

One of the required response headers according to the HTTP spec is the Date +header. As you might guess, this consists of the date, as well as the current +time on the server. This has a very specific format specified in the spec. A +naive approach to filling in this header would be, for each request, to run:

now <- getCurrentTime
+let value = formatTimeHTTP now
+    headers' = ("Date", value) : headers

However, when you're writing a server that handles hundreds of thousands of +requests per second, having to get and format the current time on each request +is incredibly expensive. Instead, quite some time ago, Kazu introduced a date +formatting worker thread, which essentially looks like this:

let getNowFormatted = formatTimeHTTP <$> getCurrentTime
+nowFormatted <- getNowFormatted >>= newIORef
+forkIO $ forever $ do
+    threadDelay 1000000
+    getNowFormatted >>= writeIORef nowFormatted
+return $ readIORef nowFormatted

We fork a single thread that recalculates the formatted time every second, +and updates a mutable reference. This means that, regardless of how many +clients connect, we will always run this computation at most once per second. +And reading the current time requires no system calls, formatting, or +allocation: it's just a memory read.

Watt's the matter

The problem with this approach is that, even if there are zero requests, we're +still going to run this computation once a second. This doesn't hurt our +performance too much (it's a relatively cheap operation). However, it does mean +that our process never stops working, which is bad for power consumption. So +we needed a way to let that worker thread turn off when it wasn't needed +anymore, and start up again on demand.

If this sounds familiar, it's because we blogged about it just two months +ago. But there's +an implementation detail I want to point out about auto-update. When I started +writing it, I aimed to have the worker thread completely shut down when not +needed, and then for a new worker thread to be spawned on demand. This +resulted in some strange corner cases. In particular, it was possible for two +different callers to spawn two different worker threads at the same time. We +used atomicModifyIORef to ensure that only one of those threads became the +"official" worker, and the other one would shut down right away. There was also +a lot of tricky business around getting async exceptions right.

The code wasn't too difficult to follow, and was still pretty short. You can +view it on +Github.

The core of the problem

Unfortunately, before release, we didn't do enough performance testing of +auto-update in highly concurrent settings. When we threw enough cores at the +problem, we quickly ran into a situation where multiple Haskell threads would +end up making concurrent requests for the auto-update value, and each of those +threads would fork a separate worker thread. Only one of those would survive, +but the forking itself was killing our performance! (I had one benchmark +demonstrating that, for one million requests across one thousand threads on +four cores, over ten thousands worker threads were forked.)

There was another problem as well. We made heavy use of atomicModifyIORef to +get this working correctly. Compare this to our original dedicated thread +solution: each data access was a simple, cheap readIORef. By introducing +atomicModifyIORef at each request site, we introduced memory barrier issues +immediately.

The solution

After iterating a few times, Kazu, Martin and I came up with a fairly elegant +solution to the problem. As noted, the original dedicated thread was in many +ways ideal. A lot of our problems were coming from trying to fork threads on +demand. So we came up with a compromise: why not have a single, dedicated +worker thread, but allow it to go to sleep/block when it's no longer needed?

Blocking semantics meant we needed to move beyond IORefs over to either +MVars or STM (we elected for the former, but it would still be interesting to +compare performance with the latter). But we still want to be able to have +incredibly cheap lookup in the common case of a value being precomputed. So we +ended up with three mutable variables:

  1. An IORef containing a Maybe a. If a precomputed value is available, it's +present in there as Just a. Otherwise, there's a Nothing value. This +mean that, in the normal case, a requester only needs to (a) do a memory read, +and (b) case analyze the value.

  2. An MVar baton to tell the worker thread that someone is waiting for a +value. In the case that the IORef contains Nothing, the requester fills +this MVar. Before it starts working, the worker thread always tries to +takeMVar this baton.

  3. An MVar for passing back result values. This may seem redundant with the +IORef, but is necessary for the case of Nothing. A requester putMVars +into the baton, and then blocks in readMVar on this reference until the value +is available. The worker thread will update both the IORef and this value at +the same time, and then go to sleep until it needs to compute again, at which +point the process will loop.

Since the requesters never try to fork threads, there's no possibility of +running into the high contention problem of multiple forks. The worst case +scenario is that many requesters see a Nothing value at the same time, and +all end up blocking on the same MVar. While less efficient than just reading +an IORef, this isn't nearly as expensive as what we had previously.

There's another big advantage: the normal case no longer uses any +atomicModifyIORef (or any other synchronized memory access), meaning we've +avoided memory barriers during periods of high concurrency.

And finally: the code is drastically +simpler.

And now that we've fixed our regression, and gotten back high performance +together with better power consumption, we can finally return to Martin's +original idea of better buffer management. More on that soon hopefully :).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/10/yesod-gitrepo.html b/public/blog/2014/10/yesod-gitrepo.html new file mode 100644 index 00000000..56312705 --- /dev/null +++ b/public/blog/2014/10/yesod-gitrepo.html @@ -0,0 +1,67 @@ + Announcing: yesod-gitrepo +

Announcing: yesod-gitrepo

October 29, 2014

GravatarBy Michael Snoyman

I'm happy to announce the first release of a new package, +yesod-gitrepo. This package +encapsulates a pattern I've used a number of times, namely: loading and +refreshing content from a Git repository. Below is the current contents of the +README.md file.

This code is currently being used in production, and should be pretty stable. +That said, it has not received a huge amount of battle testing yet. So please +due test corner cases before shipping it in production for your site.


yesod-gitrepo provides a means of embedding content from a Git repository +inside a Yesod application. The typical workflow is:

  • Use gitRepo to specify a repository and branch you want to work with.
  • Provide a function that will perform some processing on the cloned +repository.
  • Use grContent in your Handler functions to access this parsed data.
  • Embed the GitRepo as a subsite that can be used to force a refresh of the +data.
  • Set up a commit handler that pings that URL. On Github, this would be a +webhook.

This is likely easiest to understand with a concrete example, so let's go meta: +here's an application that will serve this very README.md file. We'll start +off with language extensions and imports:

{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           ClassyPrelude.Yesod
+import           Text.Markdown
+import           Yesod.GitRepo

Now we're going to create our foundation datatype. We need to give it one +field: the GitRepo value containing our parsed content. Our content will +simply be the text inside README.md, wrapped up in a Markdown newtype for +easy rendering. This gives us:

data App = App
+    { getGitRepo :: GitRepo Markdown
+    }
+
+instance Yesod App

And now let's set up our routes. We need just two: a homepage, and the subsite. +Our subsite type is GitRepo Markdown (as given above). We replace the space +with a hyphen as an escaping mechanism inside Yesod's route syntax:

mkYesod "App" [parseRoutes|
+/ HomeR GET
+/refresh RefreshR GitRepo-Markdown getGitRepo
+|]

Next up is our home handler. We start off by getting the current content parsed +from the repository:

getHomeR :: Handler Html
+getHomeR = do
+    master <- getYesod
+    content <- liftIO $ grContent $ getGitRepo master

Then it's just some normal Hamlet code:

    defaultLayout $ do
+        setTitle "yesod-gitrepo sample"
+        [whamlet|
+            <p>
+                <a href=@{RefreshR GitRepoRoute}>
+                    Force a refresh at
+                    @{RefreshR GitRepoRoute}
+            <article>#{content}
+        |]

And finally, our main function. We pass in the repo URL and branch name, plus +a function that, given the filepath containing the cloned repo, processes it +and generates a Markdown value. Finally, we provide the generated repo +value to our App constructor and run it:

main :: IO ()
+main = do
+    repo <- gitRepo
+        "https://github.com/snoyberg/yesod-gitrepo"
+        "master"
+        $ \fp -> fmap Markdown $ readFile $ fp </> "README.md"
+    warp 3000 $ App repo

Give it a shot. You should have a webapp hosting this very README file!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/11/cabal-sandbox-stackage.html b/public/blog/2014/11/cabal-sandbox-stackage.html new file mode 100644 index 00000000..dbc1b0db --- /dev/null +++ b/public/blog/2014/11/cabal-sandbox-stackage.html @@ -0,0 +1,67 @@ + Installing application dependencies using Stackage, sandboxes, and freezing +

Installing application dependencies using Stackage, sandboxes, and freezing

November 12, 2014

GravatarBy Greg Weber

Installing Haskell packages is still a pain. But I believe the community has some good enough workarounds that puts Haskell on par with a lot of other programming languages. +The problem is mostly that the tools and techniques are newer, do not always integrate easily, and are still lacking some automation.

My strategy for successful installation:

  • Install through Stackage
  • Use a sandbox when you start having complexities
  • freeze (application) dependencies

Simple definitions:

  • Stackage is Stable Hackage: a curated list of packages that are guaranteed to work together
  • A sandbox is a project-local package installation
  • Freezing is specifying exact dependency versions.

I really hope that Stackage (and sandboxes to a certain extent) are temporary workarounds before we have an amazing installation system such as backpack. +But right now, I think this is the best general-purpose solution we have. There are other tools that you can use if you are not on Windows:

  • hsenv (instead of sandboxes)
  • nix (instead of Stackage and sandboxes)

hsenv has been a great tool that I have used in the past, but I personally don't think that sandboxing at the shell level with hsenv is the best choice architecturally. +I don't want to have a sandbox name on my command line to remind me that it is working correctly, I just want cabal to handle sandboxes automatically.

Using Stackage

See the Stackage documentation. You just need to change the remote-repo setting in your ~/.cabal/config file.

Stackage is a curated list of packages that are guaranteed to work together. +Stackage solves dependency hell with exclusive and inclusive package snapshots, but it cannot be used on every project.

Stackage offers 2 package lists: exclusive, and inclusive. +Exclusive includes only packages vetted by Stackage. +Exclusive will always work, even for global installations. +This has the nice effect of speeding up installation and keeping your disk usage low, whereas if you default to using sandboxes and you are making minor fixes to libraries you can end up with huge disk usage. +However, you may eventually need packages not on Stackage, at which point you will need to use the inclusive snapshot. +At some point you will be dealing with conflicts between projects, and then you definitely need to start using sandboxes. +The biggest problem with Stackage is that you may need a newer version of a package than what is on the exclusive list. +At that point you definitely need to stop using Stackage and start using a sandbox.

If you think a project has complex dependencies, which probably includes most applications in a team work setting, you will probably want to start with a sandbox.

Sandboxes

cabal sandbox init

A sandbox is a project-local package installation. +It solves the problem of installation conflicts with other projects (either actively over-writing each-other or passively sabotaging install problems). +However, the biggest problem with sandboxes is that unlike Stackage exclusive, you still have no guarantee that cabal will be able to figure out how to install your dependencies.

sandboxes are mostly orthogonal to Stackage. +If you can use Stackage exclusive, you should, and if you never did a cabal update, you would have no need for a sandbox with Stackage exclusive. +When I am making minor library patches, I try to just use my global package database with Stackage to avoid bloating disk usage from redundant installs.

So even with Stackage we are going to end up wanting to create sandboxes. +But we would still like to use Stackage in our sandbox: this will give us the highest probability of a successful install. +Unfortunately, Stackage (remote-repo) integration does not work for a sandbox.

The good news is that there is a patch for Cabal that has already been merged (but not yet released). +Even better news is that you can use Stackage with a sandbox today! +Cabal recognizes a cabal.config file which specifies a list of constraints that must be met, and we can set that to use Stackage.

cabal sandbox init
+curl http://www.stackage.org/alias/fpcomplete/unstable-ghc78-exclusive/cabal.config > cabal.config
+cabal install --only-dep

Freezing

There is a problem with our wonderful setup: what happens when our package is installed on another location? +If we are developing a library, we need to figure out how to make it work everywhere, so this is not as much of an issue.

Application builders on the other hand need to produce reliable, re-producible builds to guarantee correct application behavior. +Haskellers have attempted to do this in the .cabal file by pegging versions. +But .cabal file versioning is meant for library authors to specify maximum version ranges that a library author hopes will work with their package. +Pegging packages to specific versions in a .cabal file will eventually fail because there are dependencies of dependencies that are not listed in the .cabal file and thus not pegged. +The previous section's usage of a cabal.config has a similar issue since only packages from Stackage are pegged, but Hackage packages are not.

The solution to this is to freeze your dependencies:

cabal freeze

This writes out a new cabal.config (overwriting any existing cabal.config). +Checking in this cabal.config file guarantees that everyone on your team will be able to reproduce the exact same build of Haskell dependencies. +That gets us into upgrade issues that will be discussed.

It is also worth noting that there is still a rare situation in which freezing won't work properly because packages can be edited on Hackage.

Installation workflow

Lets go over an installation workflow:

cabal sandbox init
+curl http://www.stackage.org/alias/fpcomplete/unstable-ghc78-exclusive/cabal.config > cabal.config
+cabal install --only-dep

An application developer will then want to freeze their dependencies.

cabal freeze
+git add cabal.config
+git commit cabal.config

Upgrading packages

cabal-install should provide us with a cabal upgrade [PACKAGE-VERSION] command. +That would perform an upgrade of the package to the version specified, but also perform a conservative upgrade of any transitive dependencies of that package. +Unfortunately, we have to do upgrades manually.

One option for upgrading is to just wipe out your cabal.config and do a fresh re-install.

rm cabal.config
+rm -r .cabal-sandbox
+cabal sandbox init
+curl http://www.stackage.org/alias/fpcomplete/unstable-ghc78-exclusive/cabal.config > cabal.config
+cabal update
+cabal install --only-dep
+cabal freeze

With this approach all your dependencies can change so you need to re-test your entire application. +So to make this more efficient you are probably going to want to think about upgrading more dependencies than what you originally had in mind to avoid doing this process again a week from now.

The other extreme is to become the solver. Manually tinker with the cabal.config until you figure out the upgrade plan that cabal install --only-dep will accept. +In between, you can attempt to leverage the fact that cabal already tries to perform conservative upgrades once you have packages installed.

rm cabal.config
+curl http://www.stackage.org/alias/fpcomplete/unstable-ghc78-exclusive/cabal.config > cabal.config
+cabal update
+cabal install --only-dep --force-reinstalls
+cabal freeze

You can make a first attempt without the --force-reinstalls flag, but the flag is likely to be necessary.

If you can no longer use Stackage because you need newer versions of the exclusive packages, then your workflow will be the same as above without the curl step. +But you will have a greater desire to manually tinker with the cabal.config file. +This process usually consists mostly of deleting constraints or changing them to be a lower bound.

Conclusion

Upgrading packages is still a horrible experience.

However, for a fresh install, using Stackage, sandboxes, and freezing works amazingly well. +Of course, once you are unable to use Stackage because you need different exclusive versions you will encounter installation troubles. +But if you originally started based off of Stackage and try to perform conservative upgrades, you may still find your situation easier to navigate because you have already greatly +reduced the search space for cabal. +And if you are freezing versions and checking in the cabal.config, the great thing is that you can experiment with installing new dependencies but can always revert back to the last known working dependencies.

Using these techniques I am able to get cabal to reliably install complex dependency trees with very few issues and to get consistent application builds.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/11/case-for-curation.html b/public/blog/2014/11/case-for-curation.html new file mode 100644 index 00000000..7ee9cfe3 --- /dev/null +++ b/public/blog/2014/11/case-for-curation.html @@ -0,0 +1,104 @@ + The case for curation: Stackage and the PVP +

The case for curation: Stackage and the PVP

November 12, 2014

GravatarBy Michael Snoyman

A number of months back there was a long series of discussions around the +Package Versioning Policy (PVP), and in particular the policy of putting in +preemptive upper bounds (that is to say, placing upper bounds before it is +proven that they are necessary). Eventually, the conversation died down, and +I left some points unsaid in the interests of letting that conversation die. +Now that Neil Mitchell kicked up the dust +again, +I may as well get a few ideas out there.

tl;dr Stackage is simply better tooling, and we should be using better +tooling instead of arguing policy. The PVP upper bounds advocates are arguing +for a world without sin, and such a world doesn't exist. Get started with Stackage now.

This blog post will be a bit unusual. Since I'm so used to seeing questions, +criticisms, and misinformation on this topic, I'm going to interject commonly +stated memes throughout this blog post and answer them directly. Hopefully this +doesn't cause too much confusion.

As most people reading this are probably aware, I manage the Stackage +project. I have a firm belief that the PVP +discussions we've been having are, essentially, meaningless for the general use +case, and simply improving our tool chain is the right answer. Stackage is one +crucial component of that improvement.

But Stackage doesn't really help end users, it's nothing more than a CI system +for Hackage. The initial Stackage work may have counted as that, but Stackage +was never intended to just be behind the scenes. Stackage +server provides a very user-friendly solution for +solving packaging problems. While I hope to continue improving the ecosystem- +together with the Haskell Platform and Cabal maintainers- Stackage server is +already a huge leap forward for most users today. (See also: GPS Haskell.)

The PVP is composed of multiple ideas. I'd like to break it into:

  1. A method for versioning packages based on API changes.
  2. How lower bounds should be set on dependencies.
  3. How upper bounds should be set on dependencies.

Just about everyone I've spoken to agrees with the PVP on (1) and (2), the only +question comes up with point (3). The arguments go like this: preemptive upper +bounds add a lot of maintainer overhead by requiring them to upload new +versions of packages to relax version bounds regularly. (This is somewhat +mitigated by the new cabal file editing feature of Hackage, but that has its +own problems.) On the other hand, to quote some people on Reddit:

I'd rather make a release that relaxes bounds rather than have EVERY previous version suddenly become unusable for folks

that upper bounds should not be viewed as handcuffs, but rather as useful information about the range of dependencies that is known to work. This information makes the solver's job easier. If you don't provide them, your packages are guaranteed to break as t -> ∞.

These statements are simply false. I can guarantee you with absolute certainty +that, regardless of the presence of upper bounds, I will be able to continue to +build software written against yesod 1.4 (or any other library/version I'm +using today) indefinitely. I may have to use the same compiler version and +fiddle with shared libraries a bit if I update my OS. But this notion that +packages magically break is simply false.

But I have some code that built two months ago, and I opened it today and it +doesn't work! I didn't say that the standard Haskell toolchain supports this +correctly. I'm saying that the absence of upper bounds doesn't guarantee that a +problem will exist.

Without dancing around the issue any further, let me cut to the heart of the +problem: our toolchain makes it the job of every end user to find a consistent +build plan. Finding such a build plan is inherently a hard problem, so why are +we pushing the work downstream? Furthermore, it's terrible practice for working +with teams. The entire team should be working in the same package environment, +not each working on "whatever cabal-install decided it should try to build +today."

There's a well known, time-tested solution to this problem: curation. It's +simple: we have a central person/team/organization that figures out consistent +sets of packages, and then provides them to downstream users. Downstream users +then never have to deal with battling against large sets of dependencies.

But isn't curation a difficult, time-consuming process? How can the Haskell +community support that? Firstly, that's not really an important question, +since the curation is already happening. Even if it took a full-time staff of +10 people working around the clock, if the work is already done, it's done. In +practice, now that the Stackage infrastructure is in place, curation probably +averages out to 2 hours of my time a week, unless Edward Kmett decides to +release a new version of one of his packages.

This constant arguing around PVP upper bounds truly baffles me, because every +discussion I've seen of it seems to completely disregard the fact that +there's an improved toolchain around for which all of the PVP upper bound +arguments are simply null and void. And let me clarify that statement: I'm not +saying Stackage answers the PVP upper bound question. I'm saying that- for +the vast majority of users- Stackage makes the answer to the question +irrelevant. If you are using Stackage, it makes not one bit of difference to +you whether a package has upper bounds or not.

And for the record, Stackage isn't the only solution to the problem that makes +the PVP upper bound question irrelevant. Having cabal-install automatically +determine upper bounds based on upload dates is entirely possible. I in fact +already implemented such a system, and sent it for review to two of the +staunchest PVP-upper-bounds advocates I interact with. I didn't actually +receive any concrete feedback.

So that brings me back to my point: why are we constantly arguing about this +issue which clearly has good arguments on both sides, when we could instead +just upgrade our tooling and do away with the problem?

But surely upper bounds do affect some users, right? For one, it affects the +people doing curation itself (that's me). I can tell you without any doubt that +PVP upper bounds makes my life more difficult during curation. I've figured out +ways to work around it, so I don't feel like trying to convince people to +change their opinions. It also affects people who aren't using Stackage or some +other improved tooling. And my question to those people is: why not?

I'd like to close by addressing the idea that the PVP is "a solution." +Obviously that's a vague statement, because we have to define "the problem." So +I'll define the problem as: someone types cabal install foo and it doesn't +install. Let me count the ways that PVP upper bounds fail to completely solve +this problem:

  1. The newest release of foo may have a bug in it, and cabal has no way of knowing it.
  2. One of the dependencies of foo may have a bug in it, and for whatever reason cabal chooses that version.
  3. foo doesn't include PVP upper bounds and a new version of a dependency breaks it. (See comment below if you don't like this point.)
  4. Some of the dependencies of foo don't include PVP upper bounds, and a new version of the transitive dependencies break things.
  5. There's a semantic change in a point release which causes tests to fail. (You do test your environments before using them, right? Because Stackage does.)
  6. I've been really good and included PVP upper bounds, and only depended on packages that include PVP upper bounds. But I slipped up and had a mistake in a cabal file once. Now cabal chooses an invalid build plan.
  7. All of the truly legitimate reasons why the build may fail: no version of the package was ever buildable, no version of the package was ever compatible with your version of GHC or OS, it requires something installed system wide that you don't have, etc.

That's not fair, point (3) says that the policy doesn't help if you don't +follow it, that's a catch 22! Nope, that's exactly my point. A policy on its +own does not enforce anything. A tooling solution can enforce invariants. +Claiming that the PVP will simply solve dependency problems is built around the +idea of universal compliance, lack of mistakes, historical compliance, and +the PVP itself covering all possible build issues. None of these claims hold up +in the real world.

To go comically over the top: assuming the PVP will solve dependency problems +is hoping to live in a world without sin. We must accept the PVP into our +hearts. If we have a build problem, we must have faith that it is because we +did not trust the PVP truly enough. The sin is not with the cabal dependency +solver, it is with ourselves. If we ever strayed from the path of the PVP, we +must repent of our evil ways, and return unto the PVP, for the PVP is good. I'm +a religious man. My religion just happens to not be the PVP.

I'm not claiming that Stackage solves every single reason why a build fails. +The points under (7), for example, are not addressed. However, maybe of the +common problems people face- and, I'd argue, the vast majority of issues that +confuse and plague users- are addressed by simply moving over to Stackage.

If you haven't already, I highly recommend you give Stackage a try +today.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/12/hackage-docs-live-again.html b/public/blog/2014/12/hackage-docs-live-again.html new file mode 100644 index 00000000..86f5bd73 --- /dev/null +++ b/public/blog/2014/12/hackage-docs-live-again.html @@ -0,0 +1,37 @@ + Hackage docs live again, please do the READMEs! +

Hackage docs live again, please do the READMEs!

December 21, 2014

GravatarBy Michael Snoyman

After sufficient complaining ensued on Reddit after my previous blog +post, +and enough claims of "this is so trivial to implement," I decided to bite the +bullet and just implement it. After debugging a lens bash +script, +I reimplemented it in pure +Haskell and +added it to my mega repo tool +chain to be part of my normal +release process.

So for everyone who thinks that Hackage is the right place to have +documentation: it's there again. I'm still quite unsatisfied with the whole +situation, and have wasted yet another hour of my life on what in my mind is +meaningless busywork. But so be it, it's frankly easier to solve a technical +problem than put up with the complaints. (I've certainly spent more than an +hour in the past four years explaining to people multiple times why docs on +Hackage don't exist for my packages.)

Now I'll put out a request: will someone please implement READMEs on +Hackage? Yes, I could do +this, but I've never touched the code base, and have lots of other things to +do. The current situation of having to describe our packages at least three +times (synopsis, description, and README) is annoying, and the fact that some +forms of documentation cannot be expressed at all in description fields is very +problematic (see linked mailing list thread in that issue).

I hope I haven't just set a precedent that complaining at me gets me to do work +I don't like...

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/12/minimal-yesod-multifile-site.html b/public/blog/2014/12/minimal-yesod-multifile-site.html new file mode 100644 index 00000000..5a4613d8 --- /dev/null +++ b/public/blog/2014/12/minimal-yesod-multifile-site.html @@ -0,0 +1,27 @@ + Minimal Yesod multifile site +

Minimal Yesod multifile site

December 16, 2014

GravatarBy Michael Snoyman

I was talking with a coworker yesterday who was uncertain of how to start on a +Yesod application without using the scaffolding. A single-file application is +easy, and is well +documented. The +question is: how do you make a trivial multi-file site, where the handler +functions can live in individual modules and still use type-safe URLs?

The problem in achieving this is separating out the data type creation from the +dispatch generation. This is also discussed in the +book, +but what seems to be lacking is a simple cookbook-style example. So here it is!

This example serves a very minimal site (actually, it even has a JSON service), +but can easily be extended to support more complex things. This is a good basis +for people looking to implement minimal services without the need for things in +the scaffolding like authentication, configurable logging, etc.

Also, if anyone's interested, it would be possible to add this as one of the +scaffolding options from yesod init.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/12/use-stackage-for-docs.html b/public/blog/2014/12/use-stackage-for-docs.html new file mode 100644 index 00000000..12239cf7 --- /dev/null +++ b/public/blog/2014/12/use-stackage-for-docs.html @@ -0,0 +1,38 @@ + Use Stackage for docs +

Use Stackage for docs

December 20, 2014

GravatarBy Michael Snoyman

I'm writing this blog post to address a personal annoyance of mine as the +maintainer of a large number of Haskell packages. +Very +often, I get bug reports about +lack of documentation on Hackage. This has occurred for years. Most people who +file these issues are not aware of the fact that lack of documentation error is +more often than not a problem with Hackage. Some people are aware of this, and +are asking me to start running a separate tool every time I upload a package to +generate the documentation locally.

I have another annoyance with documentation on Hackage: I'm forced to write my +package's description in a very strange Haddock-inside-cabal format in the +cabal file itself. I need to write a description in any event in a README for +users on Github, so this is purely wasted efforted.

To address both of these issues at the same time, I've started modifying the +description +field +in my package's to give a link to their Stackage +address. I'm doing this out of laziness on my part: I can now feel confident +that documentation will be available at pages where people will be pointed to, +I will hopefully get less needless issues opened about "Hackage documentation +is broken," and I don't need to keep two meaningful descriptions of all of my +packages written (one in the weird cabal/Haddock format, one in much nicer +Markdown).

Others are clearly welcome to do this as well, but my main motivation here is +explaining my reasoning for these changes, so I don't get a flood of new +inquiries as to "why do you have such a strange description field in all your +packages?" For those wishing to emulate this, follow these steps:

  1. Make sure your package has a good README or README.md file with a description.
  2. Include the README or README.md file in the extra-source-files field of your cabal file.
  3. Update your description field with text like: "Hackage documentation generation is not reliable. For up to date documentation, please see: http://www.stackage.org/package/*name*."

UPDATE See my next blog post for aftermath of this.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2014/12/yesods-new-scaffolding.html b/public/blog/2014/12/yesods-new-scaffolding.html new file mode 100644 index 00000000..4ff680b2 --- /dev/null +++ b/public/blog/2014/12/yesods-new-scaffolding.html @@ -0,0 +1,49 @@ + Yesod's new scaffolding +

Yesod's new scaffolding

December 3, 2014

GravatarBy Michael Snoyman

After lots of discussion and testing, I'm happy to announce a significant +update to the Yesod scaffolded site. You can see some mailing list discussions +on this at:

The newest version of yesod-bin (and all versions going forward) will ship with +this newly minted scaffolding. There is no migration guide for existing sites; +scaffoldings aren't the kind of things to be easily migrated, and all existing +sites will continue to function due to lack of breaking API changes. +Nonetheless, if people are excited enough about these changes that they'd like +to integrate them into their existing sites, please start up a discussion on +the mailing list.

The primary motivation in this change is an overhaul to the settings system. +The new system is much more modular regarding environment variables, config +files, and hard-coded settings. It's also much simpler to determine where a +setting value comes from, and to configure things differently in different +environments. Since I've already described these changes in quite some detail, +I will instead point you to the second mailing list thread linked +above.

To give an example of how this works, take a look at the following lines from +the new settings.yml file:

host:           "_env:HOST:*4" # any IPv4 host
+port:           "_env:PORT:3000"
+approot:        "_env:APPROOT:http://localhost:3000"

In the old scaffolding, both PORT and APPROOT were recognized environment +variable names, that would override whatever was defined in settings.yml. +However, that overriding occurred in a library (Yesod.Default.Config) and was +not obvious to users, which led to many bug reports/support questions. +Additionally, there was no way to override the host/interface binding via +environment variables. In the new system:

  • It's obvious from reading the config file which settings can be overridden via environment variables, and what the names of those variables are.
  • It's trivial to change the names, or disable environment variable overriding (e.g., replace the approot line with approot: http://localhost:3000).
  • New arbitrary settings can be added at will, and easily use environment variables to override their values.

The other change is that we've replaced the ad-hoc Import prelude replacement +previously found in the scaffolding with ClassyPrelude.Yesod. For users not +interested in classy-prelude, it should be trivial to replace import +ClassyPrelude.Yesod with import Prelude, import Yesod, and whatever else +you want. Greg has also spent quite some time in the past week improving the +documentation for both classy-prelude and mono-traversable, which will help +things out considerably.

Besides that, I've done a bunch of minor maintenance on the scaffolding, +cleaning up import lists, adding some missing documentation, and so on.

Like any new release, it's certainly possible that there are some issues, so if +you notice any, please feel free to bring them up. Also, documentation fixes +are great as well. As always, please send any pull requests against the +postgres branch of the yesod-scaffold +repo, from which we +automatically merge changes to all other versions of the scaffolding.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/01/polyconf-2015.html b/public/blog/2015/01/polyconf-2015.html new file mode 100644 index 00000000..10058da8 --- /dev/null +++ b/public/blog/2015/01/polyconf-2015.html @@ -0,0 +1,25 @@ + PolyConf 2015 +

PolyConf 2015

January 25, 2015

GravatarBy Michael Snoyman

Last year I spoke at PolyConf +about client/server Haskell webapps, and especially on GHCJS. This year's +PolyConf has just opened up a Call for +Proposals.

I really enjoyed my time at the conference last year: it's a great place to +meet and interact with people doing cool things in other languages, and can be +a great source of inspiration for ways we can do things better. Also, as I've +mentioned +recently, I +think it's a great thing for us Haskellers to put out content accessible by the +non-Haskell world, and a conference like this is a perfect way to do so.

If you have a topic that you think polyglot programmers would be interested in, +please take the time to submit a proposal for the conference.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/02/awesome-haskell-community.html b/public/blog/2015/02/awesome-haskell-community.html new file mode 100644 index 00000000..44ea4c6c --- /dev/null +++ b/public/blog/2015/02/awesome-haskell-community.html @@ -0,0 +1,70 @@ + The awesome Haskell community +

The awesome Haskell community

February 13, 2015

GravatarBy Michael Snoyman

Recently the Haskell community has been engaged in a very intense discussion +around potential changes to the Prelude (aka "burning bridges" or "FTP"). +Here's the most recent incarnation of the discussion for +context. +The changes under discussion are non-trivial, and many people are putting in a +huge amount of energy to try and make Haskell the best it can be. And to be +clear: I'm talking about people arguing on both sides of this discussion, and +people trying to moderate it. As someone who's mostly been sitting on the +sidelines in this one, I want to start by expressing a big thank you to +everyone working on this.

(If anyone's wondering why I'm mostly sitting this one out, it's because I +don't feel very strongly about it either way. I think there are great arguments +going both ways, and over the past week I've fluctuated between being -0.2 on +the proposal and being +0.2.)

When a big discussion like this happens, it's easy for people to misinterpret +it as something unhealthy. I'm here to remind everyone that, in fact, the +opposite is true: what we're seeing now is the sign of an incredibly healthy +community, based on an amazing language, that is undertaking extraordinary +things. And there are of course some warts being revealed too, but those are +relatively minor, and I believe we will be able to overcome them.

So to begin my cheerleading:

The fact that we can even consider doing this is amazing. I don't think +very many languages could sustain a significant rewrite of their most core +library. Let's just keep things in perspective here: even the worst case +scenario damage from this change involves updating some documentation and +modifying a relatively small amount of code in such a way that will be +backwards compatible with old versions of the library. This is a true testament +not only to the power of the Haskell language, but to the thoughtfulness with +which this proposal was made.

The discussion has been incredibly civil. This topic had all the makings +for an internet flame war: strongly held opinions, good arguments on both +sides, lots of time and effort involved, and Reddit. I am happy to say that I +have not seen a single personal attack in the entire discussion. Almost every +piece of discourse has been beyond reproach, and the few times where things +have gotten close to crossing the line, people on both sides have politely +expressed that sentiment, leading to the original content being removed.

To some extent, I think we're all a bit spoiled by how good the civility in the +Haskell world is, and we should take a moment to appreciate it. That's not to +say we should ever expect any less, but we should feel comfortable patting +ourselves on the back a bit.

We're dynamically coming up with new, better processes. When opinions are +so strongly divided, it's difficult to make any kind of progress. As a +community, we're adapting quickly and learning how to overcome that. As you can +see in the thread I linked above, we now have a clear path forward: a feedback +form that will be processed by Simon PJ and Simon Marlow, who will make the +ultimate decision. This process is clear, and we couldn't be more fortunate to +have such great and well respected leaders in our community.

Nothing else has stopped. If you look at issue trackers, commit logs, and +mailing list discussions, you can see that while many members of the community +are actively participating in this discussion, nothing else has ground to a +halt. We're a dynamic community with many things going on, so the ability to +digest a major issue while still moving forward elsewhere is vital.


That said, I think there are still some areas for improvement. The biggest one +lies with the core libraries committee, of which I'm a member. We need to learn +how to be better at communicating with the community about these kinds of large +scale changes. I'm taking responsibility for that problem, so if you don't see +improvements on that front in the next few weeks, you can blame me.

More generally, I think there are some process and communications improvements +that can be made at various places in the community. I know that's an +incredibly vague statement, but that's all I have for the moment. I intend to +follow up in the coming weeks with more concrete points and advice on how to +improve things.

In sum: Haskell's an amazing language, which has attracted an amazing +community. This discussion doesn't detract from that statement, but rather +emphasizes it. Like any group, we can still learn to do a few things better, +but we've demonstrated time and again (including right now!) that we have the +strength to learn and improve, and I'm certain we'll do so again.

I'm proud to be part of this community, and everyone else should be as well.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/02/brittle-haskell-toolchain.html b/public/blog/2015/02/brittle-haskell-toolchain.html new file mode 100644 index 00000000..a3c688bf --- /dev/null +++ b/public/blog/2015/02/brittle-haskell-toolchain.html @@ -0,0 +1,104 @@ + The brittle Haskell toolchain +

The brittle Haskell toolchain

February 9, 2015

GravatarBy Michael Snoyman

A few weeks ago I received a bug report against +streaming-commons. Since +then, the details of what we discovered when discussing this report have been +bothering me quite a bit, as they expose a lot of the brittleness of the +Haskell toolchain. I'm documenting all of these aspects now to make clear how +fragile our tooling is, and thereby explain why I think Stackage is so vital to +our community.

In this blog post, I'm going to describe six separate problems I've identified +when looking into this issue, and explain how Stackage (or some similar +deterministic build system) would have protected users against these problems +had it been employed.

The story

streaming-commons is a library that provides helper utilities for a number of +different streaming concepts, one of them being a streaming way to convert +blaze-builder Builders to filled ByteString buffers. Since blaze-builder +was released a few years ago, a new set of modules was added to the bytestring +package in version 0.10 known as a "bytestring builder." I asked one of the +engineers at FP Complete, Emanuel Borsboom, to start working on a new module for +streaming-commons to provide similar functionality for bytestring builder.

And now we run into the first problem with the Haskell toolchain. You would +think that we should just add a lower bound on bytestring >= 0.10 in the +streaming-commons.cabal file. However, setting restrictive lower bounds on +ghc-package dependencies can be a +problem. +Fortunately, Leon Smith already solved this problem for us with +bytestring-builder, +which provides a compatibility layer for older bytestrings (much like Ed's +transformers-compat). +The idea is that, when compiled against an older version of bytestring, the +bytestring-builder package provides the necessary missing modules, and +otherwise does nothing.

When Emanuel wrote his changes to streaming-commons, he added a dependency on +bytestring-builder. We then proceeded to test this on multiple versions of GHC +via Travis CI and Herbert's +multi-ghc-travis. Everything +compiled and passed tests, so we shipped the updated version.

However, that original bug report I linked +to- reported by Ozgun +Ataman- told us there was a problem with GHC 7.6. This was pretty surprising, +given that we'd tested on GHC 7.6. Fortunately Lane Seppala discovered the +culprit: +the Cabal library. It turns out that installing a new version of the Cabal +library causes the build of streaming-commons to break, whereas our tests just +used the default version of Cabal shipped with GHC 7.6. (We'll get back to +why that broke things in a bit.)

After some digging, Emanuel +discovered +the deeper cause of the problem: Bryan O'Sullivan +reported an issue a +year ago where- when using a new version of the Cabal library- +bytestring-builder does not in fact provide it's compatibility modules. This +leads us to our second issue: this known bug existed for almost +a year without resolution, and since it only occurs in unusual circumstances, +was not detected by any of our automated tooling.

The reason this bug existed though is by far the most worrisome thing I saw in +this process: the Cabal library silently changed the semantics of one of its +fields in the 1.18 (or 1.20? I'm not sure) release. You see, bytestring-builder +was detecting which version of bytestring it was compiled against by inspecting +the configConstraints field (you can see the code yourself on +Hackage). +And starting in Cabal 0.19.1 (a development release), that field was no longer +being populated. As a result, as soon as that newer Cabal library was +installed, the bytestring-builder package became worse than useless.

As an aside, this points to another problematic aspect of our toolchain: there is +no way to specify constraints on dependencies used in custom Setup.hs files. +That's actually causes more difficulty than it may sound like, but I'll skip +diving into it for now.

The fix for this was relatively +simple: +use some flag logic in the cabal file instead of a complicated custom +Setup.hs file. (Once this pull request was merged in and released, it did +fix the original bug report.) But don't take this as a critique of Leon's +choice of a complicated Setup.hs file. Because in reality, the flag trick- +while the "standard" solution to this problem- broke cabal-install's +dependency solver for quite a +while. To be fair, I'm still +not completely convinced that the bug is fixed, but for now that bug is the +lesser of two evils vs the Cabal library bug.

And finally, based on the bug report from Ozgun, it seems like an internal +build failed based on all of this occurring. This has been a constant criticism +I've made about the way we generally do builds in the Haskell world. Rarely is +reproducibility a part of the toolchain. To quote Ozgun:

We are in fact quite careful in dependency management with lower-upper bounds +on most outside packages, so breakages like this are unexpected.

And many people feel that this is the way things should be. But as this +discussion hopefully emphasizes, just playing with lower and upper bounds is +not sufficient to avoid build failures in general. In this case, we're +looking at a piece of software that was broken by a change in a library that +it didn't depend on, namely Cabal, since our tooling makes an implicit +dependency on that library, and we have no way of placing bounds on it.

The case for Stackage

So here are the toolchain problems I've identified above:

  1. Tight coupling between GHC version and some core libraries like bytestring.
  2. A known issue lasting undocumented for a corner case for over a year, without any indication on the Hackage page that we should be concerned.
  3. The Cabal library silently changed the semantics of a field, causing complete breakage of a package.
  4. cabal-install's solver gets confused by standard flag usage, at least in slightly older versions.
  5. Not all dependencies are actually specified in a cabal file. At the very least, the Cabal library version is unconstrained, and any other packages used by Setup.hs.
  6. Default Haskell toolchain doesn't protect us against these kinds of problems, or give us any concept of reproducibility.

Stackage completely solves (2), (3), (5), and (6) for end users. By specifying +all library versions used, and then testing all of those versions together, we +avoid many possible corner cases of weird library interactions, and provide a +fully reproducible build. (Note the Stackage doesn't solve all such cases: +operating system, system libraries, executables, etc are still unpinned. That's +why FP Complete is working on Docker-based +tooling.)

(1) is highly mitigated by Stackage because, even though the tight coupling +still exists, Stackage provides a set of packages that take that coupling +into account for you, so you're not stuck trying to put the pieces together +yourself.

As for (4)... Stackage helps the situation by making the job of the +solver simpler by pinning down version numbers. Unfortunately, there are still +potential gotchas when encountering solver bugs. Sometimes we end up needing to +implement terribly awkward solutions to work around those +bugs.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/03/designing-apis-for-extensibility.html b/public/blog/2015/03/designing-apis-for-extensibility.html new file mode 100644 index 00000000..41106c29 --- /dev/null +++ b/public/blog/2015/03/designing-apis-for-extensibility.html @@ -0,0 +1,97 @@ + Designing APIs for Extensibility +

Designing APIs for Extensibility

March 10, 2015

GravatarBy Michael Snoyman

This is a snapshot of some content I wrote for the Commercial Haskell group's +Haskell Documentation +project. The +official home for this content is in that +repo. +As I often do, I'm copying the content into the blog post itself for ease of +viewing.


Every time you make a breaking change in your API, it means that- potentially- +one or more of your users will need to change his/her code to adapt. Even if +this update is trivial, it adds friction to the code maintenance process. On +the other hand, we don't want to be constrained by bad design choices early on +in a project, and sometimes a breaking API change is the best option.

The point of this document, however, is to give you a third option: design your +APIs from the outset to be extensible. There are common techniques employed in +the Haskell world to make APIs that are resilient to changing feature-sets, and +by employing them early on in your design process, you can hopefully avoid the +painful choices between a better API and happy users.

Almost all techniques start with implementation hiding. Guidelines here are +simple: don't expose anything non-public. For example, if you write a number of +helper functions, you may not want to start off by exposing them, since you're +then telling users that these are good, stable functions to be relied upon. +Instead, use explicit export lists on your modules and only include functions +that are intended for public consumption.

More important- and more tricky- than functions are data constructors. In many +cases, you want to avoid exposing the internals of your data types to users, to +allow you to expand on them in the future. A common use case for this is some +kind of a data type providing configuration information. Consider that you're +going to communicate with some web services, so you write up the following API:

module MyAPI
+    ( Settings (..)
+    , makeAPICall
+    ) where
+
+data Settings = Settings
+    { apiKey :: Text
+    , hostName :: Text
+    }
+
+makeAPICall :: Settings -> Foo -> IO Bar

The way your users will access this will be something like:

makeAPICall Settings
+    { apiKey = myAPIKey
+    , hostName = "www.example.com"
+    } myFoo

Now suppose a user points out that, in some cases, the standard port 80 is +not used for the API call. So you add a new field port :: Int to your +Settings constructor. This will break your user's code, since the port +field will not be set.

Instead, a more robust way of specifying your API will look like:

module MyAPI
+    ( Settings
+    , mkSettings
+    , setHostName
+    , makeAPICall
+    ) where
+
+data Settings = Settings
+    { apiKey :: Text
+    , hostName :: Text
+    }
+
+-- | Create a @Settings@ value. Uses default value for host name.
+mkSettings :: Text -- ^ API Key
+           -> Settings
+mkSettings key = Settings
+    { apiKey = key
+    , hostName = "www.example.com"
+    }
+
+setHostName :: Text -> Settings -> Settings
+setHostName hn s = s { hostName = hn }
+
+makeAPICall :: Settings -> Foo -> IO Bar

Now your user code will instead look like:

makeAPICall (mkSettings myAPIKey) myFoo

This has the following benefits:

  • The user is not bothered to fill in default values (in this case, the hostname).
  • Extending this API to allow for more fields in the future is trivial: add a new set* function. Internally, you'll add a field to Settings and set a default value in mkSettings.

One thing to note: please do not expose the field accessors directly. If you +want to provide getter functions in addition to setters, write them explicitly, +e.g.:

getHostName :: Settings -> Text
+getHostName = hostName

The reason for this is that by exposing field accessors, users will be able to write code such as:

(mkSettings myAPIKey) { hostName = "www.example.org" }

This ties your hand for future internal improvements, since you are now +required to keep a field of name hostName with type Text. By just using +set and get functions, you can change your internal representation +significantly and still provide a compatibility layer.

For those of you familiar with other languages: this is in fact quite similar +to the approach taken in Java or C#. Just because Java does it doesn't mean +it's wrong.

Note that this advice is different to, and intended to supersede, the settings +type approach. Projects like Warp +which previously used that settings type approach are currently migrating to +this more extensible approach.

Also, while settings have been used here as a motivating example, the same +advice applies to other contexts.

Internal modules

One downside of implementation hiding is that it can make it difficult for +users to do things you didn't intend for them to do with your API. You can +always add more functionality on demand, but the delay can be a major nuissance +for users. A compromise solution in the Haskell community is to provide a +.Internal module for your project which exports non-quite-public components. +For example, in wai, the Response constructors are exposed in a +Network.Wai.Internal module. Normally, users are supposed to use smart +constructors like responseFile, but occasionally they'll want more +fine-grained control.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/04/announcing-stackage-update.html b/public/blog/2015/04/announcing-stackage-update.html new file mode 100644 index 00000000..3f6d12b4 --- /dev/null +++ b/public/blog/2015/04/announcing-stackage-update.html @@ -0,0 +1,33 @@ + Announcing stackage-update +

Announcing stackage-update

April 17, 2015

GravatarBy Michael Snoyman

I just released a simple tool to Hackage called +stackage-update. Instead +of repeating myself, below is a copy-paste of the README.md from the Github +repository.


This package provides an executable, stackage-update, which provides the same +functionality as cabal update (it updates your local package index). However, +instead of downloading the entire package index as a compressed tarball over +insecure HTTP, it uses git to incrementally update your package list, and +downloads over secure HTTPS.

It has minimal Haskell library dependencies (all dependencies are shipped with +GHC itself) and only requires that the git executable be available on the +PATH. It builds on top of the +all-cabal-files +repository.

Advantages

Versus standard cabal update, using stackage-update gives the following advantages:

  • Only downloads the deltas from the last time you updated your index, threby requiring significantly less bandwidth
  • Downloads over a secure HTTPS connection instead of an insecure HTTP connection
    • Note that the all-cabal-files repo is also updated from Hackage over a secure HTTPS connection

Usage

Install from Hackage as usual with:

cabal update
+cabal install stackage-update

From then on, simply run stackage-update instead of cabal update.

Why stackage?

You may be wondering why this tool is called stackage-update, when in fact +the functionality is useful outside of the Stackage +project itself. The reason is that the naming +allows it to play nicely with the other Stackage command line tooling. +Concretely, that means that if you have stackage-cli installed, stackage-update +works as a plugin. However, you can certainly use stackage-update on its own +without any other tooling or dependencies on the Stackage project.

Future enhancements

  • If desired, add support for GPG signature checking when cloning/pulling from the all-caba-files repo

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/05/deprecating-system-filepath.html b/public/blog/2015/05/deprecating-system-filepath.html new file mode 100644 index 00000000..054242ba --- /dev/null +++ b/public/blog/2015/05/deprecating-system-filepath.html @@ -0,0 +1,43 @@ + Deprecating system-filepath and system-fileio +

Deprecating system-filepath and system-fileio

May 14, 2015

GravatarBy Michael Snoyman

I posted this information on +Google+, but it's +worth advertising this a bit wider. The tl;dr is: system-filepath and +system-fileio are deprecated, please migrate to filepath and directory, +respectively.

The backstory here is that system-filepath came into existence at a time when +there were bugs in GHC's handling of character encodings in file paths. +system-filepath fixed those bugs, and also provided some nice type safety to +prevent accidentally treating a path as a String. However, the internal +representation needed to make that work was pretty complicated, and resulted in +some weird corner case bugs.

Since GHC 7.4 and up, the original character encoding issues have been +resolved. That left a few options: continue to maintain system-filepath for +additional type safety, or deprecate. John Millikin, the author of the package, +decided on the latter back in +December. Since we +were using it extensively at FP Complete via other libraries, we decided to +take over maintenance. However, this week we decided that, in fact, John was +right in the first place.

I've already migrated most of my libraries away from system-filepath (though +doing so quickly was a +mistake, sorry +everyone). One nice benefit of all this is there's no longer a need to convert +between different FilePath representations all over the place. I still +believe overall that type FilePath = String is a mistake and a distinct +datatype would be better, but there's much to be said for consistency.

Some quick pointers for those looking to convert:

  • You can drop basically all usages of encodeString and decodeString
  • If you're using basic-prelude or classy-prelude, you should get some deprecation warnings around functions like fpToString
  • Most functions have a direct translation, e.g. createTree becomes createDirectoryIfMissing True (yes, the system-filepath and system-fileio names often times feel nicer...)

And for those looking for more type safety: all is not lost. Chris Done has +been working on a new +package +which is aimed at providing additional type safety around absolute/relative and +files/directories. It's not yet complete, but is already seeing some +interesting work and preventing bugs at some projects we've been working on +(and which will be announced soon).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/06/cabals-does-not-exist-error-message.html b/public/blog/2015/06/cabals-does-not-exist-error-message.html new file mode 100644 index 00000000..9eef9323 --- /dev/null +++ b/public/blog/2015/06/cabals-does-not-exist-error-message.html @@ -0,0 +1,33 @@ + cabal's does not exist error message +

cabal's does not exist error message

June 11, 2015

GravatarBy Michael Snoyman

I've seen many people confused by this error message, and just ran into it +myself, so decided a blog post to explain was in order.

As I was hacking on stack this +morning, I pushed and got a build failure from +Travis with +the following content at the end:

Failed to install asn1-encoding-0.9.0
+Last 10 lines of the build log ( /home/travis/.cabal/logs/asn1-encoding-0.9.0.log ):
+cabal: /home/travis/.cabal/logs/asn1-encoding-0.9.0.log: does not exist

What the error message means is: I tried to load up the log file containing the +output of the build, but the file does not exist. It's possible that there are +multiple reasons for this, but I know of only one: when cabal is unable to +download the necessary package from Hackage, usually because Hackage is having +a temporary blip.

As a user: if you see this message, just try running your command again. If +it's a temporary Hackage outage, the second attempt may succeed. Another option +(which I'd strongly recommend) is to switch to a more reliable package index; +see the blog post on FP Complete's S3 +mirror for more +details. I've been pushing for a while to make this the default remote-repo +used by cabal-install, but unfortunately that hasn't happened. (For the record: +stack does use the S3 mirror by default.)

For cabal: I see three steps worth taking:

  1. Fix this confusing error message to say something like "Download of http://... failed". I thought there was a cabal issue opened about this already, which is why I didn't open up a new one right now. If there isn't one, it's worth opening.
  2. Automatically retry a failing download. I'm not convinced this is a good thing to do, but it's a possible mitigation. (We don't do that right now in stack, though I've been toying with the idea. Feedback appreciated.)
  3. Switch to a more reliable mirror for the default remote-repo.

Fixing this instability at the Hackage level would of course also be a good +move.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/06/cleaning-up-warp-apis.html b/public/blog/2015/06/cleaning-up-warp-apis.html new file mode 100644 index 00000000..691f9c77 --- /dev/null +++ b/public/blog/2015/06/cleaning-up-warp-apis.html @@ -0,0 +1,24 @@ + Cleaning up the Warp APIs +

Cleaning up the Warp APIs

June 25, 2015

GravatarBy Kazu Yamamoto

For the last one and a half years, I have been trying to implement HTTP/2 in Warp. +Since both HTTP/2 implementations of Firefox and Chrome requires TLS for HTTP/2, +I'm also trying to improve the performance of WarpTLS. +In the process, I need to change the Connection data type. +I felt nervous a bit because Connection is exported from the top module, Network.Wai.Handler.Warp. +I believe there are only two users of this data type: Warp itself and WarpTLS. +Normal users do not use it. +So, Connection should be exported from the Internal module. +This motivated me to clean up the Warp APIs.

The APIs of Warp 3.0 has the following issues:

  • The top module exports many internal APIs which are prone to change.
  • The Internal module does not export enough internal APIs.

Michael and I decided to clean up the Warp APIs. +The following changes will be made in the version 3.1:

  • Already deprecated APIs (settingsFoo) are removed from the top module.
  • In the documentation of the top module, each API is categorized into either standard or internal.
  • The Internal module exports all internal APIs including the internal APIs in the top module.
  • Stopping exporting the Buffer and Timeout module which have been exported from the top module.

The standard APIs of the top module mainly consist of high-level run functions, Settings related stuff and necessary data types. We try to maintain these stably.

The internal APIs in the top module will be removed in Warp version 3.2. This is just documented. DEPRECATED pragmas are not added since there is no simple way to make an API deprecated in the top moudle but live in the internal module.

Warp version 3.1 is not released yet but is available from github repository. We will wait for a week or two to hear users' opinions.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/06/stack-support-yesod-devel.html b/public/blog/2015/06/stack-support-yesod-devel.html new file mode 100644 index 00000000..ffc3dc32 --- /dev/null +++ b/public/blog/2015/06/stack-support-yesod-devel.html @@ -0,0 +1,43 @@ + stack support for yesod devel +

stack support for yesod devel

June 29, 2015

GravatarBy Michael Snoyman

Since the release of the stack build +tool, +I think I've received four different requests and bug reports about stack +support in Yesod. The sticking point is that yesod devel has not- until now- +had support for using stack. The original plan was to wait for Njagi Mwaniki's +Google Summer of Code project to finish the new ide-backend based yesod devel. +However, demand for this feature was too high, so I'm happy to announce +official stack support in yesod-bin-1.4.11.

This blog post should be consider a beta announcement for using Yesod with +stack. Please test and report issues. Also, the workflow is not yet perfected. +Once we get stack new +written and add ide-backend +support, things will be +much smoother. For now, here's the workflow:

# Install both yesod-bin and cabal-install. Both are still necessary
+$ stack install yesod-bin-1.4.11 cabal-install
+# Initialize your project
+$ stack exec yesod init
+$ cd new-directory
+# Create stack.yaml
+$ stack init
+# Build your project to get al dependencies
+# Also rebuild yesod-bin with the current GHC, just to be safe
+$ stack build yesod-bin-1.4.11 .
+# Now run yesod devel
+$ stack exec yesod devel

Like I said, this is a little kludgy right now, but will smooth out over time.

Technical details

If you're curious in how I added this support, the commit is really +short. +And there's not actually anything stack-specific in here, it's just working +around a well known limitation in cabal which makes it incompatible with the +GHC_PACKAGE_PATH environment variable. All we do in yesod devel is:

  • Detect the GHC_PACKAGE_PATH variable
  • Turn it into --package-db arugments to cabal configure
  • Remove GHC_PACKAGE_PATH from the environment of the cabal process

It would be nice if cabal got this functionality itself in the future, but I +believe the current implementation was a design goal of the cabal team.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/07/http2.html b/public/blog/2015/07/http2.html new file mode 100644 index 00000000..5fa79091 --- /dev/null +++ b/public/blog/2015/07/http2.html @@ -0,0 +1,45 @@ + Supporting HTTP/2 +

Supporting HTTP/2

July 23, 2015

GravatarBy Kazu Yamamoto

We are happy to announce that Warp version 3.1.0 and WarpTLS version 3.1.0 have been released. These versions include the following changes:

But the main new feature is HTTP/2 support! +The latest versions of Firefox and Chrome support HTTP/2 over TLS. +WarpTLS uses HTTP/2 instead of HTTP/1.1 if TLS ALPN(Application-Layer Protocol Negotiation) selects HTTP/2. +So, if you upgrade Warp and WarpTLS in your site serving TLS and anyone visits your site with Firefox or Chrome, your contents are automatically transferred via HTTP/2 over TLS.

HTTP/2 retains the semantics of HTTP/1.1 such as request and response headers, meaning you don't have to modify your WAI applications, just link them to the new version of WarpTLS. Rather, HTTP/2 redesigned its transport to solve the following issues:

  1. Redundant headers: HTTP/1.1 repeatedly transfers almost exactly the same headers for every request and response, wasting bandwidth.
  2. Poor concurrency: only one request or response can be sent in one TCP connection at a time(request pipelining is not used in practice). What HTTP/1.1 can do is make use of multiple TCP connections, up to 6 per site.
  3. Head-of-line blocking: if one request is blocked on a server, no other requests can be sent in the same connection.

To solve the issue 1, HTTP/2 provides a header compression mechanism called HPACK. +To fix the issue 2 and 3, HTTP/2 makes just one TCP connection per site and multiplex frames of requests and responses asynchronously. The default number of concurrency is 100.

I guess that the HTTP/2 implementors agree that the most challenging parts of HTTP/2 are HPACK and priority. HPACK is used to define reference sets as well as indices and Huffman encoding. During standardization activities, I found that reference sets make the spec really complicated, but does not contribute to the compression ratio. My big contribution to HTTP/2 was a proposal to remove reference sets from HPACK. The final HPACK became much simpler.

Since multiple requests and responses are multiplexed in one TCP connection, +priority is important. +Without priority, the response of a big file download would occupy the connection. +I surveyed priority queues but could not find a suitable technology. +Thus, I needed to invent random heaps by myself. +If time allows, I would like to describe random heaps in this blog someday. +The http2 library provides +well-tested HPACK and structured priority queues as well as +frame encoders/decoders.

My interest on implementing HTTP/2 in Haskell was how to map +Haskell threads to HTTP/2 elements. +In HTTP/1.1, the role of Haskell threads is clear. +That is, one HTTP (TCP) connection is a Haskell thread. +After trial and error, I finally reached an answer. +Streams of HTTP/2 (roughly, a pair of request and response) is a Haskell thread. +To avoid the overhead of spawning Haskell threads, +I introduced thread pools to Warp. +Yes, Haskell threads shine even in HTTP/2.

HTTP/2 provides plain (non-encrypted) communications, too. +But since Firefox and Chrome require TLS, +TLS is a MUST in practice. +TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 is a mandate cipher suite in HTTP/2. +Unfortunately, many pieces were missing in the tls library. +So, it was necessary for me to implement +ALPN, ECDHE(Elliptic curve Diffie-Hellman, ephemeral) and AES GCM(Galois/Counter Mode). They are already merged into the tls and cryptonite library.

My next targets are improving performance of HTTP/2 over TLS and implementing TLS 1.3.

I would like to thank Tatsuhiro Tsujikawa, the author of nghttp2 -- the reference implementation of HTTP/2 and Moto Ishizawa, the author of h2spec. Without these tools, I could not make such mature Warp/WarpTLS libraries. They also answered my countless questions. +RFC 7540 says "the Japanese HTTP/2 community provided invaluable contributions, +including a number of implementations as well as numerous technical +and editorial contributions". +I'm proud of being a member of the community.

Enjoy HTTP/2 in Haskell!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/07/s3-hackage-mirror-travis.html b/public/blog/2015/07/s3-hackage-mirror-travis.html new file mode 100644 index 00000000..bfe0e61e --- /dev/null +++ b/public/blog/2015/07/s3-hackage-mirror-travis.html @@ -0,0 +1,56 @@ + S3 Hackage mirror for Travis builds +

S3 Hackage mirror for Travis builds

July 31, 2015

GravatarBy Michael Snoyman

Yesterday, I noticed a bunch of Travis build +failures with the +cabal error message "does not exist." As I covered last +month, +this is typically due to a download failure when trying to install packages. +This leaves some ambiguity as to where the download failure originated from: +was the client (in this case, Travis) having network issues, or was there a +problem on Hackage's end? (Or, of course, any one of dozens of other possible +explanations.)

I have no hard data to prove that the failure is from the Hackage side, but +anecdotal evidence suggests that it's Hackage. In particular, Gregory Collins +reports that the Snap build +bot +is also having issues. When we had these issues in FP Haskell Center, we +resolved it by switching to an S3 +mirror, so I've +decided to start migrating my Travis jobs over to this mirror as well. This is +fairly straightforward. First, add a new script to your repo containing:

#!/bin/sh
+
+set -eux
+
+mkdir -p $HOME/.cabal
+cat > $HOME/.cabal/config <<EOF
+remote-repo: hackage.haskell.org:http://hackage.fpcomplete.com/
+remote-repo-cache: $HOME/.cabal/packages
+jobs: \$ncpus
+EOF

I called this file .travis-setup.sh, but you can give it whatever name you +want. Just make sure to set it as executable. Then, just make sure to run this +script from your .travis.yml script, likely in the before_install section, +e.g.:

before_install:
+ - export PATH=$HOME/.cabal/bin:/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
+ - ./.travis-setup.sh

My first rollout for this is the stack +repository. My plan is to keep +this blog post up-to-date with the best instructions for doing this, so if you +have recommendations on how to improve this setup, please report back to me (or +just send a pull +request.

Well Typed's TUF +implementation +is planned to allow cabal to download from mirrors automatically, which will +hopefully fix this problem in the future. But this change is a useful stop-gap +measure. The only downside I'm aware of is that the mirror can take up to 30 +minutes to sync with Hackage, so you may end up with a slightly older package +index than if you used Hackage itself.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/07/yesod-devel.html b/public/blog/2015/07/yesod-devel.html new file mode 100644 index 00000000..0fec35de --- /dev/null +++ b/public/blog/2015/07/yesod-devel.html @@ -0,0 +1,18 @@ + yesod-devel +

yesod-devel

July 22, 2015

GravatarBy Njagi Mwaniki

Yesod-devel

A new development server is upon us. It's name is yesod-devel.
This post is about yesod-devel which is my Google Summer of Code project and not the current yesod-devel that is part of the yesod framework. It's not yet available and is still under development, meaning a lot of things in this post may change.

yesod-devel is a development server for haskell web applications that are WAI compliant.

What we expect from the application.

This is my opinion of what I expect from the web application and this may therefore change depending on what the community thinks. I think this design is good and losely coupled and leaves a lot of freedom to the web application.

At the heart of your application (the root of your web application) we expect you to have an Application.hs file which holds the Appliaction module. This is the file pointed to by your main-is section of the .cabal file.:

This Application.hs file holds the main function which fires up a warp server at a an address and port specified in an environment variable. +Yesod devel will read everything it needs from the web application from environment variables and not from a config file.
It is the responsibility of the web application to set environment variables(setEnv). This way yesod-devel is very losely coupled to the web application. That is, we(yesod-devel) will not have to specify the names, paths of your config files or which serialization format it will use.

The environment variables we currently need are:

* haskellWebAddress="<ip_address>/localhost"
+* haskellWebPort="<port_number>"

What you should expect from yesod-devel.

Automatic source and data file discovery.

You shouldn't expect to tell yesod-devel where your source files or data files (hamelet files and so forth) are as long as your web application knows where everything is. All you need to do is call the yesod-devel binary inside your app's root.

Building and running your code.

yesod-devel when run in your web application's working directory will run build and run your application on localhost:3000.

Automatic code reloading.

Yesod-devel supports automatic code reloading for any file modified in the current working directory. This is more proof of just how losely coupled yesod-devel will be from your application.

Newly added files don't trigger a recompile and neither do deleted files. However, file modifications do trigger a recompile. +This is a deliberate design choice. Text editors as well as other programs keep adding and removing files from the file system and if we listened for any randomly created file or deleted file to trigger a recompile we would end up triggering useless recompiles.

This however means there's a trade-off. For being so losely coupled we have to manually restart yesod-devel everytime we add or delete files.

Reverse proxying.

Yesod-devel will start listening on the address and port specified in your environment variables haskellWebAddress and haskellWebPort respectively and reverse proxy it to your localhost:3000.

Report error messages to the browser.

Yesod-devel will report error messages from ghc to the web browser on localhost:3000.

Command line arguments.

Currently yesod-devel takes no command line arguments.

However, in the plans are the following.

  • --configure-with
  • --no-reverse-proxying
  • --show-iface fileName.hs

You should be fine without passing any of these arguments unless you have a special reason to.

Currently yesod-devel will configure your web application with the following flags to cabal.

  • -flibrary-only
  • --disable-tests
  • --disable-benchmarks
  • -fdevel
  • --disable-library-profiling
  • --with-ld=yesod-ld-wrapper
  • --with-ghc=yesod-ghc-wrapper
  • --with-ar=yesod-ar-wrapper
  • --with-hc-pkg=ghc-pkg

I assume that these arguments are self explanatory.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/07/yesod-table.html b/public/blog/2015/07/yesod-table.html new file mode 100644 index 00000000..2154bac0 --- /dev/null +++ b/public/blog/2015/07/yesod-table.html @@ -0,0 +1,124 @@ + yesod-table +

yesod-table

July 6, 2015

GravatarBy Andrew Thaddeus Martin

Announcing yesod-table

Over the last two years, I've seen the need for safe dynamic table-building +in half a dozen yesod projects I've worked on. After several design iterations, +the result of this experience is yesod-table, +which saw its first stable release last week. This blog post will contain code excerpts, +but you can also look at the documentation +the full example app on github, +which can be compiled and run.

Naive Solution

Before getting into specifics about yesod-table, I want to take a look at the naive +table-building strategy and identify the common pitfalls. Let's say that you have a data +types Color and Person:

data Color = Red | Green | Blue | Purple
+  deriving (Show)
+data Person = Person
+  { firstName     :: Text
+  , lastName      :: Text
+  , age           :: Int
+  , favoriteColor :: Color
+  }

We have a list of Person (let's call it people), and we want to show them all in a +table. You could write out a hamlet file like this:

<table>
+  <thead>
+    <tr>
+      <th>First Name</th>
+      <th>Last Name</th>
+      <th>Age</th>
+  <tbody>
+    $forall p <- people
+      <tr>
+        <td>#{firstName p}
+        <td>#{lastName p}
+        <td>#{show (age p)}

And there it is. This is the simplest solution to building a table. In fact, if you've worked +on web applications for any length of time, you've probably written code like this +before. I've implemented this pattern in PHP+html, in rails+haml, and in yesod+hamlet +projects. And every time, it is unsatisfactory.

Problems With Naive Solution

Let's take a look at three reasons why this solution leaves us wanting something more:

  • Duplication. After building a few tables this way, you realize that you are +copying the HTML elements and the list iteration ($forall) every time.
  • Non-composability. If I want to build a similar table, one that shows the +same fields but additionally has a column for favoriteColor, I have to copy +the whole thing. I can't glue another piece onto the end.
  • Breakable Invariant. If we do decide to add a favoriteColor column, we might +try simply adding <td>#{show (favoriteColor p)} to the end. This would cause incorrect +behavior at runtime, because we would have forgotten to add +<th>Favorite Color to the table header. The problem is that we have an invariant +not captured by the type system: thead and the tbody loop must have the same +number of <th>/<td> elements, and the order must match.

In particular, the last issue (breakable invariant) has been a source of great pains to me before. +On a three-column table, you are less likely to forget the <th> or put it in the wrong place. +As the table gets larger though (six or more columns), these mistakes become easier to make, and it's +harder to be sure that you did the right thing until you see it at runtime.

Example with yesod-table

So let's take a look at how yesod-table addresses these issues. The module it provides +should be imported as follows:

import Yesod.Table (Table)
+import qualified Yesod.Table as Table

Let's build the same table we saw earlier:

peopleBasicInfoTable :: Table site Person
+peopleBasicInfoTable = mempty
+  <> Table.text   "First Name" firstName
+  <> Table.text   "Last Name"  lastName
+  <> Table.string "Age"        (show . age)

And then we can feed it data and render it with buildBootstrap:

-- Although it's called buildBootstrap, it builds a table just fine
+-- if you aren't using bootstrap. It just adds bootstrap's table classes.
+getExamplePeopleR = defaultLayout $ Table.buildBootstrap peopleTable people

Explanation of Internals

The key to this approach is looking at a table pattern (called a Table in this library) +as a collection of columns, not a collection of rows. From the yesod-table source, we have:

newtype Table site a = Table (Seq (Column site a))
+  deriving (Monoid)
+
+data Column site a = Column
+  { header :: !(WidgetT site IO ())
+  , cell :: !(a -> WidgetT site IO ()) 
+  }

Each column is just the content that will go in the <th> (the value of header) and +a function that, given the data for a table row, will produce the content that belongs +in the <td>. A table is trivially a collection of columns and gets a Monoid +instance from Seq for free (for those unfamiliar, Seq a is like [a] but with +different performance characteristics). Consequently, any two Tables that are +parameterized over the same types can be concatenated. As a final note of explanation, +the Table.text function that we saw above just a helper for building singleton +tables. So, the three Tables below are equivalant:

import qualified Data.Sequence as Seq
+import qualified Data.Text as Text
+-- These three generate a single-column table that displays the age.
+reallyEasyToReadTable, easyToReadTable, hardToReadTable :: Table site Person
+reallyEasyToReadTable = Table.int "Age" age
+easyToReadTable = Table.text "Age" (Text.pack . show . age)
+hardToReadTable = Table.Table $ Seq.singleton $ Table.Column 
+  (toWidget $ toHtml "Age")
+  (toWidget . toHtml . show . age)

As should be clear, the convenience functions for singleton Tables should always be +preferred.

How Is This Better?

Now to address the most important question: Why is this better than what we had earlier? +Firstly, consider the issue of the breakable invariant. This is now a non-issue. Imagine +that we modified the earlier table to show a person's favorite color as well:

peopleFullInfoTable1 :: Table site Person
+peopleFullInfoTable1 = mempty
+  <> Table.text   "First Name"     firstName
+  <> Table.text   "Last Name"      lastName
+  <> Table.text   "Age"            (show . age)
+  <> Table.string "Favorite Color" (show . favoriteColor)

The table is correct by construction. You cannot forget the column header because +it's part of the Column data type. You're less likely to make this mistake, because +now that information is directly beside the content-extracting function, but even +if you somehow typed this instead, you would get a compile-time error:

  <> Table.string (show . favoriteColor)

Secondly, we can look at duplication. All the +table-rendering logic is moved into buildBootstrap (and you can write your own +table renderer if that one is not satisfactory). The Table that we are using now +has neither the HTML elements nor the list iteration that we dealt with earlier.

Finally, we can look at composability. As an alternative way of adding the column +for a person's favorite color, we could write:

peopleFullInfoTable2 :: Table site Person
+peopleFullInfoTable2 = mempty
+  <> peopleBasicInfoTable
+  <> Table.string "Favorite Color" (show . favoriteColor)

Additionally, if we need to promote this Table to work on something +like Entity Person (if it was backed by persistent), we could do this:

-- You need to use ekmett's contravariant package
+peopleEntityFullInfoTable :: Table site (Entity Person)
+peopleEntityFullInfoTable = contramap entityVal peopleFullInfoTable2

I won't go into contravariant functors here, but it's a very useful pattern +for working with Tables. The astute reader will notice +that the monoidal composition pattern shown earlier means that we can only append +or prepend columns. We cannot inject them into the middle. I'll give +yesod-table a B minus on to composability objective.

Closing Notes and Acknowledgements

One final closing note. You may have noticed that all of the Tables in this +post were parameterized over site. This is because they don't depend on +a particular foundation type. Usually, the way that this can happen is that +you use a route in one of the columns:

-- Assume that our foundation type was named App
+peopleTableWithLink :: Table App (Entity Person)
+peopleTableWithLink = mempty
+  <> peopleEntityFullInfoTable
+  <> Table.linked "Profile Page" (const "View") (PersonProfileR . entityKey)

The above example must be parameterized over App (or whatever your foundation +type is named), not over site.

This monoidal approach to building tables was inspired by Gabriel Gonzalez's +Equational Reasoning at Scale +and by the classic diagrams paper. +I hope that others find the library useful. I am very open to pull requests and suggestions, so if +you have an idea for a convenience function, feel free to open up an issue on +the github page.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/08/ssl-server-test.html b/public/blog/2015/08/ssl-server-test.html new file mode 100644 index 00000000..71f0c176 --- /dev/null +++ b/public/blog/2015/08/ssl-server-test.html @@ -0,0 +1,139 @@ + Getting Rating A from the SSL Server Test +

Getting Rating A from the SSL Server Test

August 31, 2015

GravatarBy Kazu Yamamoto

If you are using Warp TLS version 3.1.1 or earlier and the tls library version 1.3.1 or earlier, try the SSL Server Test provided by QUALYS SSL LABS. I'm sure that your server will get rating F and you will get disappointed. Here is a list of failed items:

+ + + + + + + + + + + + + + + + + + +
Secure Renegotiation Not supported ACTION NEEDED
Secure Client-Initiated Renegotiation Supported DoS DANGER
Insecure Client-Initiated Renegotiation Supported INSECURE
Downgrade attack prevention No, TLS_FALLBACK_SCSV not supported
Forward Secrecy With some browsers
Session resumption (caching) No (IDs assigned but not accepted)

Is the quality of the tls library low? The answer is NO. The code is really readable. But some small features were missing unfortunately. This article describes how Aaron Friel and I added such features to get an A rating.

If you are not interested in technical details, just upgrade Warp TLS to version 3.1.2 and the tls library to version 1.3.2. Your server automatically will get an A rating, or a T rating in the case of a self-signed certificate.

Secure Renegotiation

+ + + +
Secure RenegotiationNot supported ACTION NEEDED

The original TLS 1.2 renegotiation defined in RFC 5246 is now considered insecure because it is vulnerable to +man-in-the-middle attacks. +RFC 5746 +defines the "renegotiation_info" extension to authenticate both sides. +The tls library implemented this +but the result was "Not supported". +Why?

The SSL Server Test uses TLS_EMPTY_RENEGOTIATION_INFO_SCSV, +an alternative method defined in RFC 5746, +to check this item. +So, I modified the tls library to be aware of this virtual cipher +suite:

+ + + +
Secure RenegotiationSupported

Client-Initiated Renegotiation

+ + + + + + +
Secure Client-Initiated Renegotiation Supported DoS DANGER
Insecure Client-Initiated Renegotiation Supported INSECURE

A typical scenario of renegotiation is as follows: a user is browsing some pages over TLS. +Then the user clicks a page which requires the client certificate in TLS. +In this case, the server sends the TLS HelloRequest to start +renegotiation so that the client can send the client certificate +through the renegotiation phase.

A client can also initiate renegotiation by sending the TLS ClientHello. +But neither secure renegotiation (RFC 5746) nor insecure renegotiation (RFC 5246) +should not be allowed from the client side because of DOS attacks.

I added a new parameter supportedClientInitiatedRenegotiation to +the Supported data type, whose default value is False. +This modification results in:

+ + + + + + +
Secure Client-Initiated Renegotiation No
Insecure Client-Initiated Renegotiation No

Downgrade attack prevention

+ + + +
Downgrade attack prevention No, TLS_FALLBACK_SCSV not supported

Downgrade attack is a bad technique to force a client and a server to +use a lower TLS version even if higher TLS versions are available. +Some clients fall back to a lower TLS version if the negotiation of a higher TLS version fails. +An active attacker can cause network congestion or something to make the negotiation failed.

To prevent this, RFC 7507 defines Fallback Signaling Cipher Suite Value, TLS_FALLBACK_SCSV. +A client includes this virtual cipher suite to the cipher suite proposal +when falling back. +If the corresponding server finds TLS_FALLBACK_SCSV and +higher TLS versions are supported, +the server can reject the negotiation to prevent the downgrade attack.

I implemented this feature and the evaluation results in:

+ + + +
Downgrade attack prevention Yes, TLS_FALLBACK_SCSV supported

For your information, you can test your server with the following commands:

% openssl s_client -connect <ipaddr>:<port> -tls1
+% openssl s_client -connect <ipaddr>:<port> -tls1 -fallback_scsv

Forward Secrecy

+ + + +
Forward Secrecy With some browsers

Forward Secrecy can be achieved with ephemeral Diffie Hellman (DHE) or +ephemeral elliptic curve Diffie Hellman (ECDHE). +Warp TLS version 3.1.1 sets supportedCiphers to:

[ TLSExtra.cipher_ECDHE_RSA_AES128GCM_SHA256
+, TLSExtra.cipher_DHE_RSA_AES128GCM_SHA256
+, TLSExtra.cipher_DHE_RSA_AES256_SHA256
+, TLSExtra.cipher_DHE_RSA_AES128_SHA256
+, TLSExtra.cipher_DHE_RSA_AES256_SHA1
+, TLSExtra.cipher_DHE_RSA_AES128_SHA1
+, TLSExtra.cipher_DHE_DSS_AES128_SHA1
+, TLSExtra.cipher_DHE_DSS_AES256_SHA1
+, TLSExtra.cipher_AES128_SHA1
+, TLSExtra.cipher_AES256_SHA1
+]

This is evaluated as "With some browsers". +SSL Labs: Deploying Forward Secrecy suggests that +TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA and TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA +are missing. +Aaron Friel added the two cipher suites to the tls library and also +added them in Warp TLS:

[ TLSExtra.cipher_ECDHE_RSA_AES128GCM_SHA256
+, TLSExtra.cipher_ECDHE_RSA_AES128CBC_SHA256 -- here
+, TLSExtra.cipher_ECDHE_RSA_AES128CBC_SHA  -- here
+, TLSExtra.cipher_DHE_RSA_AES128GCM_SHA256
+, TLSExtra.cipher_DHE_RSA_AES256_SHA256
+, TLSExtra.cipher_DHE_RSA_AES128_SHA256
+, TLSExtra.cipher_DHE_RSA_AES256_SHA1
+, TLSExtra.cipher_DHE_RSA_AES128_SHA1
+, TLSExtra.cipher_DHE_DSS_AES128_SHA1
+, TLSExtra.cipher_DHE_DSS_AES256_SHA1
+, TLSExtra.cipher_AES128_SHA1
+, TLSExtra.cipher_AES256_SHA1
+]

This configuration is evaluated as "With modern browsers".

+ + + +
Forward Secrecy With modern browsers

Note that the article also suggests TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA. +I know that adding this cipher suite results in "Yes (with most browsers)" +But we don't want to support 3DES.

Session resumption

+ + + +
Session resumption (caching) No (IDs assigned but not accepted)

Session resumption is a mechanism to reduce the overhead of key exchange. +An exchanged key is associated with a session ID and stored in both +the client and the server side. +The next time the client sends the TLS ClientHello message, +the client can specify the session ID previously used. +So, the client and the server are able to reuse the exchanged key.

The tls library supports this mechanism. That's why the result says "IDs assgined". Since Warp TLS does not make use of SessionManager, it also says "but not accepted".

I'm not planning to implement this simple session resumption in Warp TLS since the server would need to have states of exchanged keys. Rather, I would like to implement the stateless TLS session resumption defined in RFC 5077.

Acknowledgment

I would like to thank Kazuho Oku for giving useful information about the secure renegotiation.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/08/thoughts-on-documentation.html b/public/blog/2015/08/thoughts-on-documentation.html new file mode 100644 index 00000000..0009cab4 --- /dev/null +++ b/public/blog/2015/08/thoughts-on-documentation.html @@ -0,0 +1,69 @@ + Some thoughts on documentation +

Some thoughts on documentation

August 28, 2015

GravatarBy Michael Snoyman

I write and maintain a lot documentation, both open source and commercially. +Quite a bit of the documentation I maintain is intended to be collaborative +documentation. Over the past few years, through my own observations and +insights from +others +(yes, this blog post is basically a rip-off of a smaller comment by Greg), I've +come up with a theory on collaborative documentation, and I'm interested in +feedback.

tl;dr: people don't seem to trust Wiki content, nor explore it. They're +also more nervous about editing Wiki content. Files imply: this is officially +part of the project, and people feel comfortable sending a PR

When talking about documentation, there are three groups to consider: the +maintainers, the contributors, and the readers. The most obvious medium for +collaborative documentation is a Wiki. Let's see how each group sees a Wiki:

  • Maintainers believe they're saying "feel free to make any changes you want, +the Wiki is owned by the community." By doing that, they are generally hoping +to greatly increase collaboration.

  • Contributors, however, seem to be intimidated by a Wiki. Most contributors +do not feel completely confident in their ability to add correct content, +adhere to standards, fit into the right outline, etc. So paradoxically, by +making the medium as open as possible, the Wiki discourages contribution.*

  • Readers of documentation greatly appreciate well structured content, and +want to be able to trust the accuracy of the content they're reading. Wikis +do not inspire this confidence. Despite my previous comments about +contributors, readers are (logically) concerned that Wiki content may have been +written by someone uninformed, or may have fallen out of date.

By contrast, let's take a different model for documentation: Markdown files in +a Github repository:

  • Maintainers have it easy: they maintain documentation together with their +code. The documentation can be forked and merged just like the code itself.

  • Contributors- at least in my experience- seem to love this. I've gotten +dozens (maybe even hundreds) of people sending minor to major pull requests +to documentation I maintain on open source projects this way. Examples range +from the simplest (inline API documentation) to the most theoretically most +complex (the content for the Yesod book). Since our target audience is +developers, and developers already know how to send pull requests, this just +feels natural.

  • Readers trust content of the repository itself much more. It's more +official, because it means someone with commit access to the project +agreed that this should belong here.

This discussion came up for me again when I started thinking about writing a +guide for the Haskell tool +stack. I got +halfway through writing this blog post two weeks ago, and decided to finish it +when discussing with other stack maintainers why I decided to make this a file +instead of another Wiki page. Their responses were good confirmation to this +theory:

Emanuel Borsboom:

Ok, that makes a lot of sense to me. We might want to consider moving +reference material to files (for example, the stack.yaml documentation). +Another nice thing about that is that it means the docs follow the versions +(so no more confusion about whether the stack.yaml page is for current master +vs. latest release).

Jason Boyer:

I can confirm this sentiment is buried in me somewhere, I've definitely felt +this way (as a user/developer contributing little bits). On a more technical +note, the workflow with editing the wiki doesn't offer up space for review - +it is a done deal, there is no PR.

While Wikis still have their place (at the very least when collaborating with +non-technical people), I'm quite happy with the +file-as-a-collaborative-document workflow (that- I again admit- Greg introduced +me to). My intended behavior moving forward is:

  • Keep documentation in the same repo as the project
  • Be liberal about who has commit access to repos

* I've seen a similar behavior with code itself: while many people (myself + included in the past) are scared to give too many people commit access to a + repository, my experience (following some advice from Edward Kmett) with giving + access more often rather than less has never led to bad maintainer decisions. + Very few people are actually malicious, and most will be cautious about + breaking a project they love. (Thought experiment: how would you act if you + were suddenly given commit access to a major open source project + (GHC/Linux/etc)? I'm guessing you wouldn't go through making serious + modifications without asking for your work to be reviewed.)

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/09/new-runtime-hamlet-api.html b/public/blog/2015/09/new-runtime-hamlet-api.html new file mode 100644 index 00000000..98de03c2 --- /dev/null +++ b/public/blog/2015/09/new-runtime-hamlet-api.html @@ -0,0 +1,51 @@ + The new runtime Hamlet API +

The new runtime Hamlet API

September 18, 2015

GravatarBy Michael Snoyman

The Hamlet HTML templating system is, by default, parsed at compile time, +allowing it to do quite a bit of static checking. But it can also be useful to +parse these templates at runtime, such as if you wanted to allow dynamic +content to be submitted by users.

A few weeks ago, a discussion on +Reddit +pointed out that the API for runtime Hamlet in shakespeare was pretty bad. Just +a week later, I came up with a use case for runtime Hamlet myself, and decided +to put together a new, more user friendly API.

This new API is provided in +Text.Hamlet.Runtime. +Hopefully it's much easier to follow going on here. And this time, there are +even comments on the functions! I'm including the example from the module +below to give a better feel for how this works:

{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet.Runtime
+import qualified Data.Map as Map
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+main :: IO ()
+main = do
+    template <- parseHamletTemplate defaultHamletSettings $ unlines
+        [ "<p>Hello, #{name}"
+        , "$if hungry"
+        , "  <p>Available food:"
+        , "  <ul>"
+        , "    $forall food <- foods"
+        , "      <li>#{food}"
+        ]
+    let hamletDataMap = Map.fromList
+            [ ("name", "Michael")
+            , ("hungry", toHamletData True) -- always True
+            , ("foods", toHamletData
+                [ "Apples"
+                , "Bananas"
+                , "Carrots"
+                ])
+            ]
+    html <- renderHamletTemplate template hamletDataMap
+    putStrLn $ renderHtml html

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/09/true-root-pvp-debate.html b/public/blog/2015/09/true-root-pvp-debate.html new file mode 100644 index 00000000..bbf6004c --- /dev/null +++ b/public/blog/2015/09/true-root-pvp-debate.html @@ -0,0 +1,69 @@ + The true root of the PVP debate +

The true root of the PVP debate

September 24, 2015

GravatarBy Michael Snoyman

I recently +wrote a new Stack feature and blogged about it. The +feature is about adding support for +PVP bounds in +cabal files. I did my very best to avoid stirring up anything +controversial. But as is usual when the PVP comes up, +a disagreement broke out on Reddit about version bounds. This +essentially comes down to two different ways to view an upper bound on +a package:

  • We've tested, and know for certain that the new version of a +dependency is incompatible with our package
  • I can't guarantee that any new versions of a dependency will be +compatible with our package

If you look through the history of PVP debates, you'll see that this +argument comes up over and over again. I'm going to make a bold +statement: if a core feature to how we do package management is so +easily confused, there's a problem with how we're using the feature. I +made an offhand comment about this on Twitter:

+

Based on the positive feedback to that tweet, I'm moving ahead with +making a more official proposal.

How PVP bounds work today

Here's the theory of how you're supposed to write PVP bounds in a file:

  • Test your package against a range of dependencies. For example, +let's say we tested with text-1.1.2 and text-1.2.0.3
  • Modify the build-depends in your .cabal file to say text >= 1.1.2 +&& < 1.3, based on the fact that it's known to work with at least +version 1.1.2, and unknown to work on anything than major version +1.2.
  • Next time you make a release, go through this whole process again.

PVP detractors will respond with a few points:

  • You don't know that your package won't work with text-1.1.1
  • You don't know that your package won't work with text-1.3
  • You don't know for certain that your package will work with +text-1.1.3, text-1.2.0.0, or text-1.2.1 (yes, it should based on +PVP rules, but mistakes can happen.
  • Repeating this testing/updating process manually each time you make +a code change is tedious and error-prone.

Collect the real information

If you notice, what we did in the above was extract the cabal file's +metadata (version bounds) from what we actually know (known versions +that the package works with). I'm going to propose a change: let's +capture that real information, instead of the proxy data. The data +could go into the cabal file itself, a separate metadata file, or a +completely different database. In fact, the data doesn't even need to +be provided by the package author. Stackage Nightly, for instance, +would be a wonderful source of this information.

A dependency solver - perhaps even cabal-install's - would then be +able to extract exactly the same version bound information we have +today from this data. We could consider it an intersection between the +.cabal file version bounds and the external data. Or, we could ignore +.cabal file bounds and jump straight to the database. Or we could even +get more adventurous, e.g. preferring known-good build plans (based on +build plan history).

In theory, this functionality - if done as a separate database from +the .cabal files themselves - means that on-Hackage revisions would be +much less important, possibly even a feature that no one needs in the +future.

Automation!

And here's the best part: this doesn't require authors to do +anything. We can automate the entire process. There could even be +build servers sitting and churning constantly trying to find +combinations that build together. We've seen already how difficult it +is to get authors to adopt a policy. The best policy is one that can +be automated and machine run.

Downsides

Problems I've thought of so far:

  • Some packages (notably Edward Kmett's) have a versioning scheme +which expresses more information than the PVP itself, and therefore +the generated version bounds from this scheme may be too strict. But +that won't necessarily be a problem, since a build server will be +able to just test new versions as they come out.
  • This very blog post may start a flame war again, which I sincerely +hope doesn't happen.

Adoption

In order for this to really happen, we need:

  1. Broad support for the idea
  2. Changes to the cabal-install dependency solver (or an alternate +replacement)
  3. Central infrasturcture for tracking the build successes
  4. Tooling support for generating the build success information

And to be blunt: this is not a problem that actually affects me right +now, or most people I work with (curation is in a sense a simplified +version of this). If the response is essentially "don't want it," +let's just drop it. But if people relying on version bounds today +think that this may be a good path forward, let's pursue it.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/10/beginner-friendly-code-and-apis.html b/public/blog/2015/10/beginner-friendly-code-and-apis.html new file mode 100644 index 00000000..30fbc1b9 --- /dev/null +++ b/public/blog/2015/10/beginner-friendly-code-and-apis.html @@ -0,0 +1,80 @@ + Beginner friendly code and APIs +

Beginner friendly code and APIs

October 30, 2015

GravatarBy Michael Snoyman

I just flew from Tel Aviv to Los Angeles. It's a long flight, and includes +significant travel time outside of the flight itself. There are two results of +that: (1) I've slept something like 8 hours in the past 72, and (2) I've had a +lot of time to play with random code sitting on my laptop and think about it +from different perspectives. I'm going to talk about (2), and use (1) as an +excuse for anything ridiculous I say. Note that this blog post is just +exploring ideas, not giving any real statements.

The following two pieces of code are identical in functionality:

someFunc1 key =
+    case lookup key someMap of
+        Nothing -> getDefaultValue
+        Just value -> otherFunc value
+
+someFunc2 = maybe getDefaultValue otherFunc . flip lookup someMap

Which is better? I'll start with objective facts from my own experience:

  1. When I was a beginner Haskeller, I would have been able to write something +like someFunc1
  2. When I was a beginner Haskeller, I would have had to try and puzzle out +someFunc2` for a long time if I read it, and almost certainly couldn't have +written it
  3. Past the beginner phase, reading code like someFunc2 taught me new ways to +compose Haskell code easily
  4. As an intermediate Haskeller, I definitely got the feeling that I should be +writing my code in someFunc2 style (point-free, avoid pattern matching, +use function composition)
  5. I seem to receive pull requests on a regular (though not frequent) basis +rewriting code in the style of someFunc1 to look like someFunc2
  6. I have a gut reaction that someFunc2 is better
  7. someFunc2 is shorter
  8. And yet, to this date, I think I can still parse and understand someFunc1 +more easily, and also believe I will more quickly spot a bug in that style

There's quite a few "I got the feeling" statements above. Those are objective +statements about my subjective observations; I'd be really intersted in +whether other people have had the same kinds of observations over time, or if +my experience is unique. But now, the truly subjective/exploratory part:

It seems like my progression as a Haskeller results in forcing myself to write +in a harder-to-parse style to make my code shorter, to satisfy some base need +for "better" code, even though by most measurements I just made, the +longer/explicit/pattern matching code is in fact better.

There are certainly advantages to point-free style though, right? Some more +complicated combinators - like foldr - result in clearer code, plus code that +generalizes to more data types than just a list (thanks to Foldable). In some +cases (like stream fusion and other rewrite rules) point-free style may be more +efficient; I know that in conduit await >>= maybe f g is more efficient than +pattern matching.

To try and generalize my point beyond point-free style: we've been having some +discussions about more code sharing in the web framework space recently. One +point that's come up is a difference between Servant and Yesod regarding +explicitness of parameters. For example, in Yesod (and I think other WAI +frameworks), there's a function available inside your handler code to lookup +request headers. In Servant, a dependency on such a piece of data is explicit +at the type level. (There are similar issues around short-circuiting requests, +and explicitness of different content representations.)

My 5-year-more-Haskell-experienced self is very much intrigued by the Servant +approach. It seems more sound. And yet, I still interact regularly with less +experienced Haskellers. And just this past week, this kind of need for explicit +declaration of all data came up in practice for a customer, and resulted in +Haskell being a harder adoption for them.

I'm feeling the same tension again. The Yesod API seems to appeal to what the +beginner would expect at a straightforward level: if you want to send a +redirect, you just call the redirect function, and don't need to worry about +changing type signatures around. Need a request header? Ask for it. However, I +know that being explicit about these things gives great benefit (in Servant's +case, it has the ability to generate API bindings that Yesod doesn't have). But +even so, today when I write a web app in Haskell, I still like the ability to +use the "beginner-friendly" API without the explicitness.

This is where I'm stuck. There are clearly benefits to each kind of approach in +these dichotomies. And I'm sure there are more dichotomies than I've listed. +The kind of questions I've been pondering are:

  1. If I would design an API differently today than in the past, based on my +larger Haskell experience, what does that mean? Is the new API better? Or +have I lost touch with what's most useful, and am instead chasing an less +important ideal?
  2. Is there an inherent tension between beginner-friendly and expert-friendly +APIs for some of these things, and are the two groups best served by +separating out the APIs?
  3. When do I give in to the voice that says "you could rewrite that to be +cleaner" and when do I say "no, the simple code is better, don't complicate +it in the name of cleanliness."

Sorry for the mostly unintelligible rambling. In the interest of sometimes +listening to the less experienced person inside of me, I'm posting this without +much review, in case I'm actually hitting on an important topic. If not, please +ignore me.

And in case anyone's curious, a lot of this came up when working on +wai-frontend-monad, +which attempts to extract the HandlerT transformer from Yesod into something +more generally useful, and in a more modern coding style. That work (combined +with lack of sleep on a plane) seems to have opened up some existential +dilemmas ;).

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/10/resurrecting-servius.html b/public/blog/2015/10/resurrecting-servius.html new file mode 100644 index 00000000..44a2c8ac --- /dev/null +++ b/public/blog/2015/10/resurrecting-servius.html @@ -0,0 +1,30 @@ + Resurrecting servius +

Resurrecting servius

October 25, 2015

GravatarBy Michael Snoyman

A while ago, I wrote a small package called +servius, a simple executable that +serves static files with Warp, and will additionally render Hamlet and Lucius +templates. In some earlier package consolidation, the tool became part of +shakespeare, and eventually was commented out (due to concerns around the +dependency list on Hackage looking too big).

Today, I just resurrected this package, and added support for rendering +Markdown files as well. I often times end up working on Markdown files (such as +for this blog, the Haskell Documentation +project, and the FP +Complete blog), and being able to easily view the files in a browser is useful.

As it stands, the three specially-handled file types of Hamlet (.hamlet), +Lucius (.lucius), and Markdown (.markdown and .md). If others wish to add more +templating or markup languages to this list, I'm more than happy to access +pull requests.

Final note: this package is currently uploaded using the pvp-bounds feature of +Stack, so don't be +surprised when the version bounds on Hackage are more restrictive than those in +the repo itself.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/10/using-wais-vault.html b/public/blog/2015/10/using-wais-vault.html new file mode 100644 index 00000000..5f9561e0 --- /dev/null +++ b/public/blog/2015/10/using-wais-vault.html @@ -0,0 +1,149 @@ + Using WAI's vault for fun and profit +

Using WAI's vault for fun and profit

October 20, 2015

GravatarBy Michael Snoyman

There is a not-often-used part of the WAI Request value - the vault field - +which provides a means of extension, especially for WAI middlewares. Getting +all of the pieces to fit together exactly right isn't always obvious. This blog +post is intended to provide some quick examples of how to use it, and how a web +framework (such as Yesod) can interact with such values.

What is a vault?

The vault field is a value from the vault +package. A Vault is essentially a +Map that can hold heterogenous data, and whose lookup keys are well typed. +We'll focus on just the IO-specified version of the interface, but there's +also an ST interface.

Example 1: safe key generation, WAI application

The big trick with vault for our purposes is that the keys are fully opaque, +and must be generated in the IO monad. Our first example program will have a +middleware that accepts a Key as a parameter and sets a value in the Vault, +and an Application that also accepts that Key as a parameter and uses it.

#!/usr/bin/env stack
+-- stack --resolver lts-3.9 runghc --package warp --package random
+
+import qualified Data.Vault.Lazy as V
+import Network.Wai
+import Network.Wai.Handler.Warp
+import Network.HTTP.Types
+import System.Random
+import qualified Data.ByteString.Lazy.Char8 as L8
+
+middleware :: V.Key Int -> Middleware
+middleware key app req respond = do
+    -- Generate a random number
+    value <- randomIO
+    let vault' = V.insert key value (vault req)
+        req' = req { vault = vault' }
+    app req' respond
+
+app :: V.Key Int -> Application
+app key req respond =
+    respond $ responseLBS status200 [] $ L8.pack str
+  where
+    str =
+        case V.lookup key (vault req) of
+            Nothing -> "Key not found"
+            Just value -> "Random number is: " ++ show value
+
+main :: IO ()
+main = do
+    key <- V.newKey
+    run 3000 $ middleware key $ app key

Example 2: unsafe key generation, WAI application

In practice, while generating the key like this works, it can be annoying to +have to pass it into your middlewares and applications. Instead, you can use +unsafePerformIO when defining your middleware to avoid the problem. This is a +safe usage, though you need to make sure to use the NOINLINE pragma to +ensure that the computation is only run once.

#!/usr/bin/env stack
+-- stack --resolver lts-3.9 runghc --package warp --package random
+
+import qualified Data.Vault.Lazy as V
+import Network.Wai
+import Network.Wai.Handler.Warp
+import Network.HTTP.Types
+import System.Random
+import qualified Data.ByteString.Lazy.Char8 as L8
+import System.IO.Unsafe
+
+randomKey :: V.Key Int
+randomKey = unsafePerformIO V.newKey
+{-# NOINLINE randomKey #-}
+
+middleware :: Middleware
+middleware app req respond = do
+    -- Generate a random number
+    value <- randomIO
+    let vault' = V.insert randomKey value (vault req)
+        req' = req { vault = vault' }
+    app req' respond
+
+app :: Application
+app req respond =
+    respond $ responseLBS status200 [] $ L8.pack str
+  where
+    str =
+        case V.lookup randomKey (vault req) of
+            Nothing -> "Key not found"
+            Just value -> "Random number is: " ++ show value
+
+main :: IO ()
+main = run 3000 $ middleware app

Example 3: safe key generation, Yesod application

Adding this same approach to a Yesod application is fairly straight-forward, the only two tricks are:

  • Put the generated key into the foundation data type
  • Use the waiRequest function to get access to the raw request value

A simple working exactly:

#!/usr/bin/env stack
+-- stack --resolver lts-3.9 runghc --package yesod-core
+
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+
+import qualified Data.Vault.Lazy as V
+import Network.Wai
+import Network.Wai.Handler.Warp
+import Network.HTTP.Types
+import System.Random
+import qualified Data.ByteString.Lazy.Char8 as L8
+import Yesod.Core
+
+middleware :: V.Key Int -> Middleware
+middleware key app req respond = do
+    -- Generate a random number
+    value <- randomIO
+    let vault' = V.insert key value (vault req)
+        req' = req { vault = vault' }
+    app req' respond
+
+app :: V.Key Int -> Application
+app key req respond =
+    respond $ responseLBS status200 [] $ L8.pack str
+  where
+    str =
+        case V.lookup key (vault req) of
+            Nothing -> "Key not found"
+            Just value -> "Random number is: " ++ show value
+
+data App = App
+    { randomKey :: V.Key Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    App key <- getYesod
+    req <- waiRequest
+    defaultLayout [whamlet|#{show $ V.lookup key (vault req)}|]
+
+main :: IO ()
+main = do
+    key <- V.newKey
+    app <- toWaiApp $ App key
+    run 3000 $ middleware key app

Exercise: unsafe key generation, Yesod application

The last example in our matrix would be using unsafePerformIO for generating +the key, and using it in a Yesod application. That should be a straightforward +modification of what we've already seen, and is left as an exercise to the +reader.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2015/12/yesod-hosting-docker-kubernetes.html b/public/blog/2015/12/yesod-hosting-docker-kubernetes.html new file mode 100644 index 00000000..30a41b1b --- /dev/null +++ b/public/blog/2015/12/yesod-hosting-docker-kubernetes.html @@ -0,0 +1,171 @@ + Yesod hosting with Docker and Kubernetes +

Yesod hosting with Docker and Kubernetes

December 14, 2015

GravatarBy Michael Snoyman

N.B. The stack image command referenced in this blog post has been removed. Please see Building Haskell Apps with Docker for a more up-to-date guide.

About a month ago, there were a few days of instability for yesodweb.com +hosting. The reason for this was that I was in the midst of moving hosting of +yesodweb.com (and a few other sites) to new hosting. I went through a few +iterations, and wanted to mention how the hosting now works, what I like, and +some pain points to notice.

The end result of this is a +Docker/Kubernetes +deployment, consisting of a single Docker image containing various sites (six +currently), and an extra application that reverse proxies to the appropriate +one based on virtual host. But let's step through it bit by bit.

Stack's Docker support

The Stack build tool supports using Docker in two +different ways, both of which are leveraged by this setup.

  1. Using a Docker build image to provide build tools (like GHC), system +libraries, and optionally Haskell libraries, and performing the build within +such a container. This isolates your build from most host-specific +configurations, and grants you immediate access to many tools (like PostgreSQL +client libraries) without modifying your host.
  2. Generate a Docker image based on a base image that includes necessary system +libraries, and includes generated executables and any additional files +requested (such as configuration files and static resources like CSS and +Javascript).

What's really nice about this setup vs a more standard Docker image generation +approach is that our generated runtime image (from (2)) does not include any +build-specific tools. This makes our images lighter-weight, and avoids having +unnecessary code in production (which is good from a security standpoint).

What's really nice about all of this is how simple the Stack configuration is +to make it happen. Consider the following stack.yaml file:

# Every Stack.yaml needs to specify its resolver
+resolver: lts-3.14
+
+# Build using Docker. Will use the default stack-build image, which
+# includes build tools but not precompiled libraries.
+docker:
+  enable: true
+
+# Generate a Docker image
+image:
+  container:
+    # New image will be called snoyberg/yesodweb
+    name: snoyberg/yesodweb
+    # Base it on the stack-run images
+    base: fpco/stack-run
+    # Include some config and static files in the image
+    add:
+      config: /app/config
+      static: /app/static

With this in place, running stack image container will generate the +snoyberg/yesodweb image, which I can then push to whatever Docker registry I +want using normal Docker commands.

For more information on Stack's Docker support, see the Docker integration +page.

Mega-repo

I initially deployed each of my sites as a separate deployment. However, for +various resource-related reasons (disk space, number of machines), I decided to +try out deploying the six sites as a single deployment. I'm not convinced yet +that this is a great idea, but it's certainly working in practice. The result +is my snoyman-webapps repo. As a +short snippet:

apps:
+- vhost: www.yesodweb.com
+  dir: /app/yesodweb.com
+  exe: yesodweb
+  args:
+  - production
+- vhost: www.haskellers.com
+  dir: /app/haskellers
+  exe: haskellers
+  args:
+  - production
+
+redirects:
+- src: yesodweb.com
+  dst: www.yesodweb.com
+- src: yesodweb.org
+  dst: www.yesodweb.com

This file has a few important things to note:

Let's jump into that last one right away.

Reverse proxying

Probably the most instructive file on this program is the +webapps.yaml +config file. This shows that the web app is capable of:

  • Running child applications (the six sites I mentioned)
  • Reverse proxying to the appropriate applications
  • Performing simple redirects between domain names

In theory this code could be turned into something standalone, but for now it's +really custom-tailored to my needs here.

The biggest downside with this approach is that (without Server Name +Indication, or SNI) it +doesn't support TLS connections. I chose sites for this that are not served +over TLS currently, and do not handle sensitive information (e.g., no password +collection). Upgrading to have SNI support and using something like Let's +Encrypt would be a fun upgrade in the future.

Kubernetes configuration

Kubernetes uses YAML files for configuration. I'm not going to jump into the +syntax of the config files or the overarching model Kubernetes uses for running +your applications. If you're unfamiliar and interested, I recommended reading +the Kubernetes docs.

Secrets

Three of the sites I'm hosting have databases, and therefore the database +credentials need to be securely provided to the apps during deployment. +Kubernetes provides a nice mechanism for this: secrets. You specify some +(base64-encoded) content in a YAML file, and then you can mount a virtual +filesystem for your apps to access the data from. Let's have a look at the +haskellers.com (scrubbed) secrets file:

apiVersion: v1
+kind: Secret
+metadata:
+    name: haskellers-postgresql-secret
+data:
+    # https://github.com/snoyberg/haskellers/blob/master/config/db/postgresql.yml.example
+    postgresql.yml: base64-yaml-contents
+    # https://github.com/snoyberg/haskellers/blob/master/config/db/aws.example
+    aws: base64-yaml-contents
+    client-session-key.aes: base64-version-of-client-session-key

I have a separate secrets config for each site. The decision was made mostly +for historical reasons, since the sites were originally hosted separately. I +still like to keep them separate though, since it's easy to put the secrets +into different subdirectories, as we'll see next.

Replication controller

There's a lot of content in the replication controller +config. +I'll strip it down just a bit:

apiVersion: v1
+kind: ReplicationController
+metadata:
+    name: snoyman-webapps
+    labels:
+        name: snoyman-webapps
+spec:
+  replicas: 1
+  selector:
+    name: snoyman-webapps
+  template:
+    metadata:
+        labels:
+            name: snoyman-webapps
+    spec:
+        volumes:
+        - name: haskellers-postgresql-volume
+          secret:
+              secretName: haskellers-postgresql-secret
+        containers:
+        - name: webapps
+          image: snoyberg/snoyman-webapps:latest
+          command: ["webapps"]
+          workingDir: /app/webapps
+          ports:
+          - name: webapps
+            containerPort: 3000
+          env:
+          - name: PORT
+            value: "3000"
+          volumeMounts:
+          - name: haskellers-postgresql-volume
+            readOnly: true
+            mountPath: /app/haskellers/config/db

The interesting stuff:

  • We mount the haskellers.com secret at /app/haskellers/config/db, where the +app itself expects it
  • The webapps app needs to know what port to listen on, so we tell it via the +PORT environment variable
  • We also tell Kubernetes that the application is listening on port 3000
  • The internally listened on ports for each application are irrelevant to us: +the webapps app handles though for us automatically

Service (load balancer)

The load balancing service is quite short and idiomatic:

apiVersion: v1
+kind: Service
+metadata:
+  name: snoyman-webapps
+  labels:
+    name: snoyman-webapps
+spec:
+  type: LoadBalancer
+  ports:
+  - name: http
+    port: 80
+    targetPort: webapps
+  selector:
+    name: snoyman-webapps

Updates

When it comes time to make updates to one the these sites, I do the following:

  • Change that site's repo and commit
  • Update the submodule reference for snoyman-webapps
  • Run stack image container && docker push snoyberg/snoyman-webapps
  • Perform a rolling update with Kubernetes: kubectl rolling-update snoyman-webapps --image=snoyberg/snoyman-webapps:latest

Some notes:

  • The rolling-update is lack-luster in Kubernetes; it can fail to work for a +variety of reason I have yet to fully understand. My biggest advice: when +possible, create single-container replication controllers.
  • I always just push and use the latest image. For more control/reliability, I recommend tagging Docker images with the Git SHA1 of the commit being built from. I'm lazy for these sites, but for client deployments at FP Complete, we always follow this practice.

Google Container Engine

I started off this whole project as a way to evaluate Kubernetes. Based on +that, I started hosting this on Google Container Engine instead of fiddling +with configuring AWS to host Kubernetes myself. Overall, I'm happy with how it +turned out. I had a few ugly issues

  • Running out of disk space due to the large number of Docker images (likely the primary motivation to moving towards a single Docker image for all the sites).
  • All my sites went down one day when my account switched over from the trial to non-trial. I don't remember getting an email warning me about this, which would have been nice.

A n1-standard-1 instance size has been plenty to support all of these sites, +which is nice (yay lightweight Haskell/Yesod!). That said, at FP Complete, we +host our stuff on AWS, and have had pretty good experience with running +Kubernetes there.

Conclusion

Overall, I find the Docker/Kubernetes deployment workflow quite pleasant to +work with. I may find more hiccups over time, but for now, I'd strongly +recommend people consider it for deployments of their own, especially if you're +using tooling like Stack that makes it so easy to create Docker images.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/01/auto-generate-docbook-xml.html b/public/blog/2016/01/auto-generate-docbook-xml.html new file mode 100644 index 00000000..6b0fa1a1 --- /dev/null +++ b/public/blog/2016/01/auto-generate-docbook-xml.html @@ -0,0 +1,121 @@ + Auto generate Docbook XML for the Yesod book +

Auto generate Docbook XML for the Yesod book

January 25, 2016

GravatarBy Michael Snoyman

I just made a minor tweak to the yesodweb.com-content +repository, which contains +all of the content that gets displayed on this site, including this blog post +and the entire Yesod book. This repo contains both the original asciidoc +version of the content, as well as the XML docbook files that are generated +from it. Though that's a slightly contentious decision, storing it this way +avoids having to have the asciidoc tools installed on the server hosting the +website.

Until now, I've had to manually generate the XML from time to time after making +updates or merging pull requests. However, doing this kind of manual +maintenance is annoying. Much better to use automated systems. And whenever +possible, I like to go for Travis CI. The setup is pretty simple, but involves +some finer points that I thought may be interesting to others (not to mention +good documentation for myself in the future).

Encrypted deployment key

In order to push from Travis back to Github, we need to have an SSH key. Travis +allows us to put encrypted content into the repo, which anyone can see +themselves. +Within Travis, this file can be decrypted and then placed in the ~/.ssh +directory with the following commands:

mkdir -p $HOME/.ssh
+openssl aes-256-cbc -K $encrypted_92ac0cbbb1f3_key -iv $encrypted_92ac0cbbb1f3_iv -in id_rsa.enc -out id_rsa -d
+mv id_rsa $HOME/.ssh
+chmod 400 $HOME/.ssh/id_rsa

If you want to create your own encrypted files for Travis, you'll want to use +the Travis command line interface's +encrypt-file command.

Get necessary prerequisites installed

In order to build the XML files, we need three tools: asciidoc, GHC, and Stack. +The first two can be installed via apt, but until +travis-ci/apt-source-whitelist#7 +is merged, we need to install Stack more manually. This is all still pretty +easy to get set up:

addons:
+  apt:
+    packages:
+    - asciidoc
+    - ghc-7.10.3
+    sources:
+    - hvr-ghc
+
+install:
+- export PATH=$HOME/.local/bin:/opt/ghc/7.10.3/bin:$PATH
+- mkdir -p $HOME/.local/bin
+- curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'

Generate the XML

This part's easy: we wipe out the original XML files and run the generate.sh +script:

- rm -f book/generated-xml/*
+- book/tools/generate.sh
+- git diff

We run git diff at the end to provide some useful feedback during PRs of the +resulting XML difference.

Commit and push

This is where the magic happens. Let's look at the code (a bash script inlined in the YAML for Travis):

- |
+  if [ $TRAVIS_PULL_REQUEST != false ]
+  then
+    echo Not pushing diff for a pull request
+  elif [ -n "$(git status --porcelain)" ]
+  then
+    mkdir -p $HOME/.ssh
+    openssl aes-256-cbc -K $encrypted_92ac0cbbb1f3_key -iv $encrypted_92ac0cbbb1f3_iv -in id_rsa.enc -out id_rsa -d
+    mv id_rsa $HOME/.ssh
+    chmod 400 $HOME/.ssh/id_rsa
+    git config --global user.email "michael+travis@snoyman.com"
+    git config --global user.name "Travis job for yesodweb/yesodweb.com-content"
+    git add -A
+    git commit -m "Travis auto-generate XML files, $(date --utc --iso=sec)"
+    git push git@github.com:yesodweb/yesodweb.com-content.git HEAD:$TRAVIS_BRANCH
+  else
+    echo No changes present
+  fi

If we're looking at a pull request, we don't want to ever push to the branch. +(Travis will also prevent us from making a silly mistake here, since the +decryption key we need to get the SSH key won't be available.) We also check if +there are any changes to the XML files. But in the case of a non-PR build that +has changes, we:

  1. Set up the SSH key, as we described above
  2. Commit all changes locally
  3. Push the changes to the current branch (1$TRAVIS_BRANCH`)

Full file

You can look at the current version of the .travis.yml on +Github. +Here's the content at the time of writing:

language: c
+sudo: false
+
+cache:
+  directories:
+  - $HOME/.stack
+
+addons:
+  apt:
+    packages:
+    - asciidoc
+    - ghc-7.10.3
+    sources:
+    - hvr-ghc
+
+install:
+- export PATH=$HOME/.local/bin:/opt/ghc/7.10.3/bin:$PATH
+- mkdir -p $HOME/.local/bin
+- curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
+
+script:
+- rm -f book/generated-xml/*
+- book/tools/generate.sh
+- git diff
+- |
+  if [ $TRAVIS_PULL_REQUEST != false ]
+  then
+    echo Not pushing diff for a pull request
+  elif [ -n "$(git status --porcelain)" ]
+  then
+    mkdir -p $HOME/.ssh
+    openssl aes-256-cbc -K $encrypted_92ac0cbbb1f3_key -iv $encrypted_92ac0cbbb1f3_iv -in id_rsa.enc -out id_rsa -d
+    mv id_rsa $HOME/.ssh
+    chmod 400 $HOME/.ssh/id_rsa
+    git config --global user.email "michael+travis@snoyman.com"
+    git config --global user.name "Travis job for yesodweb/yesodweb.com-content"
+    git add -A
+    git commit -m "Travis auto-generate XML files, $(date --utc --iso=sec)"
+    git push git@github.com:yesodweb/yesodweb.com-content.git HEAD:$TRAVIS_BRANCH
+  else
+    echo No changes present
+  fi

There's nothing particularly complicated or earth-shattering about this +approach, but hopefully putting it all together like this can help others +implement this themselves more easily.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/02/first-class-stream-fusion.html b/public/blog/2016/02/first-class-stream-fusion.html new file mode 100644 index 00000000..e0ea2f88 --- /dev/null +++ b/public/blog/2016/02/first-class-stream-fusion.html @@ -0,0 +1,152 @@ + First class stream fusion +

First class stream fusion

February 28, 2016

GravatarBy Michael Snoyman

This is a blog post about a little thought +experiment I've been playing with +recently. The idea has been bouncing around in my head for a few years now, but +some recent discussions with coworkers at FP Complete (particularly Chris Done +and Francesco Mazzoli) made me spend a few hours on this, and I thought it +would be worth sharing for some feedback.

Premise

The basic premise is this: we typically follow a philosophy in many common +libraries that there's the nice abstraction layer that we want to present to +users, and then the low-level approach under the surface that the user should +never know about but makes everything fast. This is generally a concept of +fusion and rewrite rules, and appears in things like build/foldr fusion in +base, and stream fusion in vector (and more recently, in +conduit).

Here are the ideas fueling this thought experiment:

  • Making our code only fast when GHC rewrite rules fire correctly leads to +unreliable speedups. (Check the benchmarks on the example +repo, which +show conduit slowing down despite its implementation of stream fusion.) This is +a very difficult situation to solve as a library user.

  • By hiding the real implementation away under a nice abstraction, library +users do not necessarily have any understanding of what kinds of code will be +fast and what will be slow. This is not quite as frustrating as the previous +point, but still quite surprising.

  • On the flip side, the high level abstractions generally allow for more +flexible code to be written than the lower level approach may allow.

  • Is there a way to make the low-level, fast approach the primary interface +that the user sees, lose a minimal amount of functionality, and perhaps +regain that functionality by making the more featureful abstraction available +via explicit opt-in?

  • Perhaps we can get better category laws out of a different formulation of a +streaming library (like pipes has), but still hold onto extra functionality +(like conduit has).

If that was too abstract, don't worry about it. Keep reading, and you'll see +where these ideas led me.

Standard stream fusion

Duncan Coutts, Roman Leshchinskiy and Don Stewart introduced a concept called stream fusion, which powers the awesome speed and minimal memory usage of the vector package for many common cases. The idea is:

  • We have a stream abstraction which can be aggressively optimized by GHC (details unimportant for understanding this post)

  • Represent vector operations as stream operations, wrapped by functions that convert to and from vectors. For example:

    mapVector f = streamToVector . mapStream f . vectorToStream
  • Use GHC rewrite rules to remove conversions back and forth between vectors and streams, e.g.:

    mapVector f . mapVector g
    +    = streamToVector . mapStream f
    +                     . vectorToStream . streamToVector
    +                     . mapStream g . vectorToStream
    +      -- Apply rule: vectorToStream . streamToVector = id
    +    = streamToVector . mapStream f
    +                     . id
    +                     . mapStream g . vectorToStream
    +    = streamToVector . mapStream f . mapStream g . vectorToStream

In practice, this can allow long chains of vector operation applications to +ultimately rewrite away any trace of the vector, run in constant space, and get +compiled down to a tight inner loop to boot. Yay!

User facing stream fusion

However, there's an underlying, unstated assumption that goes along with all of +this: users would rather look at vector functions instead of stream functions, +and therefore we should rely on GHC rewrite rules to hide the "complicated" +stream stuff. (Note: I'm simplifying a lot here, there are other reasons to +like having a Vector-oriented interface for users. We'll touch on that later.)

But let's look at this concretely with some type signatures. First, our main Stream datatype:

data Stream o m r

This type produces a stream of o values, runs in the m monad, and +ultimately ends with a value of r. The r type parameter is in practice most +useful so that we can get Functor/Applicative/Monad instance of our type, +but for our purposes today we can assume it will always be (). And m allows +us more flexibility for optimizing things like mapM, but if you treat it as +Identity we have no effects going on. Said another way: Stream o Identity +() is more or less identical to [o] or Vector o.

How about common functions? Well, since this is just a thought experiment, I +only implemented a few. Consider:

enumFromToS :: (Ord o, Monad m, Num o) => o -> o -> Stream o m ()
+
+mapS :: Functor m => (i -> o) -> Stream i m r -> Stream o m r
+
+foldlS :: (Monad m) => (r -> i -> r) -> r -> Stream i m () -> m r
+
+-- Yes, we can build up more specific functions
+sumS :: (Num i, Monad m) => Stream i m () -> m i
+sumS = foldlS (+) 0

If you ignore the m and r type parameters, these functions look identical +to their list and vector counterparts. As opposed to lists and vectors, though, +we know for a fact that these functions will never end up creating a list of +values in memory, since no such capability exists for a Stream. Take, for +example, the typical bad implementation of average for lists:

average :: [Double] -> Double
+average list = sum list / length list

This is problematic, since it traverses the entire list twice, being both CPU +inefficient and possibly forcing a large list to remain resident in memory. +This mistake cannot be made naively with the stream implementation. Instead, +you're forced to write it the efficient way, avoiding confusion down the road:

averageS :: (Fractional i, Monad m) => Stream i m () -> m i
+averageS =
+    fmap (\(total, count) -> total / count) . foldlS go (0, 0)
+  where
+    go (!total, !count) i = (total + i, count + 1)

Of course, this is also a downside: when you're trying to do something simple +without worrying about efficiency, being forced to deal with the lower-level +abstraction can be an annoyance. That's one major question of this thought +experiment: which world is the better one to live in?

Capturing complex patterns

Coroutine-based streaming libraries like conduit and pipes provide for the +ability for some really complex flows of control without breaking +composability. For example, in conduit, you can use ZipSink to feed two +consumers of data in parallel and then use standard Applicative notation to +combine the result values. You can also monadically compose multiple +transformers of a data stream together and pass unconsumed data from one to the +other (leftovers). Without some significant additions to our stream layer +(which would likely harm performance), we can't do any of that.

Interestingly, all of the "cool" stuff you want to do in conduit happens before +you connect a component to its upstream or downstream neighbors. For example, +let's say I have two functions for parsing different parts of a data file:

parseHeader :: Monad m => Sink ByteString m Header
+
+parseBody :: Monad m => Sink ByteString m Body

I can compose these together monadically (or applicatively in this case) like +so:

parseHeaderAndBody :: Monad m => Sink ByteString m (Header, Body)
+parseHeaderAndBody = (,) <$> parseHeader <*> parseBody

So what if we had a conversion function that takes a coroutine-based +abstraction and converted it into our streaming abstraction? We don't expect to +have the same level of performance as a hand-written streaming abstraction, but +can we at least get composability? Thankfully, the answer is yes. The +Gotenks +module +implements a conduit-like library*. This library follows all of the common +patterns: await, yield, and leftover functions, monadic composition, and +could be extended with other conduit features like ZipSink.

* Unlike conduit, Gotenks does not provide finalizers. They complicate +things for a small example like this, and after a lot of thought over the +years, I think it's the one extra feature in conduit vs pipes that we could +most do without.

One thing notably missing, though, is any kind of operator like =$=, $$, or +(from pipes) >-> or <-<, which allows us to connect an upstream and +downstream component together. The reason is that, instead, we have three +functions to convert to our streaming abstraction:

toSource :: Applicative m => Gotenks () o m r -> Stream o m r
+toTransform :: Applicative m => Gotenks i o m r -> Stream i m () -> Stream o m r
+toSink :: Monad m => Gotenks i Void m r -> Stream i m () -> m r

And then, we're able to use standard function applications - just like in the +streaming layer - to stick our components together. For example, take this +snippet from the benchmark:

[ bench' "vegito" $ \x ->
+          runIdentity
+        $ sumS
+        $ mapS (+ 1)
+        $ mapS (* 2)
+        $ enumFromToS 1 x
+, bench' "gotenks" $ \x ->
+          runIdentity
+        $ toSink sumG
+        $ toTransform (mapG (+ 1))
+        $ toTransform (mapG (* 2))
+        $ toSource (enumFromToG 1 x)

The obvious benefit here is that our coroutine-based layer is fully compatible +with our stream-based layer, making for easy interop/composition. But in +addition:

  • We now get to trivially prove the category laws, since we're just using +function composition! This is more important than it may at first seem. To my +knowledge, this is the first time we've ever gotten a streaming implementation +that has baked-in leftover support and full category laws, including left +identity. The reason this works is because we now have an explicit conversion +step where we "throw away" leftovers, which doesn't exist in conduit.
  • In case you were worried: the Gotenks layer is implemented as a functor +combined with the codensity transform, guaranteeing trivially that we're also +obeying the monad laws. So without breaking a sweat, we've now got a great +law-abiding system. (Also, we get efficient right-association of monadic bind.)
  • While the coroutine-based code will by nature be slower, the rest of our +pipeline can remain fast by sticking to streams.

What's next?

Honestly, I have no idea what's next. I wanted to see if I could write a +streaming implementation that was guaranteed fast, provided interop with +conduit-style workflows, and would be relatively easy to teach. With the +exception of the two extra type parameters possibly causing confusion, I think +everything else is true. As far as where this goes next, I'm very much open to +feedback.

UPDATE Benchmark results

Don Stewart asked me on Twitter to share the benchmark results for this +repo. They're not particularly +enlightening, which is why I didn't include them initially. Nonetheless, +putting them here makes it clear what I'm getting at: vegito, vector, and +conduit (when stream fusion kicks in) are all the same speed. In fact, the more +interesting thing is to look at their compiled core, which is identical. The +annoyance is that, while Data.Conduit.List and Data.Conduit.Combinators +both fire their rewrite rules, the combinators provided by the Conduit module +do not fire, leading to a significant (read: 200-fold) slowdown. This +slowdown is exacerbated by the choice of benchmark, which is intended to +demonstrate the specific power of the stream fusion optimizations.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/03/why-i-prefer-typeclass-based-libraries.html b/public/blog/2016/03/why-i-prefer-typeclass-based-libraries.html new file mode 100644 index 00000000..0d669566 --- /dev/null +++ b/public/blog/2016/03/why-i-prefer-typeclass-based-libraries.html @@ -0,0 +1,158 @@ + Why I prefer typeclass-based libraries +

Why I prefer typeclass-based libraries

March 22, 2016

GravatarBy Michael Snoyman

This blog post came out of some discussions I had with my coworkers at FP +Complete. I realized I have a number of reasons I prefer working with a +typeclass based libraries (such as classy-prelude) that I've never put down +in writing anywhere. I decided to write up this blog post to collect all of my +thoughts in one place.

This same FP Complete discussion is also what fueled my Tweet on the subject:

+

Common practice

This is hardly a strong argument, but I thought I'd get this out of the way +first. It's common practice in many other languages to program to abstract +interfaces instead of concrete types. Java is likely the best example of this, +though Python's duck-typing, Go's interfaces, and C++ templates are good +examples too. In most language ecosystems, a culture of programming against +something abstract took over a while ago.

Based on the fact that I'm a Haskell developer, I obviously don't put too much +faith in the best practices of Java, Python, Go, and C++. Nonetheless, the fact +that this pattern is repeated in many places, it's worth taking the experiences +of others into account when making choices for ourselves.

Also, choosing a programming paradigm already familiar to people from other +languages can help with language adoption. This isn't just idle speculation: I +have heard new Haskellers express confusion about the lack of abstract +interfaces for some common datatypes.

Uniform interfaces

One of the nicest things about writing code against well-designed libraries is +that the ideas, intuitions, and names translate between them. For example, +whether I'm working with a list, Vector, Seq, or ByteString, I know that +I can use foldr over the contents of the container. I don't have to wonder if +the author of the library decided to call the function reduce instead, or +changed the order of the arguments to the foldr function. My gut instincts +about this function carries through.

Unfortunately, that statement does not work universally. I chose foldr as an +example of a good citizen, but there are many other functions which are not so +uniform:

  • The mapM_ function is not provided by the Data.ByteString module. (I +actually opened a PR about +it.) Sure, I can get the same +functionality via foldr, but it's extra cognitive overhead for the author +versus a simple mapM_ call, and extra overhead for the reader to understand +the goal of the code.
  • The Data.HashMap.Strict API is missing a number of functions that are +available in Data.Map and Data.IntMap, and which cannot be trivially +reimplemented.

By contrast, when we use a typeclass-based interface (like Foldable or +Traversable), we get the following benefits:

  • All the functions I know are available to me. I can use Data.Foldable.mapM_ +for any instance of Foldable, even if the author of that data type never +considered mapM_.
  • Add-on packages can provide new combinators without ever knowing about the +data types I care about. The best example of this is Monad: there are +dozens (if not hundreds or thousands) of useful Monad-based combinators +sprinkled across Hackage, and these will work for arbitrary monadic datatypes I +write in my own application.
  • We can't accidentally end up with conflicting type signatures or semantics +for a function name. Some real-world examples of this are:

    • Data.Map.toList behaves very differently from Data.Foldable.toList
    • Data.ByteString.split versus Data.Text.split (text's split is +closer to Data.ByteString.splitWith)

By programming against interfaces, a developer learns an interface once, gets +access to a large array of convenience functions, and implementors can get away +with providing less functionality themselves.

No qualified imports

Many people claim that programming to typeclasses is really just about +laziness: the right way to do this is to import everything qualified. The +previous section shows it's about more than just laziness, but let me address +qualified imports head-on:

  • "Just laziness" is still a valid argument: why do extra work if it's not +necessary?
  • When I'm not using classy-prelude, I will often times in the middle of +coding realize I need access to, for example, Data.Map. I now need to break +my train-of-thought from my current code to:

    • Check if Data.Map is currently imported
    • If not, add it to the imports
    • Check if containers is in the cabal file
    • If not, add it to the build-depends in the cabal file
  • The above steps are even more confusing for new users
  • On multi-person projects, inconsistent qualified import names are a real +problem.

To expand on the last point: I've worked with many different people, and seen +plenty of disagreements on import naming. Consider all of the following:

import qualified Data.Map as Map
+import qualified Data.Map as M
+
+import qualified Control.Concurrent.MVar as M
+import qualified Control.Concurrent.MVar as MVar
+
+import qualified Data.ByteString as B
+import qualified Data.ByteString as S
+import qualified Data.ByteString as BS
+
+import qualified Data.ByteString.Char8 as B
+import qualified Data.ByteString.Char8 as B8
+
+import qualified Data.ByteString.Lazy as BS
+import qualified Data.ByteString.Lazy as L

This is a small subset of the confusion I've seen. We have the same modules +with different qualified names, and the same qualified names being shared by +multiple modules. The upshot of this is that, each time I'm working on a +different module, I need to remember which qualified import name is being used +here. Standardizing this would be a great solution, but the problem is that we +already tried it and it failed: most of the modules I listed above give a +recommended short name associated with them, but people (myself included) do +not follow it.

Gives us more information

Speaking in terms of type classes will often times tell us more than using a +concrete type. My favorite example of this is IO. Consider:

foo :: IO a -> IO a

We know virtually nothing about foo. However, if instead we had:

foo :: Monad m => m a -> m a

We now know a lot more. There are no monadic side-effects being added by foo +itself (though the provided action may perform some). We still don't know +everything: how many times is the supplied action being called, for example? +But it's still a great start.

What I find really interesting is, despite what you may initially think, the +following is also more informative than a plain IO:

foo :: MonadIO m => m a -> m a

In this case, foo can once again perform unbounded IO actions itself, e.g.:

foo action = do
+    liftIO fireTheMissiles
+    res <- action
+    liftIO fireTheMissilesAgainForGoodMeasure
+    return res

However, we know that some control flows (such as exception handling) are not +being used, since they are not compatible with MonadIO. (Reason: MonadIO +requires that the IO be in positive, not negative, position.) This lets us +know, for example, that foo is safe to use in a continuation-based monad like +ContT or Conduit.

Continue using the same abstractions

If you're familiar with the list API, you can pick up the majority of +classy-prelude trivially. Functions like length, break, takeWhile, and +sum are all present, work just like their list-specific cousins, and are +generalized to many additional types. Many of those types provide some nice +performance improvements, making it easy to speed up your code.

Another library which provides this kind of common interface to many datatypes +is lens. In many cases, this is done via a special typeclass (such as At, +Cons, or Each). However, these all require learning a different abstraction +from what you typically start Haskell with. On the other hand, the lens +approach allows for new ways of composing code that the more standard functions +make more difficult, so it certainly has advantages. I'm merely pointing out +here the much lower learning curve of picking up typeclasses based on the +functions you already know.

Performance

Since in many cases, we can implement our typeclass-based functions in terms of +underlying efficient functions for a specific data (together with INLINE and +REWRITE rules), we will usually pay no overhead for using these abstractions.

Easily test out alternative implementations

I've mentioned this in passing, but I'll call it out explicitly: programming to a typeclass lets you easily switch between different concrete implementations, which can be great for performance comparisons. Curious if a Map or a HashMap is faster? In the qualified import world, you'll need to:

  • Change your import
  • Modify the datatype name in all function signatures
  • Rewrite any code using a Data.Map function that's not present in Data.HashMap.Strict

In a typeclass-based approach, you can write your functions the first time in +terms of the typeclass, and then likely get away with changing just one +explicit Map signature to HashMap.

Common arguments against

I've given you a pretty opinionated list of reasons why I like typeclass-based +programming. That's not to say that strong arguments against this approach +don't exist. I'm going to give a (non-exhaustive) list of some such arguments +and address them.

  • Error messages are more confusing. This is definitely the case; an error of +Map is not a Vector is more clear than Map is not an instance of +IsSequence. My response to this is that, fairly quickly, you get used to the +error messages GHC generates and can understand them easily. Not quite as +easily as monomorphic error messages, but easily enough.

  • I've heard people claim things like "I don't know what the code is doing." +The argument goes like this: if you see a lookup, you don't know if it's a +lookup on a Map or a Hashmap. I consider this the weakest argument +against typeclass programming, because it means you aren't following the +paradigm at all. If you're coding to typeclasses, you don't care which +underlying implementation you're using: swapping out a Map for a HashMap +would be just fine, and you just care about the stated semantics of the +lookup function itself.

    There are times where you really do care about something that goes beyond +the semantics of the typeclass. For example, if you need the contents of a +set-like structure returned as a list in ascending order, toList on a +HashSet will fail you. But in such a case: you're really not looking for the +general toList function, you're looking for a specific function which +guarantees ordering.

  • An annoyance with typeclass coding is that, sometimes, you need to write +extra type signatures, since the compiler can't guess exactly which +implementation you're trying to use. This is certainly true, and can be +annoying, but it also goes hand-in-hand with being able to easily test out +alternative data types.

  • Some people only want to use typeclasses based on well-established +mathematical foundations. While I agree that having a mathematical basis to +a typeclass is a great way to ensure you have a good abstraction, I disagree +with it being a prerequisite for such a typeclass to be useful. And as my +evidence for this, I call the venerable Foldable typeclass, which has no laws +associated with it but is eminently useful. (Similar examples: Binary, +Storable, ToJSON, and FromJSON.)

    • That said, I will readily admit that some of my original work in +classy-prelude was far too loose in its usage of typeclasses for +getting simple name overloading. The first few releases of the library wanted +to test what was possible, but since then the typeclasses are restricted to +what I would consider to be very sensible abstractions. You may disagree, and +I'd be interested in hearing concrete examples of typeclasses you think are bad +abstractions. But I think it's possible to look at code written exclusively +against typeclasses and understand exactly what the code does.
  • There's a greater learning curve with an abstract interface versus a +monomorphic interface. This is true, but once you need to learn two +monomorphic interfaces, that learning curve is quickly amortized.

Reward for the patient

Those of you patient enough to sit through to the end of this long monologue +can get a sneak preview. I've put my Map abstraction classes on Github +inside the Jump +project. +The Jump project also happens to be the topic of conversation I had with the FP +Complete team that I mentioned in the first paragraph above. Expect more +information on this in the coming months.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/04/fixing-monad-either.html b/public/blog/2016/04/fixing-monad-either.html new file mode 100644 index 00000000..cf116dbc --- /dev/null +++ b/public/blog/2016/04/fixing-monad-either.html @@ -0,0 +1,66 @@ + Fixing the Monad instance for Either +

Fixing the Monad instance for Either

March 32, 2016

GravatarBy Michael Snoyman

Over the years, much has been said about Either-like Monad instances. To summarize our history, we've had:

  • No Monad instance at all for Either
  • An orphan Monad instance in transformers that used the Error class
  • A Monad instance for Either in base without an Error constraint
  • The ErrorT transformer, which introduces the Error class constraints
  • The EitherT transformer from the eithert package
  • The newer ExceptT transformer, which greatly improves on EitherT by giving it a more intuitive name

I'm not going to dive into all of these points, I merely raise them to stress the idea of how many approaches have been attempted to get this right. I'd like to explain how our current situation is wrong, how we can do much better, and propose that – despite the large impact on all Haskell users out there to make such a change – we should finally fix our library ecosystem correctly, once and for all.

The broken fail function

The real crux of the matter here is that, as everyone knows, the Monad instance for Either is exclusively used for representing some kind of error/failure/exception/etc (all those terms are, of course, synonymous). In the past, with the Error constraint, we had a valid implementation of the fail method for Either, which meant this vital method was properly implemented. Unfortunately now, we have fail = error for Either, preventing us from doing proper exception handling without resorting to more complex tricks.

The problem with Error

If we look at the definition of the Error typeclass, we'll see two methods:

class Error a where
+    noMsg :: a
+    strMsg :: String -> a

However, there's a perfectly good implementation of noMsg in terms of strMsg: noMsg = strMsg "". So really, our Error typeclass boils down to just one method of type String -> a. Once we realize that, it turns out that there's a far more appropriate typeclass already present in base to represent errors:

class IsString a where
+    fromString :: String -> a

The proper Monad Either instance

So now we can define a proper Monad instance for Either:

instance IsString e => Monad (Either e) where
+    return = ...
+    (>>=) = ...
+
+    fail = Left . fromString

This allows us to recover a properly behaving fail function, restoring sanity to our world.

Either for arbitrary error types

The final issue we may wish to address is allowing Either to work for arbitrary types. We can do so by leveraging overlapping instances, one of the most powerful and ubiquitous language extensions available in GHC. Using Typeable, we can generate truly beautiful error messages for our usage of Either. Below is a proof of concept demonstrating how we can fully fix our Either situation in GHC.

{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE UndecidableInstances #-}
+import Data.String
+import Data.Typeable
+
+data Either' a b = Left' a | Right' b
+    deriving (Functor, Show)
+
+instance Applicative (Either' a) where
+    pure = Right'
+
+    Left' a <*> _ = Left' a
+    Right' _ <*> Left' a = Left' a
+    Right' f <*> Right' x = Right' (f x)
+
+instance IsString a => Monad (Either' a) where
+    return = pure
+    (>>) = (*>)
+
+    Left' e >>= _ = Left' e
+    Right' x >>= f = f x
+
+    fail = Left' . fromString
+
+instance {-# OVERLAPPABLE #-} Typeable a => IsString a where
+    fromString s =
+        res
+      where
+        res = error $ concat
+            [ "No specific IsString instance for "
+            , show $ typeRep $ Just res
+            , ", when converting string: "
+            , s
+            ]
+
+main :: IO ()
+main = do
+    print (fail "failure 1" :: Either' String Int)
+    print ((do
+        Just x <- return Nothing
+        return x) :: Either' String Int)
+    print ((do
+        Just x <- return Nothing
+        return x) :: Either' Int Int)

What's next

I'm sure most of you are in full agreement with me at this point that this is our only way forward. Hopefully we can have a quick discussion period on the libraries@ list, and get this into base in time for GHC 8, which is currently on release candidate 37.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/04/split-db.html b/public/blog/2016/04/split-db.html new file mode 100644 index 00000000..aa9d0a64 --- /dev/null +++ b/public/blog/2016/04/split-db.html @@ -0,0 +1,60 @@ + Supporting separate read and write databases in persistent +

Supporting separate read and write databases in persistent

April 10, 2016

GravatarBy Eric Easley

Introduction

persistent has just released a new major version. With this release, queries which only read data are distinguished, at the type level, from queries which write data. This makes using a read replica database safer and generally makes our types more informative. Breakage should be minimal for end users.

Scaling a database

Many applications eventually reach the limits of a single database machine and work across multiple machines. Distributing state across multiple machines can introduce a great deal of complexity. Consequently, there are many approaches to dealing with multiple databases. The CAP theorem ensures that none of them is perfect (Is there ever a good impossibility theorem?). Front Row Education chose to go with streaming, native replication from a PostgreSQL write database to a PostgreSQL read database.

Possible solutions

What's the best way to program against a write database with a read replica? Here are a few approaches:

1. Run opaque queries against chosen backend.

In this approach, we don't change any of the persistent machinery. selectList, insert and runSqlPool stay the same:

runSqlPool :: (MonadBaseControl IO m) => ReaderT SqlBackend m a -> Pool SqlBackend -> m a
+writeBackend :: SqlBackend
+readBackend :: SqlBackend
+
+insertStudent :: ReaderT SqlBackend IO StudentId
+insertStudent = insert student
+
+selectStudents :: ReaderT SqlBackend IO [Entity Student]
+selectStudents = selectList ([] :: [Filter Student]) []
+
+insertAndSelect :: IO [Entity Student]
+insertAndSelect = do
+  _ <- runSqlPool (insertStudent >> insertStudent) writeBackend
+  runSqlPool selectStudents readBackend

We choose which backend to run our queries against at execution time. That is, we pass one backend to runSqlPool if we want to execute the query against the read database and a different backend if we want to execute our query against the write database.

This approach does work in the most basic sense of the word. But it's manual and error-prone. Nothing stops us from accidentally running a write query against a read database and getting an error at runtime. That's not the Haskell way! We'd be much better off encoding this read and write information in the query's type.

2. update, delete and insert write. select reads.

In this approach we create wrappers around SqlBackend called SqlReadBackend and SqlWriteBackend. Then, we specify that all selects (reads) will operate against the read database and all inserts, update, or delete (writes) will operate against the write database. We can intermix queries of different types with multiple (now type safe) calls to runSqlPool:

runSqlPool :: (MonadBaseControl IO m, IsSqlBackend backend) => ReaderT backend m a -> Pool backend -> m a
+writeBackend :: SqlWriteBackend
+readBackend :: SqlReadBackend
+
+insertStudent :: ReaderT SqlWriteBackend IO StudentId
+
+selectStudents :: ReaderT SqlReadBackend IO [Entity Student]
+
+insertAndSelect :: IO [Entity Student]
+insertAndSelect = do
+  _ <- runSqlPool (insertStudent >> insertStudnet) writeBackend
+  runSqlPool selectStudents readBackend

Attempting to run insertStudent against on the readBackend will result in a type error. Nice!

Unfortunately, it will also result in a type error when attempting to run selectStudents against the writeBackend. Which is why we used two calls to runSqlPool in the above example. This inability to mix reads and writes in a single transaction is rather restrictive.

This approach also ignores problems of eventual consistency. Even under streaming replication, there is some lag (hopefully, only a few milliseconds or less) between the read database and the write database. If we can't run reads in the same transaction, on the same DB as writes we have a serious problem. In the above example, we have no guarantee that our student insertions will have propagated to the read DB in time for the select that immediately follows the insert.

3. update, delete and insert write. select can be used in a read or write context.

We must generalize our read operations so that we can still run them against the write database when we need to.

runSqlPool :: (MonadBaseControl IO m, IsSqlBackend backend) => ReaderT backend m a -> Pool backend -> m a
+writeBackend :: SqlWriteBackend
+readBackend :: SqlReadBackend
+instance SqlBackendCanRead SqlWriteBackend
+instance SqlBackendCanRead SqlReadBackend
+
+insertStudent :: ReaderT SqlWriteBackend IO StudentId
+
+selectStudents :: (SqlBackendCanRead backend) => ReaderT backend IO [Entity Student]
+
+insertAndSelect :: IO [Entity Student]
+insertAndSelect =
+  runSqlPool (insertStudent >> insertStudent >> selectStudents) writeBackend

We now use type classes to say that write queries can only run against the write database but read queries can run against either type of database and we can defer the decision of where to run a read query until use. But in a safe way.

persistent

The new version of persistent follows the third approach.

Types as documentation

IO is sometimes referred to as Haskell's "sin bin". That is, a great number of effects end up marked as IO. Consequently, when you see IO in a type signature, it's hard to determine which effects that function uses. Does the function write to disk or get the current time? A more fine-grained type would make our types more informative.

Along similar lines, splitting the monolithic SqlPersistT into SqlReadT and SqlWriteT allows us to more clearly signal the capabilities leveraged inside a given function. When we see SqlReadT, we can be confident that the underlying database state hasn't changed.

Breaking changes

This version of persistent shouldn't break application authors and end users of persistent. You can continue to use SqlBackend which is now an instance of SqlBackendCanRead and SqlBackendCanWrite.

Library authors may need to modify some type signatures to work with the new machinery.

For example,

get404
+  :: (MonadIO m, PersistStore backend, backend ~ PersistEntityBackend val, PersistEntity val)
+  => Key val -> ReaderT backend m val

becomes

get404
+  :: (MonadIO m, PersistStore backend, BaseBackend backend ~ PersistEntityBackend val, PersistEntity val)
+  => Key val -> ReaderT backend m val

which leverages

instance HasPersistBackend SqlBackend where
+  type BaseBackend SqlBackend = SqlBackend
+instance HasPersistBackend SqlReadBackend where
+  type BaseBackend SqlReadBackend = SqlBackend
+instance HasPersistBackend SqlWriteBackend where
+  type BaseBackend SqlWriteBackend = SqlBackend

from persistent.

This new concept of BaseBackend allows us to still assign a unique type of backend to each PersistEntity despite the availability of SqlReadBackend, SqlWriteBackend and SqlBackend.

Conclusion

If you have a read replica, you can now access it more safely. Even if you don't, your types are now a little more informative. And you get this for almost free!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/04/stackifying-the-cookbook.html b/public/blog/2016/04/stackifying-the-cookbook.html new file mode 100644 index 00000000..4aa8eb83 --- /dev/null +++ b/public/blog/2016/04/stackifying-the-cookbook.html @@ -0,0 +1,39 @@ + Stackifying the cookbook +

Stackifying the cookbook

April 8, 2016

GravatarBy Michael Snoyman

Not too long ago, Sibi did a great job of cleaning up the Yesod cookbook and +moving it to its own Github +repository. The cookbook has been +a collaborative project from the community, and has accumulated lots of useful +examples over the years.

I'd now like to encourage others to help further improve it. As a recent Reddit +post brought to my attention, some of the cookbook examples do not compile with +recent versions of Yesod. I'd like to encourage the following:

  • Look through the cookbook and update examples to compile with the latest version of Yesod
  • Add Stack script interpreter lines to the beginning of each example

If you don't know about that second point, you can read the docs from the +Stack +website. But +you may find it easier to just look at the commit I made +yesterday, +or even just this part of it:

#!/usr/bin/env stack
+{- stack
+     --resolver lts-5.10
+     --install-ghc
+     runghc
+     --package yesod
+     --package yesod-static
+     --package persistent-sqlite
+ -}

With this addition, you can save the example into any .hs file and run it +with stack foo.hs (or chmod +x foo.hs && ./foo.hs). This gives three +really important advantages over an unadorned example:

  • It clearly documents which version of GHC and all dependencies are being used (via the resolver line)
  • It states which packages need to be available. Note that I'm not providing an exhaustive list of packages, since depending on yesod will automatically include all of its dependencies (e.g., yesod-form, warp)
  • For someone trying to get started quickly, it takes care of many details automatically (installing GHC, building packages). All they need to do is install Stack, and the #!/usr/bin/env stack is a good hint in that direction

I'd recommend communicating intentions of working on cookbook examples on the +mailing list, so that work isn't duplicated, and so others can review changes. +And if there are questions about how to make updates to some examples, +definitely ask!

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/05/are-unused-import-warnings-harmful.html b/public/blog/2016/05/are-unused-import-warnings-harmful.html new file mode 100644 index 00000000..a0bc33d8 --- /dev/null +++ b/public/blog/2016/05/are-unused-import-warnings-harmful.html @@ -0,0 +1,45 @@ + Are unused import warnings harmful? +

Are unused import warnings harmful?

May 18, 2016

GravatarBy Michael Snoyman

Which of the following snippets of code is better?

#if MIN_VERSION_base(4,8,0)
+import Control.Applicative ((<*))
+#else
+import Control.Applicative ((<*), pure)
+#endif

Versus:

import Control.Applicative ((<*), pure)

If you are working on a project that supports multiple GHC versions, enable +extra warnings via -Wall, and actually like to get your code to compile +without any warnings, you'll probably say that the former is better. I'm going +to claim, however, that any sane human being knows intuitively that the latter +is the better version of the code, for multiple reasons:

  • It doesn't require a language extension to be enabled
  • It's much shorter without losing any useful information to the reader
  • It's more robust to future changes: if you need to add an import, you don't +have to remember to update two places

However, if you look through my code bases, and the code bases of many other +open source Haskell authors, you'll find the former examples regularly. I'm +beginning to come to the conclusion that we've been attacking this problem the +wrong way, and what we should be doing is:

  • Turning on -Wall in our code
  • Either modify -Wall in GHC to not warn about unused imports, or explicitly +disable unused import warnings via -fno-warn-unused-imports
  • As many of us already do, religiously use Travis +CI to +check multiple GHC versions to avoid accidental regressions
  • In our Travis builds, start turning on -Werror

Maintaining complex CPP in our imports is sometimes a necessary evil, such as +when APIs change. But when we are simply doing it to work around changes in +what Prelude or other modules export, it's an unnecessary evil. This is +similar to the change to GHC a few years back which allowed hiding +(isNotExported) to not generate a warning: it made it much easier to deal with +the now-no-longer-present Prelude.catch function.

While it's true that removing unused imports is a nice thing to do to our +codebases from time to time, their presence does not actually indicate any +potential issues with our code. My concern with the presence of these warnings +is that they will lead to one of two situations:

  • We simply accept that our libraries generate warnings when compiled, which +ends up hiding actionable warnings via a terrible signal-to-noise ratio
  • In an effort to clean up all warnings, we end up creating hideous messes like +those above, or breaking backwards compatibility with old versions of +dependencies

I haven't actually started making these modifications to my libraries, as I'm +not yet fully convinced that this is a good idea. There are also other points +in this design space, like explicitly marking some imports as redundant, though +that would require some deeper changes to GHC and wouldn't be usable until we +drop support for all current GHC versions.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/07/http2-server-push.html b/public/blog/2016/07/http2-server-push.html new file mode 100644 index 00000000..f1438844 --- /dev/null +++ b/public/blog/2016/07/http2-server-push.html @@ -0,0 +1,67 @@ + Implementing HTTP/2 server push +

Implementing HTTP/2 server push

July 6, 2016

GravatarBy Kazu Yamamoto

What is HTTP/2 server push?

Server push is a hot topic of HTTP/2. +Effective usage of server push can improve user experience. +For instance, let's consider a scenario: +downloading "index.html" which requires "style.css".

If a browser uses HTTP/1.1, it gets the "index.html" first, +parses it, +then obtains "style.css". +So, two round-trip-times (RTTs) are necessary +before the browser starts rendering.

On the other hand, if a browser uses HTTP/2 and +the corresponding server supports server push, +only one RTT is necessary. +That is, when the browser send a GET request for "index.html", +the server pushes "style.css" before +it transfers "index.html". +When the browser parses "index.html", +it finds "style.css" in its local cache. +Thus, it can start rendering at this timing.

APIs for server push

So, how can we implement HTTP/2 server push in Warp? +Clearly, it is necessary for an application to tell Warp +what files should be pushed. +One way is to extend the Response type to store information on server push. +But this breaks compatibility. +We need to modify all existing applications.

To keep all existing types as is, +I decided to use vault in the Request type. +Vault is a heterogeneous infinite map which can store any type. +Warp creates a new IORef and store its getter and setter to the vault:

getHTTP2Data :: Request -> IO (Maybe HTTP2Data)
+setHTTP2Data :: Request -> Maybe HTTP2Data -> IO ()

HTTP2Data is the data type to store a list of files to be pushed. +An application can set files to be pushed by setHTTP2Data. +Warp retrieves them by getHTTP2Data and +pushes them before sending the corresponding Response.

Note that the vault is a part of Web Application Interface (WAI) but +these two functions are not. +They are APIs provided only by Warp.

Middleware for server push

The next question is how an application knows which files to be pushed for a given URL. +One way is manifest files.

Another way is learning based on Referer:. +This idea came from Jetty. +Typically, there is the Referer: whose value +is, say, https://example.com/index.html", +in a GET request to "style.css". +So, analyzing requests for a while, +we can learn that "style.css" should be pushed +when "index.html" is requested.

@davean suggested that I implement this mechanism as a middleware. +So, I created a middleware for HTTP/2 server push based on Referer:. +Its default behavior for a given Request and Response is as follows:

  • If files to be pushed are found for a given path in a learning dictionary, set them by setHTTP2Data.
  • Otherwise, register the file of Response to the learning dictionary only when the following conditions are met:
  1. The Request stores a valid Referer:
  2. The Response is a file type
  3. The path of Referer: is "/" or ends with ".html"/".html"
  4. The path ends with ".js" and ".css"

The learning dictionary is cleared every 30 seconds.

Warp v3.2.7 with the push APIs and wai-http2-extra v0.0.0 with the middleware are already +on Hackage. +If you implement a server push middleware based on manifest files, +please send a pull request on github.

Visualization

Here are screen shots of Firefox accessing the new Warp. +The first figure is the first access and the middleware has not learned anything. +So, no pushes are used.

Figure 1: Download time-line without server push

The second figure is the second access. +You can see .js and .css files are pushed +since bullets are not green.

Figure 2: Download time-line with server push

Next Step

The next step would be an implementation of +Cache Digests for HTTP/2. +In this scheme, a browser can tell a list of cached file to a server. +So, the server can avoid unnecessary pushes.

Acknowledgment

Efforts to bring HTTP/2 server push feature to Warp was originally made by Andrew Pritchard. +Without his experience and suggestions, +this work would be impossible. +I thank him deeply.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/07/new-http-client-mono-traversable.html b/public/blog/2016/07/new-http-client-mono-traversable.html new file mode 100644 index 00000000..825bcdb6 --- /dev/null +++ b/public/blog/2016/07/new-http-client-mono-traversable.html @@ -0,0 +1,65 @@ + New http-client and mono-traversable releases +

New http-client and mono-traversable releases

July 5, 2016

GravatarBy Michael Snoyman

I'm happy to announce the release of a number of packages today, but mostly +this comes down to upgrades to +http-client and +mono-traversable. This +blog post will cover the main changes you should be aware of it you are using +one of these two packages. In addition to these two packages, the following +related packages are being released as well: +http-client-tls, +http-client-openssl, +http-conduit, +mono-traversable-instances, +minlen, +chunked-data, +conduit-combinators, +mutable-containers, +classy-prelude, +classy-prelude-conduit, +classy-prelude-yesod.

http-client

http-client is an HTTP client library leveraged by a number of other packages, +including http-conduit, +pipes-http, and +wreq. This release is mostly about +addressing issue #193, +about an controversial design decision to throw runtime exceptions on +non-successful HTTP response statuses. If you want a quick explanation of +what's changed and what you should do:

  • If the HTTP server you're talking to gives a non-success HTTP response status +(e.g., 404 not found), you will no longer get a runtime exception by +default.

    • If you want the old behavior, switch to the parseUrlThrow function
    • The parseUrl function remains with the old behavior, but is deprecated in favor of parseRequest
    • The IsString instance has also switched to the non-throwing behavior
  • In an effort to make the remaining exceptions more useful, and avoid people +accidentally relying on the old exception behavior, there's a new structure +to the HttpException type. In particular, almost all exceptions are now +contained in the HttpExceptionContent type, and will be wrapped up with the +HttpExceptionRequest constructor, which provies information on the Request +used to generate the exception. Hopefully this will make for much more useful +error messages.

Based on the feedback I've received, this should bring the default behavior for +http-client into line with what people expect, and will hopefully have a +minimal impact of migration for existing users relying on the current behavior.

mono-traversable

The mono-traversable package provides a typeclass hierarchy based around the +idea of monomorphic containers. This allows, for examples, a unified foldMap +function that works on lists, ByteStrings, and unboxed vectors, as well as +abstractions over sequences (functions like break and drop) and containers +(Maps and Sets).

I laid out a plan for +a cleanup of the mono-traversable package. Most of the changes here were +minor, and will not affect end users. Of import:

  • The mono-traversable package itself has much reduced dependencies, by +putting a number of instances into the new mono-traversable-instances +package.
  • A few typeclasses that used to live in chunked-data have moved to mono-traversable
  • The Data.NonNull module is much simpler, and no longer based on Data.MinLen (which lives on in the minlen package)

classy-prelude

Mostly, classy-prelude is just inheriting the upstream changes in +mono-traversable. The only other point I'd like to make about this +classy-prelude release is that it is switching over to the new safe-exceptions +package, which I recently +announced on the FP Complete +blog.

Mega repo

To simplify maintenance, and address a common problem of people not knowing +which repo to report issues to, I've combined a few repos together into a +mono-traversable mega-repo:

  • classy-prelude (and -conduit and -yesod)
  • chunked-data
  • mutable-containers

I've updated the old repo locations with a final commit pointing to the new +location.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/09/better-ci-yesod-scaffoldings.html b/public/blog/2016/09/better-ci-yesod-scaffoldings.html new file mode 100644 index 00000000..57cefb6d --- /dev/null +++ b/public/blog/2016/09/better-ci-yesod-scaffoldings.html @@ -0,0 +1,54 @@ + Better CI for the Yesod scaffoldings +

Better CI for the Yesod scaffoldings

September 5, 2016

GravatarBy Michael Snoyman

After having completely forgotten to do this for a long time, I +finally set aside some time last night to fix up the Travis CI for the +Yesod scaffoldings. We +have a number of different flavors of scaffoldings (e.g., PostgreSQL, +MySQL, simple, and minimal), and keep all of the different flavors as +branches on a single repo so that improvements to the base scaffolding +can easily be merged into all of the others. The goal of my changes was to:

  • Have Travis check against the different snapshots likely to be +selected by the stack new command
  • Automate testing against live databases, so that I can be lazy and +not set up those databases for local testing

Overall, this went pretty smoothly, and also serves as a nice example +of a short Stack-based Travis +configuration. You can +see the latest PostgreSQL Travis configuration.

Beyond my desire to be lazy in the scaffolding release process, the +other obvious benefit is much more confidence when review PRs against +the scaffolding that things will actually work.

Interesting discoveries

I discovered two things in this work that I hadn't realized +previously:

  • Due to a dependency on a relatively recent yesod-auth version, only +LTS Haskell 6 and up and Stackage Nightly are supported by the +scaffolding. Therefore, for the build matrix, I haven't included LTS +5 and lower.
  • I was unaware of +a bug in GHC 8.0.1 +which prevents the scaffolding from compiling. This bug has already +been resolved upstream and the fix will be included in GHC 8.0.2. In +the meanwhile, I've +blocked GHC 8.0.1 from usage in the scaffolding.

Upon reflection, this is probably a good thing: we are all but +guaranteed that a new user will start off with LTS 6, which is a well +tested set of packages and a very stable GHC version, leading to +hopefully little user friction when getting started.

A user could in theory do something like stack new foo yesod-simple +--resolver lts-5 --solver, but I think it's fair to assume that +someone passing in such specific flags knows what he/she is doing and +we should just stay out of his/her way.

Contributing

Now that CI is in better shape, this is a good time to remind everyone of how +to contribute to the Yesod scaffoldings. The PostgreSQL scaffolding is +considered the base, with the other flavors merging in changes from there. If +you want to make a change, please submit a PR to the postgres branch of the +yesod-scaffold +repo. If you have a +patch which is specific to one of the scaffoldings instead (like changing MySQL +config settings), please submit it to that branch.

Note that it is not supported to send pull requests against the .hsfiles +files in the stack-templates +repo, as such changes +can't be properly tested, and will be overwritten the next time a change from +the yesod-scaffold repo is merged in.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/11/new-yesod-devel-server.html b/public/blog/2016/11/new-yesod-devel-server.html new file mode 100644 index 00000000..3608eaf6 --- /dev/null +++ b/public/blog/2016/11/new-yesod-devel-server.html @@ -0,0 +1,73 @@ + The New (Experimental) Yesod Development Server - Feedback Requested! +

The New (Experimental) Yesod Development Server - Feedback Requested!

November 24, 2016

GravatarBy Michael Snoyman

I'm guessing almost every Yesod user has - at some point - used the +venerable yesod devel command, which launches a server which +auto-recompiles your source code on any file changes. This has been a +core part of the Yesod ecosystem for many years. Unfortunately, it's +had to be far more complicated than I'd have liked:

  • Since it predates addDependentFile (good work Greg Weber on +getting that in!), it has some pretty complex logic around guessing +which external files (like Hamlet files) should force a recompile. +(Adding support for addDependentFile to the current yesod devel +is possible, but it's a non-trivial undertaking.)
  • In order to ensure a consistent set of dependencies, it does some +real fancy footwork around intercepting arguments passed to ghc +and linker executables.
  • In order to parse various files, it links against the ghc library, +tying it to a specific compiler version. This makes things difficult +for users (don't accidentally use yesod from GHC 7.10.3 with GHC +8.0.1!), and sometimes +really +painful for +maintainers.

For a few months now, I've been meaning to greatly simplify yesod +devel, but the maintenance burden finally gave me the excuse I needed +to bite the bullet and do it. The result is a +dramatic simplification of the code base. First +I'd like to ask for user feedback, and then I'll discuss some of the +details of implementation.

Please try it out!

Since this is such a big change, I'd really appreciate if others could +give this a shot before I release it. There are two ways you can do +this:

Method 1:

  • git clone https://github.com/yesodweb/yesod --branch 1304-stack-based-devel yesod-new-devel
  • cd yesod-new-devel
  • stack install yesod-bin
  • From your project directory, run yesod devel. NOTE: do not use +stack exec -- yesod devel, you want to use the newly globally +installed executable, not the one from your snapshot!

Method 2:

  • Add the following to your stack.yaml file's packages list:

    - location:
    +    git: https://github.com/yesodweb/yesod
    +    commit: f3fc735a25eb3d5c051c761b59070eb9a0e4e156
    +  subdirs:
    +  - yesod-bin
    +  extra-dep: true
  • Likely: add the following to your stack.yaml file's extra-deps list:

    - say-0.1.0.0
    +- typed-process-0.1.0.0
  • stack build yesod-bin
  • stack exec -- yesod devel

Use whichever method you feel most comfortable with. Please let me +know both successes and failures, and then I'll try to get this rolled +out. Comments would be great +on the Github pull request. +So far, in my limited testing, I've found that the new yesod devel +runs faster than the current one, but that could very much be +confirmation bias speaking.

Note: there are a few removed features in this update, please see the +changelog.

How it works

The big change - as the branch name implies - was depending entirely +on Stack for all of the heavy lifting. Stack already provides a +--file-watch command to automatically recompile, and uses GHC's own +addDependentFile information to track external file +dependencies. This cuts out the vast majority of the +complexity. There's no longer any need to depend on the ghc library, +there's less Cabal library code involved (making cross-version +support much simpler), and almost everything is handled by shelling +out to external executables.

I also got to redo the concurrency aspects of this using my absolute +favorite package in the world: +async. The result is, in my +opinion, very straightforward. I also leveraged some of the newer +libraries I've worked on, like +safe-exceptions, +typed-process, and +say.

The code is (finally) well commented, so you can +jump in and look yourself. I've +also added +a decent README, +and +an example of using yesod devel with a non-Yesod project.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2016/11/use-mysql-safely-in-yesod.html b/public/blog/2016/11/use-mysql-safely-in-yesod.html new file mode 100644 index 00000000..ca22a62e --- /dev/null +++ b/public/blog/2016/11/use-mysql-safely-in-yesod.html @@ -0,0 +1,30 @@ + Use MySQL Safely in Yesod Applications +

Use MySQL Safely in Yesod Applications

November 6, 2016

GravatarBy Paul Rouse

With the latest version (0.1.4) of the mysql library, we now have the machinery needed to use it properly in a concurrent setting. In the past, any multi-threaded use was a little risky, although in practice it seems to have been satisfactory for applications which were not too demanding.

The necessary changes have just been made to the MySQL version of the scaffolding, and are described here. Existing Yesod sites should be updated in a similar manner. This post should give you all you need to know, but further background can be found in the MySQL manual and Roman Cheplyaka's blog.

But It Worked Anyway, Didn't It?

Let's start by reviewing why the mysql library works automatically in a single-threaded program, and why we might have got away with it most of the time in Yesod applications.

The underlying C library (libmysqlclient) requires a one-off initialisation, and then each thread in which it is called must be initialised to allocate some thread-local data. However, these actions are carried out automatically when a connect call is made, if they have not already been done. So nothing further is needed in a single-threaded program: a connect necessarily comes first, and it performs the required initialisations.

This behaviour of the connect call probably also explains why we have mostly got away with ignoring the problem in Yesod applications. Warp creates lightweight, Haskell threads by default, and these run in a rather small number of OS threads. When a new connection is opened and added to the pool, the OS thread running at the time will be initialised, as just described. Due to the small number of these threads, there is a reasonable chance that this is the first database action in each of them, resulting in correct initialisation. But there are no guarantees!

Correct Multi-Threaded Use

To be completely correct, we have to do all of the following:

  • Initialise the library as a whole.
  • Use bound threads for those which might perform database operations.
  • Initialise each thread properly.
  • Finalise each thread to free the memory used by its thread-local state.

The library initialisation is not thread-safe; it needs to be called separately to ensure that subsequent connect calls, occurring in multiple threads, detect that it has been done and do not repeat it themselves. This has been achieved in the scaffolding by calling MySQL.initLibrary from makeFoundation, before any database actions are carried out:

...
+import qualified Database.MySQL.Base as MySQL
+...
+makeFoundation appSettings = do
+    ...
+    MySQL.initLibrary

The point about bound threads is that they provide a guarantee that related initialisation and database operations really do occur in the same OS thread. However, using them means that OS threads are created frequently, and the argument given above no longer applies, not even as an approximation: the threads definitely need explicit initialisation. They also need finalising to avoid a memory leak - again this is made important by the large number of threads. (There are some situations in which the finalisation can be omitted, but check the documentation carefully before doing so.)

The settings passed to warp can be used to make it spawn bound threads, instead of Haskell threads, and to specify functions to initialise and finalise them. This code shows how it is now done in the scaffolding, in Application.hs:

warpSettings foundation =
+    ...
+    $ setFork (\x -> void $ forkOSWithUnmask x)
+    $ setOnOpen (const $ MySQL.initThread >> return True)
+    $ setOnClose (const MySQL.endThread)
+      defaultSettings

Warp forks a new thread to handle each connection, using the function specified by setFork. The functions passed to setOnOpen and setOnClose are called right at the start of processing the connection, and right at the end, so they are valid places to initialise and finalise the thread for use by the mysql library.

The argument to setFork is a function which creates bound threads. If you are wondering why it is written the way it is, instead of void . forkOSWithUnmask, it simply avoids the need for the ImpredicativeTypes language extension, which is considered fragile and is sometimes broken by new compiler releases!

Unfortunately, forkOSWithUnmask is not exported by Control.Concurrent until base-4.9 (ie GHC 8), so, when using earlier versions, we have to copy its definition into our code:

{-# LANGUAGE RankNTypes           #-}
+...
+import GHC.IO                               (unsafeUnmask)
+...
+forkOSWithUnmask :: ((forall a . IO a -> IO a) -> IO ()) -> IO ThreadId
+forkOSWithUnmask io = forkOS (io unsafeUnmask)

What About Efficiency?

OS threads are more expensive than Haskell threads, but the difference may not matter much compared to all the other processing which is going on in a real application. It would be wise to do some benchmarks before worrying about it!

One possible optimisation is to make sure that HTTP keepalive is used, since warp creates a thread per connection, not per request. Some reverse proxies might need explicit configuration for this.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2017/02/changes-yesods-ci.html b/public/blog/2017/02/changes-yesods-ci.html new file mode 100644 index 00000000..e24e72ec --- /dev/null +++ b/public/blog/2017/02/changes-yesods-ci.html @@ -0,0 +1,31 @@ + Changes to Yesod's CI +

Changes to Yesod's CI

February 8, 2017

GravatarBy Michael Snoyman

I've made some changes in the past few days to the CI setup for the +yesodweb/yesod repo that I +thought contributors may be interested in knowing about.

  • We were regularly running into build timeouts on OS X on Travis. To +work around this, we no longer build benchmarks and Haddocks on OS +X, and compile with +-O0. Relevant Travis changes
  • I've (finally) bit the bullet and made the repo compile with -Wall +-Werror enabled, at least for recent versions of GHC. From now on, +PRs will need to maintain +warning-cleanliness. Relevant Travis changes
  • There's now an AppVeyor configuration, so PRs can be checked against +Windows (in addition to the existing Linux and OS X coverage +provided by Travis). This did, in fact, reveal +two +issues +around line +endings. Relevant AppVeyor addition.

Nothing major, just a few changes that contributors should be aware +of. Hopefully that green checkmark on a PR will now have a few less of +both false positives and false negatives.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2017/06/updated-yesod-scaffolding.html b/public/blog/2017/06/updated-yesod-scaffolding.html new file mode 100644 index 00000000..c48ab841 --- /dev/null +++ b/public/blog/2017/06/updated-yesod-scaffolding.html @@ -0,0 +1,33 @@ + Updated Yesod Scaffolding +

Updated Yesod Scaffolding

June 7, 2017

GravatarBy Michael Snoyman

A few days ago I released an +update to the Yesod scaffolding. It's +nothing major, but it has some new niceness I thought people would be +interested in:

  1. I've (finally) moved the Haskell source files into a src +directory. I rejected some moves in the past. But since then, this +style has become the dominant style in the Haskell world, and it +makes sense to embrace it.
  2. Instead of putting language extensions in the default-extensions +field of the cabal file, they are now in LANGUAGE pragmas in each +source file. This was not an obvious decision to make, and there +are still people (myself included) who are conflicted on it. You +can see some of the discussion of this on Twitter:

  3. We've moved from a cabal file to an hpack package.yaml file. I +only started using hpack a few months back, but it's completely won +me over already. For those not familiar, check out +the hpack repo. Note that +hpack generates a cabal file, so there is full compatibility with +cabal-the-build-system. We just get some niceties, like +leaving off exposed-modules.

Next time you create a scaffolded Yesod project (by running, +e.g. stack new mysite yesod-postgres), you'll automatically get this +updated scaffolding.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2017/11/mega-sdist.html b/public/blog/2017/11/mega-sdist.html new file mode 100644 index 00000000..41e541b0 --- /dev/null +++ b/public/blog/2017/11/mega-sdist.html @@ -0,0 +1,107 @@ + mega-sdist: the mega repo helper +

mega-sdist: the mega repo helper

November 21, 2017

GravatarBy Michael Snoyman

Many years ago, I wrote a utility called mega-sdist to help me with managing +mega repos (more on that below). I've been using it myself ever since, making +some minor improvements over the years. But I realized recently that I never +really announced it to others, and especially not to the people whom it would +help the most: other Yesod contributors and maintainers. Consider this the +(massively belated) announcement.

You can find the most up-to-date information in the project README.md on +Github. Below is the current +content of that file, to help save you a click.


This is a utility written to address the specific needs in maintaining +Haskell "mega-repos," or Git repositories containing multiple Cabal +projects. It is intended to ease the process of deciding which +packages need to be released and tagging those releases appropriately.

It provides the following functionality:

  • Detect when local code has changed from what's on Hackage
    • Note that, due to Hackage revisions, sometimes this logic isn't +perfect
  • Detect when a version number needs to be updated
  • Dump the difference between the Hackage version of your package and +the local version

To install it... well, listen. This tool is intended for people +authoring Haskell packages. Odds are, you already know how to do +this. And if you don't know, this probably isn't a tool that will help +you. Anyway, in order to install it, first +install Stack and then run +stack install mega-sdist, or just stack install inside this +repository.

Opinionated tool

This utility is highly opinionated in some ways, e.g.:

  • It only supports one style of Git tag name: +packagename/version. This may look weird in non-mega-repos, where +v1.2.3 looks better than foo/1.2.3, but for mega-repos the +former doesn't make sense.
  • It depends on Stack for both discovering all of your local packages, +and for uploading to Hackage.

If you're OK with these opinions, keep reading for usage.

Have I changed anything?

Let's say I'm working on the +monad-unlift megarepo (chosen +as an example of a relatively small repo). I've merged some PRs +recently, or at least think I have. But I don't remember which of the +individual packages within the repo this affected. Instead of looking +at the commit history like some caveman, I'll typically do:

$ git pull # make sure I have all latest changes
+$ mega-sdist

The mega-sdist command will:

  • Build tarballs for all local packages
  • Check what the latest versions of my packages on Hackage are
  • Do a full diff on these two things and see if anything's changed

At the time of writing, here's the output from this repo:

The following packages from Hackage have not changed:
+monad-unlift-0.2.0
+
+The following packages require a version bump:
+monad-unlift-ref-0.2.1

What this means is:

  • The monad-unlift package I have locally is at version 0.2.0. And +it perfectly matches that version on Hackage. No actions necessary.
  • The monad-unlift-ref package I have locally is at version +0.2.1. And it doesn't match the code on Hackage. Therefore, if I +wanted to run stack upload monad-unlift-ref successfully, I'd need +to bump the version number.

What did I change?

Well, again, if I wanted to see what changed, I could run (again, like +a caveman):

$ git diff monad-unlift-ref/0.2.1 -- monad-unlift-ref

But that's long! mega-sidst's got your back. Just run:

$ mega-sdist monad-unlift-ref --get-diffs

This will print out the difference between the tarball uploaded to +Hackage and what you have locally. Besides my tongue-in-cheek comment +above, this is also useful if, for some reason, you either don't have +or don't trust the tags in your Git repo.

One other thing: this diff is currently based on the pristine tarball +from Hackage, ignoring cabal file revisions. So the difference may be +slightly different from what you'd get from stack unpack +monad-unlift-ref-0.2.1. But ¯\_(ツ)_/¯ that's revisions for you.

The default behavior of mega-sdist is to look at all packages +specified in your stack.yaml. Targets can be any directory. And +mega-sdist will automatically look at packages in any +subdirectory, so that mega-sdist . is the same as mega-sdist at +the root of your repo*.

* Assuming all of your packages are actually in your repo, but only +crazy people would do otherwise.

Preparing a new release

OK, now I continue working on my project, and I've:

  • Made some changes to monad-unlift
  • Updated the cabal file's version number
    • And of course I also updated the ChangeLog.md, I'm not some +monster

From the root of my repo, I run:

$ mega-sdist monad-unlift

Or, equivalently, from inside the monad-unlift subdirectory I run:

$ mega-sdist .

Either way, I get:

The following new packages exist locally:
+monad-unlift-0.2.1
+
+No version bumps required, good to go!

This tells me that my package has local changes, and the version +number has been updated, so that stack upload monad-unlift will +work. Neato! Now, you could just run stack upload ..., but here's +what I usually do. First, I'll review the changes I'm about to upload +and make sure there are no surprises:

$ mega-sdist --get-diffs .
+
+The following new packages exist locally:
+monad-unlift-0.2.1
+diff -r old/monad-unlift-0.2.0/ChangeLog.md new/monad-unlift-0.2.1/ChangeLog.md
+0a1,4
+> ## 0.2.1
+>
+> * Silly changes
+>
+diff -r old/monad-unlift-0.2.0/Control/Monad/Trans/Unlift.hs new/monad-unlift-0.2.1/Control/Monad/Trans/Unlift.hs
+51a52,54
+>
+> -- I just need some space
+>
+diff -r old/monad-unlift-0.2.0/monad-unlift.cabal new/monad-unlift-0.2.1/monad-unlift.cabal
+2c2
+< version:             0.2.0
+---
+> version:             0.2.1
+
+No version bumps required, good to go!

OK, that's what I wanted. Time to release. Next, I'm going to use +mega-sdist to tag the release:

$ mega-sdist --gittag .

From the root of my repo, this would notice that monad-unlift-ref +still requires a version bump, and refuse to proceed. But inside the +monad-unlift directory, it notices that all necessary version bumps +are done, and happily tags:

$ mega-sdist --gittag .
+The following new packages exist locally:
+monad-unlift-0.2.1
+
+No version bumps required, good to go!
+Raw command: git tag monad-unlift/0.2.1

And suddenly I notice something new:

$ ls tarballs/
+monad-unlift-0.2.1.tar.gz

Neat, mega-sdist left behind tarballs I can upload! To do so, I run:

$ stack upload tarballs/*

Note that this will work whether I'm trying to upload just one +package, or all of the updated packages in my repo. Finally, I need to +push the new tags to Github (or wherever):

$ git push --tags

And in fact, this upload sequence is so common that I have a shell +alias set up:

$ alias upload
+alias upload='mega-sdist --gittag . && stack upload tarballs/* && git push --tags'

So there you have it: convenient little utility to help manage repos +with lots of packages in them.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2018/01/upcoming-yesod-breaking-changes.html b/public/blog/2018/01/upcoming-yesod-breaking-changes.html new file mode 100644 index 00000000..5e567faa --- /dev/null +++ b/public/blog/2018/01/upcoming-yesod-breaking-changes.html @@ -0,0 +1,98 @@ + Upcoming Yesod breaking changes +

Upcoming Yesod breaking changes

January 11, 2018

GravatarBy Michael Snoyman

With all of the talk I've had about breaking changes in my libraries, +I definitely didn't want the Yesod world to feel left out. We've been +stable at yesod-core version 1.4 since 2014. But the changes going +through my package ecosystem towards MonadUnliftIO are going to +affect Yesod as well. The question is: how significantly?

For those not aware, MonadUnliftIO is an alternative typeclass to +both MonadBaseControl and the MonadCatch/MonadMask classes in +monad-control and exceptions, respectively. I've mentioned the +advantages of this new approach in a number of places, but the best +resource is probably the +release announcement blog post.

At the simplest level, the breaking change in Yesod would consist of:

  • Modifying WidgetT's internal representation. This is necessary +since, currently, it's implemented as a WriterT. Instead, to match +with MonadUnliftIO, it needs to be a ReaderT holding an +IORef. This is just about as minor a breaking change as I can +imagine, since it only affects internal modules. (Said another way: +it could even be argued to be a non-breaking change.)
  • Drop the MonadBaseControl and MonadCatch/MonadMask +instances. This isn't strictly necessary, but has two advantages: it +allows reduces the dependency footprint, and further encourages +avoiding dangerous behavior, like using concurrently with a +StateT on top of HandlerT.
  • Switch over to the new versions of the dependent libraries that are +changing, in particular conduit and resourcet. (That's not +technically a breaking change, but I typically consider dropping +support for a major version of a dependency a semi-breaking change.)
  • A number of minor cleanups that have been waiting for a breaking +changes. This includes things like adding strictness annotations in +a few places, and removing the defunct GoogleEmail and BrowserId +modules.

This is a perfectly reasonable set of changes to make, and we can +easily call this Yesod 1.5 (or 2.0) and ship it. I'm going to share +one more slightly larger change I've experimented with, and I'd +appreciated feedback on whether it's worth the breakage to users of +Yesod.

Away with transformers!

NOTE All comments here, as is usually the case in these discussions, +refer to code that must be in IO anyway. Pure code gets a pass.

You can check out the changes (which appear larger than they actually +are) in +the no-transformers branch. You'll +see shortly that that's a lie, but it does accurately indicate +intent. If you look at the pattern of the blog posts and recommended +best practices I've been discussing for the past year, it ultimately +comes down to a simple claim: we massively overuse monad transformers +in modern Haskell.

The most extreme response to this claim is that we should get rid of +all transformers, and just have our code live in IO. I've made a +slight compromise to this for ergonomics, and decided it's worth +keeping reader capabilities, because it's a major pain (or at least +perceived major pain) to pass extra stuff around for, e.g., simple +functions like logInfo.

The core data type for Yesod is HandlerT, with code that looks like +getHomeR :: HandlerT App IO Html. Under the surface, HandlerT +looks something like:

newtype HandlerT site m a = HandlerT (HandlerData site -> m a)

Let's ask a simple question: do we really need HandlerT to be a +transformer? Why not simply rewrite it to be:

newtype HandlerFor site a = HandlerFor (HandlerData site -> IO a)

All we've done is replaced the m type parameter with a concrete +selection of IO. There are already assumptions all over the place +that your handlers will necessarily have IO as the base monad, so +we're not really losing any generality. But what we gain is:

  • Slightly clearer error messages
  • Less type constraints, such as MonadUnliftIO m, floating around
  • Internally, this actually simplifies quite a few ugly things around +weird type families

We can also regain a lot of backwards compatibility with a helper type +synonym:

type HandlerT site m = HandlerFor site

Plus, if you're using the Handler type synonym generated by the +Template Haskell code, the new version of Yesod would just generate +the right thing. Overall, this is a slight improvement, and we need to +weigh the benefit of it versus the cost of breakage. But let me throw +one other thing into the mix.

Handling subsite (yes, transformers)

I lied, twice: the new branch does use transformers, and HandlerT +is more general than HandlerFor. In both cases, this has to do +with subsites, which have historically been a real pain to write +(using them hasn't been too bad). In fact, the entire reason we have +HandlerT today is to try and make subsites work in a nicely layered +way (which I think I failed at). Those who have been using Yesod long +enough likely remember GHandler as a previous approach for this. And +anyone who has played with writing a subsite, and the hell which +ensues when trying to use defaultLayout, will agree that the +situation today is not great.

So cutting through all of the crap: when writing a subsite, almost +everything is the same as writing normal handler code. The following +differences pop up:

  • When you call getYesod, you get the master site's app data +(e.g. App in a scaffolded site). You need some way to get the +subsite's data as well (e.g., the Static value in yesod-static).
  • When you call getCurrentRoute, it will give you a route in the +master site. If you're inside yesod-auth, for instance, you don't +want to deal with all of the possible routes in the parent, but +instead get a route for the subsite itself.
  • If I'm generated URLs, I need some way to convert the routes for a +subsite into the parent site.

In today's Yesod, we provide these differences inside the HandlerT +type itself. This ends up adding some weird complications around +special-casing the base (and common) case where m is IO. Instead, +in the new branch, we have just one layer of ReaderT sitting on top +of HandlerFor, providing these three pieces of functionality. And if +you want to get a better view of this, +check out the code.

What to do?

Overall, I think this design is more elegant, easier to understand, +and simplifies the codebase. In reality, I don't think it's either a +major departure from the past, or a major improvement, which is what +leaves me on the fence about the no transformer changes.

We're almost certainly going to have a breaking change in Yesod in the +near future, but it need not include this change. If it doesn't, the +breaking change will be the very minor one mentioned above. If the +general consensus is in favor of this change, then we may as well +throw it in at the same time.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2019/02/deprecating-googleemail2.html b/public/blog/2019/02/deprecating-googleemail2.html new file mode 100644 index 00000000..3463de32 --- /dev/null +++ b/public/blog/2019/02/deprecating-googleemail2.html @@ -0,0 +1,30 @@ + Deprecating GoogleEmail2 +

Deprecating GoogleEmail2

February 12, 2019

GravatarBy Michael Snoyman

As I'm sure many readers are aware, Google+ is shutting down. This +affects Yesod's authentication +system. In particular, +any users of the Yesod.Auth.GoogleEmail2 module will need to +migrate.

Fortunately for all of us, Patrick Brisbin has written both the +code +for using Google Sign-in, but has also put together a great migration +blog post. If +you're affected, please check that out for a relatively painless +migration.

Earlier today, I migrated Haskellers.com in two commits: the first +did all of the +work, +and the second made things more +future-proof.

As you may be able to tell from the GoogleEmail2 module, this isn't +the first time we've had to migrate between Google authentication +APIs. Hopefully this one will stick.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2019/12/bookportuguese.html b/public/blog/2019/12/bookportuguese.html new file mode 100644 index 00000000..e10815e6 --- /dev/null +++ b/public/blog/2019/12/bookportuguese.html @@ -0,0 +1,30 @@ + A new Yesod book in Portuguese +

A new Yesod book in Portuguese

December 19, 2019

GravatarBy Alexandre Garcia de Oliveira

Yesod users,

In order to help to spread the Yesod word here in Brazil, we, Alexandre Garcia de Oliveira, Patrick Augusto da Silva (@ptkato_ on Twitter), and +Felipe Cannarozzo Lourenço, wrote a book, recently released, about Yesod +called "Yesod e Haskell: Aplicações web com Programação Funcional pura" ("Yesod and Haskell: Web +applications with pure Functional Programming", in English).

The book covers the principles of developing a web application with Yesod. Needless +to say, it follows the same model of Alexandre's first book on Haskell, giving a much needed +insight into the Yesod world within the Brazilian borders.

The book aims to allow the reader to learn Yesod from scratch, starting from the basics, like setting up +the environment using stack, and using a monolithic example in a single file, to explain the foundations of +the framework. The book goes through the Stackage's snapshots, the difference between templates and +scaffolding, Shakespearean Templates, type-safe routing, persistent basics, authentication & +authorization, finishing up with a RESTful app example.

The idea of writing a book about Yesod came to fruition during one of Alexandre's lectures on Yesod at +FATEC-Santos, when it was realized that many students didn't read English at all +and Yesod documentation in Portuguese was virtually non-existent. Not only that, +the volition to write this book became even stronger when the TAs' (Patrick and Felipe) tutoring classes were almost always spent on going through the same topics, when Portuguese documentation could have easily solved the problem.

All the pedagogic skills to write this book were founded on top of the fact that it was written in an +academic medium, considering that, it was shaped in such a way that fits the needs of an undergrad student +fairly well. Now, hundreds of FATEC-Santos alumni experienced the joy of using Yesod, Santos indeed is the +city in Brazil with the highest number of people who knows Yesod. We're committed to pushing Yesod forward through the local market, and maybe in the future, even beyond.

The book was published by "Casa do Código" and can be found here.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2020/05/blogannouncement.html b/public/blog/2020/05/blogannouncement.html new file mode 100644 index 00000000..7f05032b --- /dev/null +++ b/public/blog/2020/05/blogannouncement.html @@ -0,0 +1,15 @@ + A new Haskell/Yesod Beginners Blog +

A new Haskell/Yesod Beginners Blog

May 3, 2020

GravatarBy Alexander Nolte

This is the annoncement for a new Haskell Beginners Blog, which first series of articles will handle the building of two Yesod-projects: a personal Blog with slighly powered batteries and in addition examples of functionality from a small-buisiness-website.

As I have decided to learn Haskell late and not long ago, this Blogpost is written out of a straight programming beginners point of view, and like so will handle also prelimiaries and standarts maybe annoying to people with a different background.

I was a bit afraid to use Yesod as a first step in my learning Haskell journey - you all know it is a big, non trival framework - but somehow, after a while of proving and looking into it, I felt quite save and secure to not miss the point. I mean, moving around in it's structure felt somehow good and plausible. Hope to transport that in this Blog.

https://blog.onepigayear.de

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/blog/2021/02/chatwisely-intro.html b/public/blog/2021/02/chatwisely-intro.html new file mode 100644 index 00000000..8dc4b366 --- /dev/null +++ b/public/blog/2021/02/chatwisely-intro.html @@ -0,0 +1,23 @@ + We Made ChatWisely With Haskell +

We Made ChatWisely With Haskell

February 17, 2021

GravatarBy Michael Litchard

Three years ago, I met fellow Haskeller Brian Hurt while working with Obsidian Systems on a Reflex project. Not long after, he started telling me his ideas about how to fix social media. These ideas intrigued me, one thing led to another, and we began building ChatWisely. We thought other Haskellers might like to hear about what we’re doing and how we use Haskell to do it.

ChatWisely is a member-supported mini-blogging social network currently in open beta. We envision a place where people connect in a spirit of comradery. We see an elevated discourse and a way to show bullies the door by providing a platform to debate safely. Here’s how we’re doing it.

Safety First

+Brian and I built several mechanisms to filter out people looking for a fight or to harm others. First and foremost will be the monthly subscription fee of one dollar. We think that will discourage a large portion of toxic people. Another is a sharable mute list that we believe will help mitigate the rest. And finally, in the event of a serious Terms of Service violation, we ban payment methods rather than just accounts.

Mini-Blogging

+The big idea here is that sometimes, a short post isn’t enough. Brian and I made a way to link that short post to a longer one. So the timeline looks something like Twitter’s, but with some posts that can expand to something more detailed. These can be connected to other people’s posts to create a continuity in conversation hard to come by on other platforms. So when another member’s post inspires you to write a longer one about your experience with the ghcjs ffi (for example), you can link your post to theirs.

Ownership of Your Timeline

+Members can organize their timeline and choose to what extent they follow other people’s posts. The typical mainstream social network requires that when you follow someone you must follow everything they post, or nothing. Sure, there are filtered word lists in some cases. But none of it seems to work quite right. Instead, we have groups called caboodles that members can use to decide where other people’s posts fit, and how to share their own. So say someone likes their uncle’s cookie recipes but not his political posts. They can follow one but not the other.

Geolocated Messaging

+One day this pandemic will be over and we’ll be there to meet that day. At that time, when a member’s movie caboodle wants to organize local screenings of the latest blockbuster from the House of Mouse they can make posts visible to people in their proximity. Perhaps you want to target local Haskellers to organize a meetup, or leave them a message that pops up when they’ve found the meeting place. Also, I think running a scavenger hunt with geolocated clues sounds like a hoot.

How It’s Built

+Brian and I rely heavily on the Haskell ecosystem to build ChatWisely. Haskell’s type system reduces errors and cognitive load. GHC is our pair programming partner that tightens the delivery cycle and lets us learn how to build ChatWisely while we’re building it. Refactoring is a breeze, and unit testing is constrained to the I/O edges of the system, which means we spend less time on that and more time building the product. Here’s the principal tools we rely on to get the job done. +

+

Ghcjs

The fact is we all hate javascript. The problem is, we can’t build web apps without it. Ghcjs lets us deal with the realities of building a product that uses a web browser for a client. The FFI lets us wrap our hand-written javascript in Haskell which helps to keep that part of the codebase pretty small.. We especially love what is built on top of that, Reflex-Dom.

Reflex-Dom

Reflex helps us build a system that needs to adapt to changes in data, requirements and platform. We’re learning how to build ChatWisely as we build it, and reflex keeps up with our changing ideas on how to do that. Our first app store product will be a PWA, delivered with reflex.

Servant

Servant delivers the API, and requires us to separate definition from implementation. This helps us keep the backend from turning into a big ball of mud. We can auto-generate clients, which we currently use in unit testing. They even have a way to generate a reflex client, and we’ll be adding that in the near future.

Yesod

We use Yesod for marketing tasks, which largely require static pages. Currently it handles our landing page and unsubscribe mechanism for our emails. The Yesod Widget is a monoid, and therefore composable, which makes structuring html simple.

Three Reasons Why People Should Use ChatWisely.

+

We are member supported

We won’t have ads, which means we have no need to manipulate people’s timelines in order to serve those ads. Their timeline should be about conversations of interest.

We solve the tweet thread problem

Brian and I find tweet threads hard to follow. Our mini-blog looks like twitter in the sense that you get a timeline of short posts. However if a post is the beginning of something more developed, that message can open up to access it.

Keep the RealWorld conversation going

We have delayed the development of these features for obvious reasons. But one day we’ll be together again. By then we’ll have useful geo-location tools for conference attendees and speakers to continue the conversation.

What makes a weekend conference fun for me are the conversations in-between formal talks. I get all caught up in compelling conversations, and want to keep that going. We’ll have a way to do that, without having to know or remember anyone’s email address or phone number.

Conference speakers will often want to build on the momentum gathered after a successful talk. Brian and I think Twitter hashtags are the terrible but often only way to do this. We’ll have a way to use proximity and common interests to help build that momentum and keep everyone engaged.


We built ChatWisely as a response to the unpleasantness all too common on mainstream social networks. Depending on our membership for support creates the place we want to meet because ad revenue and data-mining motivates engagement, not conversations and connection. No one is fooled by what mainstream social networks call engagement because that looks to us like derailed conversations, confusing timelines we only have a shallow control over, and unsafe situations.

Brian and I love the daily experience of building ChatWisely, the Haskell ecosystem brings joy to the experience of running our startup. You can support us on patreon and should come by and test the beta. We look forward to hearing from you about any ideas or questions you may have.

Comments

comments powered by Disqus

Archives

+
\ No newline at end of file diff --git a/public/book-1.1.html b/public/book-1.1.html new file mode 100644 index 00000000..c2f6c399 --- /dev/null +++ b/public/book-1.1.html @@ -0,0 +1,160 @@ + Yesod Web Framework Book- Version 1.1 +

Available from O'Reilly:

+
Developing Web Applications with Haskell and Yesod + +
+
+

The current version of the book +covers Yesod 1.6. We also maintain older copies for +version 1.4, version 1.2 +, and +version 1.1.

+ +
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/authentication-and-authorization.html b/public/book-1.1/authentication-and-authorization.html new file mode 100644 index 00000000..29eca720 --- /dev/null +++ b/public/book-1.1/authentication-and-authorization.html @@ -0,0 +1,614 @@ + Authentication and Authorization :: Yesod Web Framework Book- Version 1.1 +
+

Authentication and Authorization

+ + +

+

+

Authentication and authorization are two very related, and yet separate, concepts. While + the former deals with identifying a user, the latter determines what a user is allowed + to do. Unfortunately, since both terms are often abbreviated as "auth," the concepts are + often conflated.

+

Yesod provides built-in support for a number of third-party authentication systems, such + as OpenID, BrowserID and OAuth. These are systems where your application trusts some + external system for validating a user's credentials. Additionally, there is support for + more commonly used username/password and email/password systems. The former route + ensures simplicity for users (no new passwords to remember) and implementors (no need to + deal with an entire security architecture), while the latter gives the developer more + control.

+

On the authorization side, we are able to take advantage of REST and type-safe + URLs to create simple, declarative systems. Additionally, since all authorization code + is written in Haskell, you have the full flexibility of the language at your + disposal.

+

This chapter will cover how to set up an "auth" solution in Yesod and discuss some + trade-offs in the different authentication options.

+
+

Overview

+

+

The yesod-auth package provides a unified interface for a + number of different authentication plugins. The only real requirement for these backends is that + they identify a user based on some unique string. In OpenID, for instance, this would be the + actual OpenID value. In BrowserID, it's the email address. For HashDB (which uses a database of + hashed passwords), it's the username.

+

Each authentication plugin provides its own system for logging in, whether it be via + passing tokens with an external site or a email/password form. After a successful login, the + plugin sets a value in the user's session to indicate his/her AuthId. This + AuthId is usually a Persistent ID from a table used for keeping track of + users.

+

There are a few functions available for querying a user's AuthId, + most commonly maybeAuthId, requireAuthId, + maybeAuth and requireAuth. The require + versions will redirect to a login page if the user is not logged in, while the second set of + functions (the ones not ending in Id) give both the table ID and entity value.

+

Since all of the storage of AuthId is built on top of sessions, all + of the rules from there apply. In particular, the data is stored in an encrypted, HMACed client + cookie, which automatically times out after a certain configurable period of inactivity. + Additionally, since there is no server-side component to sessions, logging out simply deletes the + data from the session cookie; if a user reuses an older cookie value, the session will still be + valid.

+ +

On the flip side, authorization is handled by a few methods inside the + Yesod typeclass. For every request, these methods are run to determine if + access should be allowed, denied, or if the user needs to be authenticated. By default, these + methods allow access for every request. Alternatively, you can implement authorization in a more + ad-hoc way by adding calls to requireAuth and the like within individual handler + functions, though this undermines many of the benefits of a declarative authorization system.

+
+
+

Authenticate Me

+

+

Let's jump right in with an example of authentication.

+
{-# LANGUAGE OverloadedStrings, TemplateHaskell, TypeFamilies,
+             MultiParamTypeClasses, QuasiQuotes #-}
+import Yesod
+import Yesod.Auth
+import Yesod.Auth.BrowserId
+import Yesod.Auth.GoogleEmail
+import Data.Text (Text)
+import Network.HTTP.Conduit (Manager, newManager, def)
+
+data MyAuthSite = MyAuthSite
+    { httpManager :: Manager
+    }
+
+mkYesod "MyAuthSite" [parseRoutes|
+/ RootR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod MyAuthSite where
+    -- Note: In order to log in with BrowserID, you must correctly
+    -- set your hostname here.
+    approot = ApprootStatic "http://localhost:3000"
+
+instance YesodAuth MyAuthSite where
+    type AuthId MyAuthSite = Text
+    getAuthId = return . Just . credsIdent
+
+    loginDest _ = RootR
+    logoutDest _ = RootR
+
+    authPlugins _ =
+        [ authBrowserId
+        , authGoogleEmail
+        ]
+
+    authHttpManager = httpManager
+
+instance RenderMessage MyAuthSite FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getRootR :: Handler RepHtml
+getRootR = do
+    maid <- maybeAuthId
+    defaultLayout [whamlet|
+<p>Your current auth ID: #{show maid}
+$maybe _ <- maid
+    <p>
+        <a href=@{AuthR LogoutR}>Logout
+$nothing
+    <p>
+        <a href=@{AuthR LoginR}>Go to the login page
+|]
+
+main :: IO ()
+main = do
+    man <- newManager def
+    warpDebug 3000 $ MyAuthSite man
+

We'll start with the route declarations. First we declare our standard RootR + route, and then we set up the authentication subsite. Remember that a subsite needs four + parameters: the path to the subsite, the route name, the subsite name, and a function to get the + subsite value. In other words, based on the line:

+
/auth AuthR Auth getAuth
+

We need to have getAuth :: MyAuthSite -> Auth. While we haven't written that + function ourselves, yesod-auth provides it automatically. With other subsites + (like static files), we provide configuration settings in the subsite value, and therefore need + to specify the get function. In the auth subsite, we specify these settings in a separate + typeclass, YesodAuth.

+ +

So what exactly goes in this YesodAuth instance? There are six required + declarations:

+
    +
  • +

    + AuthId is an associated type. This is the value + yesod-auth will give you when you ask if a user is logged in (via + maybeAuthId or requireAuthId). In our case, we're simply + using Text, to store the raw identifier- email address in our case, as we'll + soon see.

    +
  • +
  • +

    + getAuthId gets the actual AuthId from the Creds (credentials) data type. This type has three pieces of information: + the authentication backend used (browserid or googleemail in our case), the actual identifier, + and an associated list of arbitrary extra information. Each backend provides different extra + information; see their docs for more information.

    +
  • +
  • +

    + loginDest gives the route to redirect to after a successful + login.

    +
  • +
  • +

    Likewise, logoutDest gives the route to redirect to after a + logout.

    +
  • +
  • +

    + authPlugins is a list of individual authentication backends to + use. In our example, we're using BrowserID, which logs in via Mozilla's BrowserID system, and + Google Email, which authenticates a user's email address using their Google account. The nice + thing about these two backends is:

      +
    • +

      They require no set up, as opposed to Facebook or OAuth, which require setting up + credentials.

      +
    • +
    • +

      They use email addresses as identifiers, which people are comfortable with, as + opposed to OpenID, which uses a URL.

      +
    • +
    +

    +
  • +
  • +

    + authHttpManager gets an HTTP connection manager from the foundation type. + This allow authentication backends which use HTTP connections (i.e., almost all third-party + login systems) to share connections, avoiding the cost of restarting a TCP connection for each + request.

    +
  • +
+

In our RootR handler, we have some simple links to the login and + logout pages, depending on whether or not the user is logged in. Notice how we construct these + subsite links: first we give the subsite route name (AuthR), followed by the + route within the subsite (LoginR and LogoutR).

+

The figures below show what the login process looks like from a user perspective.

+
+

Initial page load

+ + + + + +
+
+

BrowserID login screen

+ + + + + +
+
+

Homepage after logging in

+ + + + + +
+
+
+

Email

+

+

For many use cases, third-party authentication of email will be sufficient. Occassionally, + you'll want users to actual create passwords on your site. The scaffolded site does not include + this setup, because:

+
    +
  • +

    In order to securely accept passwords, you need to be running over SSL. Many users are not + serving their sites over SSL.

    +
  • +
  • +

    While the email backend properly salts and hashes passwords, a compromised database could + still be problematic. Again, we make no assumptions that Yesod users are following secure + deployment practices.

    +
  • +
  • +

    You need to have a working system for sending email. Many web servers these days are + not equipped to deal with all of the spam protection measures used by mail + servers.

    +

    +
  • +
+

But assuming you are able to meet these demands, and you want to have a separate password login + specifically for your site, Yesod offers a built-in backend. It requires quite a bit of code to + set up, since it needs to store passwords securely in the database and send a number of different + emails to users (verify account, password retrieval, etc.).

+

Let's have a look at a site that provides email authentication, storing passwords in a + Persistent SQLite database.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes, GADTs,
+             TemplateHaskell, MultiParamTypeClasses, FlexibleContexts #-}
+import Yesod
+import Yesod.Auth
+import Yesod.Auth.Email
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Text (Text)
+import Network.Mail.Mime
+import qualified Data.Text.Lazy.Encoding
+import Text.Shakespeare.Text (stext)
+import Text.Blaze.Renderer.Utf8 (renderHtml)
+import Text.Hamlet (shamlet)
+import Data.Maybe (isJust)
+import Control.Monad (join)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
+User
+    email Text
+    password Text Maybe -- Password may not be set yet
+    verkey Text Maybe -- Used for resetting passwords
+    verified Bool
+    UniqueUser email
+|]
+
+data MyEmailApp = MyEmailApp Connection
+
+mkYesod "MyEmailApp" [parseRoutes|
+/ RootR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod MyEmailApp where
+    -- Emails will include links, so be sure to include an approot so that
+    -- the links are valid!
+    approot = ApprootStatic "http://localhost:3000"
+
+instance RenderMessage MyEmailApp FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Set up Persistent
+instance YesodPersist MyEmailApp where
+    type YesodPersistBackend MyEmailApp = SqlPersist
+    runDB f = do
+        MyEmailApp conn <- getYesod
+        runSqlConn f conn
+
+instance YesodAuth MyEmailApp where
+    type AuthId MyEmailApp = UserId
+
+    loginDest _ = RootR
+    logoutDest _ = RootR
+    authPlugins _ = [authEmail]
+
+    -- Need to find the UserId for the given email address.
+    getAuthId creds = runDB $ do
+        x <- insertBy $ User (credsIdent creds) Nothing Nothing False
+        return $ Just $
+            case x of
+                Left (Entity userid _) -> userid -- newly added user
+                Right userid -> userid -- existing user
+
+    authHttpManager = error "Email doesn't need an HTTP manager"
+
+-- Here's all of the email-specific code
+instance YesodAuthEmail MyEmailApp where
+    type AuthEmailId MyEmailApp = UserId
+
+    addUnverified email verkey =
+        runDB $ insert $ User email Nothing (Just verkey) False
+
+    sendVerifyEmail email _ verurl =
+        liftIO $ renderSendMail (emptyMail $ Address Nothing "noreply")
+            { mailTo = [Address Nothing email]
+            , mailHeaders =
+                [ ("Subject", "Verify your email address")
+                ]
+            , mailParts = [[textPart, htmlPart]]
+            }
+      where
+        textPart = Part
+            { partType = "text/plain; charset=utf-8"
+            , partEncoding = None
+            , partFilename = Nothing
+            , partContent = Data.Text.Lazy.Encoding.encodeUtf8 [stext|
+Please confirm your email address by clicking on the link below.
+
+\#{verurl}
+
+Thank you
+|]
+            , partHeaders = []
+            }
+        htmlPart = Part
+            { partType = "text/html; charset=utf-8"
+            , partEncoding = None
+            , partFilename = Nothing
+            , partContent = renderHtml [shamlet|
+<p>Please confirm your email address by clicking on the link below.
+<p>
+    <a href=#{verurl}>#{verurl}
+<p>Thank you
+|]
+            , partHeaders = []
+            }
+    getVerifyKey = runDB . fmap (join . fmap userVerkey) . get
+    setVerifyKey uid key = runDB $ update uid [UserVerkey =. Just key]
+    verifyAccount uid = runDB $ do
+        mu <- get uid
+        case mu of
+            Nothing -> return Nothing
+            Just _ -> do
+                update uid [UserVerified =. True]
+                return $ Just uid
+    getPassword = runDB . fmap (join . fmap userPassword) . get
+    setPassword uid pass = runDB $ update uid [UserPassword =. Just pass]
+    getEmailCreds email = runDB $ do
+        mu <- getBy $ UniqueUser email
+        case mu of
+            Nothing -> return Nothing
+            Just (Entity uid u) -> return $ Just EmailCreds
+                { emailCredsId = uid
+                , emailCredsAuthId = Just uid
+                , emailCredsStatus = isJust $ userPassword u
+                , emailCredsVerkey = userVerkey u
+                }
+    getEmail = runDB . fmap (fmap userEmail) . get
+
+getRootR :: Handler RepHtml
+getRootR = do
+    maid <- maybeAuthId
+    defaultLayout [whamlet|
+<p>Your current auth ID: #{show maid}
+$maybe _ <- maid
+    <p>
+        <a href=@{AuthR LogoutR}>Logout
+$nothing
+    <p>
+        <a href=@{AuthR LoginR}>Go to the login page
+|]
+
+main :: IO ()
+main = withSqliteConn "email.db3" $ \conn -> do
+    runSqlConn (runMigration migrateAll) conn
+    warpDebug 3000 $ MyEmailApp conn
+
+
+

Authorization

+

+

Once you can authenticate your users, you can use their credentials to authorize requests. Authorization in Yesod is simple and declarative: most of + the time, you just need to add the authRoute and + isAuthorized methods to your Yesod typeclass instance. Let's see an + example.

+
{-# LANGUAGE OverloadedStrings, TemplateHaskell, TypeFamilies,
+             MultiParamTypeClasses, QuasiQuotes #-}
+import Yesod
+import Yesod.Auth
+import Yesod.Auth.Dummy -- just for testing, don't use in real life!!!
+import Data.Text (Text)
+import Network.HTTP.Conduit (Manager, newManager, def)
+
+data MyAuthSite = MyAuthSite
+    { httpManager :: Manager
+    }
+
+mkYesod "MyAuthSite" [parseRoutes|
+/ RootR GET POST
+/admin AdminR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod MyAuthSite where
+    authRoute _ = Just $ AuthR LoginR
+
+    -- route name, then a boolean indicating if it's a write request
+    isAuthorized RootR True = isAdmin
+    isAuthorized AdminR _ = isAdmin
+
+    -- anyone can access other pages
+    isAuthorized _ _ = return Authorized
+
+isAdmin = do
+    mu <- maybeAuthId
+    return $ case mu of
+        Nothing -> AuthenticationRequired
+        Just "admin" -> Authorized
+        Just _ -> Unauthorized "You must be an admin"
+
+instance YesodAuth MyAuthSite where
+    type AuthId MyAuthSite = Text
+    getAuthId = return . Just . credsIdent
+
+    loginDest _ = RootR
+    logoutDest _ = RootR
+
+    authPlugins _ = [authDummy]
+
+    authHttpManager = httpManager
+
+instance RenderMessage MyAuthSite FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getRootR :: Handler RepHtml
+getRootR = do
+    maid <- maybeAuthId
+    defaultLayout [whamlet|
+<p>Note: Log in as "admin" to be an administrator.
+<p>Your current auth ID: #{show maid}
+$maybe _ <- maid
+    <p>
+        <a href=@{AuthR LogoutR}>Logout
+<p>
+    <a href=@{AdminR}>Go to admin page
+<form method=post>
+    Make a change (admins only)
+    \ #
+    <input type=submit>
+|]
+
+postRootR :: Handler ()
+postRootR = do
+    setMessage "You made some change to the page"
+    redirect RootR
+
+getAdminR :: Handler RepHtml
+getAdminR = defaultLayout [whamlet|
+<p>I guess you're an admin!
+<p>
+    <a href=@{RootR}>Return to homepage
+|]
+
+main :: IO ()
+main = do
+    manager <- newManager def
+    warpDebug 3000 $ MyAuthSite manager
+

+ authRoute should be your login page, almost always + AuthR + LoginR. isAuthorized is a function that takes two + parameters: the requested route, and whether or not the request was a "write" request. + You can actually change the meaning of what a write request is using the + isWriteRequest method, but the out-of-the-box version follows + RESTful principles: anything but a GET, HEAD, + OPTIONS or TRACE request is a write request.

+

What's convenient about the body of isAuthorized is that you can run + any Handler code you want. This means you can:

+
    +
  • +

    Access the filesystem (normal IO)

    +
  • +
  • +

    Lookup values in the database

    +
  • +
  • +

    Pull any session or request values you want

    +
  • +
+

Using these techniques, you can develop as sophisticated an authorization system as you like, + or even tie into existing systems used by your organization.

+
+
+

Conclusion

+

+

This chapter covered the basics of setting up user authentication, as well as how the built-in + authorization functions provide a simple, declarative approach for users. While these are + complicated concepts, with many approaches, Yesod should provide you with the building blocks you + need to create your own customized auth solution.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/basics.html b/public/book-1.1/basics.html new file mode 100644 index 00000000..74a68105 --- /dev/null +++ b/public/book-1.1/basics.html @@ -0,0 +1,387 @@ + Basics :: Yesod Web Framework Book- Version 1.1 +
+

Basics

+ + +

+

+

The first step with any new technology is getting it running. The goal of this chapter is + to get you started with a simple Yesod application, and cover some of the basic concepts + and terminology.

+
+

Hello World

+

+

Let's get this book started properly: a simple web page that says Hello World:

+
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
+             TemplateHaskell, OverloadedStrings #-}
+import Yesod
+
+data HelloWorld = HelloWorld
+
+mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod HelloWorld
+
+getHomeR :: Handler RepHtml
+getHomeR = defaultLayout [whamlet|Hello World!|]
+
+main :: IO ()
+main = warpDebug 3000 HelloWorld
+

If you save that code in helloworld.hs and run it with runhaskell helloworld.hs, you'll get a web server running on port 3000. If you point your browser to http://localhost:3000, you'll get the following HTML:

+
<!DOCTYPE html>
+<html><head><title></title></head><body>Hello World!</body></html>
+

We'll refer back to this example through the rest of the chapter.

+
+
+

Routing

+

+

Like most modern web frameworks, Yesod follows a front controller pattern. This means that every request to a + Yesod application enters at the same point and is routed from there. As a contrast, in + systems like PHP and ASP you usually create a number of different files, and the web + server automatically directs requests to the relevant file.

+

In addition, Yesod uses a declarative style for specifying routes. In our example above, + this looked like:

+
mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+ +

In English, all this means is: In the HelloWorld application, create + one route. I'd like to call it HomeR, it should listen for requests + to / (the root of the application), and should answer + GET requests. We call HomeR a resource, which is where the "R" suffix comes from.

+

+

The mkYesod TH function generates quite a bit of code here: a route data + type, a dispatch function, and a render function. We'll look at this in more detail in + the routing chapter. But by using the + -ddump-splices GHC option, we can get an immediate look at the + generated code. A much cleaned up version of it is:

+
instance RenderRoute HelloWorld where
+  data Route HelloWorld = HomeR
+    deriving (Show, Eq, Read)
+  renderRoute HomeR = ([], [])
+
+instance YesodDispatch HelloWorld HelloWorld where
+    yesodDispatch master sub toMaster app404 app405 method pieces =
+        case dispatch pieces of
+            Just f -> f
+                master
+                sub
+                toMaster
+                app404
+                app405
+                method
+            Nothing -> app404
+      where
+        dispatch = Yesod.Routes.Dispatch.toDispatch
+            [ Yesod.Routes.Dispatch.Route [] False onHome
+            ]
+        onHome [] = Just $ \master sub toMaster _app404 app405 method ->
+            case method of
+                "GET" -> yesodRunner
+                    (fmap chooseRep getHomeR)
+                    master
+                    sub
+                    (Just HomeR)
+                    toMaster
+                _ -> app405 HomeR
+
+

Some of that will likely not make sense yet. In particular, the implementation of + yesodDispatch is a bit hairy to accomodate different dispatch + approaches and fit the model necessary for our high-performance routing structures. + However, the RenderRoute implementation with its associated data type + should already give you a good feel for what's going on under the surface.

+
+
+

Handler function

+

+

So we have a route named HomeR, and it responds to + GET requests. How do you define your response? You write a handler function. Yesod follows a standard naming scheme for these + functions: it's the lower case method name (e.g., GET becomes + get) followed by the route name. In this case, the function name + would be getHomeR.

+

Most of the code you write in Yesod lives in handler functions. This is where you + process user input, perform database queries and create responses. In our simple + example, we create a response using the defaultLayout + function. This function wraps up the content it's given in your site's template. By + default, it produces an HTML file with a doctype and html, + head and body tags. As we'll + see in the Yesod typeclass + chapter, this function can be overridden to do much more.

+

In our example, we pass [whamlet|Hello World!|] to + defaultLayout. whamlet is another quasi-quoter. In + this case, it converts Hamlet syntax into a Widget. Hamlet is the default HTML + templating engine in Yesod. Together with its siblings Cassius, Lucius and Julius, you + can create HTML, CSS and Javascript in a fully type-safe and compile-time-checked + manner. We'll see much more about this in the Shakespeare chapter.

+

Widgets are another cornerstone of Yesod. They allow you to create modular + components of a site consisting of HTML, CSS and Javascript and reuse them throughout + your site. We'll get into more detail on them in the widgets + chapter.

+
+
+

The Foundation

+

+

The word "HelloWorld" shows up a number of times in our example. Every Yesod + application has a foundation datatype. This datatype must be an instance of + the Yesod typeclass, which provides a central place for declaring a number + of different settings controlling the execution of our application.

+

In our case, this datatype is pretty boring: it doesn't contain any information. + Nonetheless, the foundation is central to how our example runs: it ties together the + routes with the instance declaration and lets it all be run. We'll see throughout this + book that the foundation pops up in a whole bunch of places.

+

But foundations don't have to be boring: they can be used to store lots of useful + information, usually stuff that needs to be initialized at program launch and used + throughout. Some very common examples are:

+
    +
  • +

    A database connection pool.

    +
  • +
  • +

    Settings loaded from a config file.

    +
  • +
  • +

    An HTTP connection manager.

    +
  • +
+ +
+
+

Running

+

+

Once again we mention HelloWorld in our main function. Our + foundation contains all the information we need to route and respond to requests in our + application; now we just need to convert it into something that can run. A useful + function for this in Yesod is warpDebug, which runs the Warp webserver + with debug output enabled on the specified port (here, it's 3000).

+

One of the features of Yesod is that you aren't tied down to a single deployment + strategy. Yesod is built on top of the Web Application Interface (WAI), allowing it to + run on FastCGI, SCGI, Warp, or even as a desktop application using the Webkit library. + We'll discuss some of these options in the deployment + chapter. And at the end of this chapter, we will explain the development server.

+

Warp is the premiere deployment option for Yesod. It is a lightweight, highly efficient + web server developed specifically for hosting Yesod. It is also used outside of Yesod + for other Haskell development (both framework and non-framework applications), as well + as a standard file server in a number of production environments.

+
+
+

Resources and type-safe URLs

+

+

In our hello world, we defined just a single resource (HomeR). A + web application is usually much more exciting with more than one page on it. Let's take + a look:

+
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
+             TemplateHaskell, OverloadedStrings #-}
+import Yesod
+
+data Links = Links
+
+mkYesod "Links" [parseRoutes|
+/ HomeR GET
+/page1 Page1R GET
+/page2 Page2R GET
+|]
+
+instance Yesod Links
+
+getHomeR  = defaultLayout [whamlet|<a href=@{Page1R}>Go to page 1!|]
+getPage1R = defaultLayout [whamlet|<a href=@{Page2R}>Go to page 2!|]
+getPage2R = defaultLayout [whamlet|<a href=@{HomeR}>Go home!|]
+
+main = warpDebug 3000 Links
+

Overall, this is very similar to Hello World. Our foundation is now + Links instead of HelloWorld, and in addition to + the HomeR resource, we've added Page1R and + Page2R. As such, we've also added two more handler functions: + getPage1R and getPage2R.

+

The only truly new feature is inside the whamlet + quasi-quotation. We'll delve into syntax in the Shakespeare chapter, but we can see that:

+
<a href=@{Page1R}>Go to page 1!
+

creates a link to the Page1R resource. The important thing to + note here is that Page1R is a data constructor. By making each resource + a data constructor, we have a feature called type-safe URLs. + Instead of splicing together strings to create URLs, we simply create a plain old + Haskell value. By using at-sign interpolation (@{...}), Yesod + automatically renders those values to textual URLs before sending things off to the + user. We can see how this is implemented by looking again at the + -ddump-splices output:

+
instance RenderRoute Links where
+    data Route Links = HomeR | Page1R | Page2R
+      deriving (Show, Eq, Read)
+
+    renderRoute HomeR  = ([], [])
+    renderRoute Page1R = (["page1"], [])
+    renderRoute Page2R = (["page2"], [])
+
+

In the Route associated type for Links, we have + additional constructors for Page1R and Page2R. We also + now have a better glimpse of the return values for returnRoute. The + first part of the tuple gives the path pieces for the given route. The + second part gives the query string parameters; for almost all use cases, this will be an + empty list.

+

It's hard to over-estimate the value of type-safe URLs. They give you a huge + amount of flexibility and robustness when developing your application. You can move URLs + around at will without ever breaking links. In the routing chapter, we'll see that routes can take + parameters, such as a blog entry URL taking the blog post ID.

+

Let's say you want to switch from routing on the numerical post ID to a + year/month/slug setup. In a traditional web framework, you would need to go through + every single reference to your blog post route and update appropriately. If you miss + one, you'll have 404s at runtime. In Yesod, all you do is update your route and compile: + GHC will pinpoint every single line of code that needs to be corrected.

+
+
+

The scaffolded site

+

+

Installing Yesod will give you both the Yesod library, as well as a yesod executable. This executable accepts a few commands, but the first one you'll + want to be acquainted with is yesod init. It will ask you some + questions, and then generate a folder containing the default scaffolded + site. Inside that folder, you can run cabal install + --only-dependencies to build any extra dependencies (such as your database + backends), and then yesod devel to run your site.

+

The scaffolded site gives you a lot of best practices out of the box, setting up files and + dependencies in a time-tested approach used by most production Yesod sites. However, all this + convenience can get in the way of actually learning Yesod. Therefore, most of this book will + avoid the scaffolding tool, and instead deal directly with Yesod as a library.

+

We will cover the structure of the scaffolded site in more detail later.

+
+
+

Development server

+

+

One of the advantages interpreted languages have over compiled languages + is fast prototyping: you save changes to a file and hit refresh. If we want to make any + changes to our Yesod apps above, we'll need to call runhaskell from + scratch, which can be a bit tedious.

+

Fortunately, there's a solution to this: yesod + devel automatically rebuilds and reloads your code for you. This can be a + great way to develop your Yesod projects, and when you're ready to move to production, + you still get to compile down to incredibly efficient code. The Yesod scaffolding + automatically sets things up for you. This gives you the best of both worlds: rapid + prototyping and fast production code.

+

It's a little bit more involved to set up your code to be used by + yesod devel, so our examples will just use + warpDebug. But when you're ready to make your real-world + applications, yesod devel will be waiting for you.

+
+
+

Summary

+

+

Every Yesod application is built around a foundation datatype. We associate some + resources with that datatype and define some handler functions, and Yesod handles all of + the routing. These resources are also data constructors, which lets us have type-safe + URLs.

+

By being built on top of WAI, Yesod applications can run with a number of different + backends. warpDebug is an easy way to get started, as it's included + with Yesod. For rapid development, you can use yesod devel is a good + choice. And when you're ready to move to production, you have Warp as a high-performance + option.

+

When developing in Yesod, we get a number of choices for coding style: + quasi-quotation or external files, warpDebug or yesod + devel, and so on. The examples in this book will tend towards using the + choices that are easiest to copy-and-paste, but the more powerful options will be + available when you start building real Yesod applications.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/blog-example-advanced.html b/public/book-1.1/blog-example-advanced.html new file mode 100644 index 00000000..a67c9d3f --- /dev/null +++ b/public/book-1.1/blog-example-advanced.html @@ -0,0 +1,546 @@ + Blog: i18n, authentication, authorization, and database :: Yesod Web Framework Book- Version 1.1 +
+

Blog: i18n, authentication, authorization, and database

+ + +

+

+
This is a simple blog app. It allows an admin to add blog posts via a rich text
+editor (nicedit), allows logged-in users to comment, and has full i18n support.
+It is also a good example of using a Persistent database, leveraging Yesod's
+authorization system, and templates.
+
+While in general we recommend placing templates, Persist entity definitions,
+and routing in separate files, we'll keep it all in one file here for
+convenience. The one exception you'll see below will be i18n messages.
+
+We'll start off with our language extensions. In scaffolded code, the language
+extensions are specified in the cabal file, so you won't need to put this in
+your individual Haskell files.
+
+> {-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+>              TemplateHaskell, GADTs, FlexibleContexts,
+>              MultiParamTypeClasses #-}
+
+Now our imports.
+
+> import Yesod
+> import Yesod.Auth
+> import Yesod.Form.Nic (YesodNic, nicHtmlField)
+> import Yesod.Auth.BrowserId (authBrowserId)
+> import Data.Text (Text)
+> import Network.HTTP.Conduit (Manager, newManager, def)
+> import Database.Persist.Sqlite
+>     ( ConnectionPool, SqlPersist, runSqlPool, runMigration
+>     , createSqlitePool
+>     )
+> import Data.Time (UTCTime, getCurrentTime)
+> import Control.Applicative ((<$>), (<*>), pure)
+
+First we'll set up our Persistent entities. We're going to both create our data
+types (via mkPersist) and create a migration function, which will automatically
+create and update our SQL schema. If you were using the MongoDB backend,
+migration would not be needed.
+
+> share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+
+Keeps track of users. In a more robust application, we would also keep account
+creation date, display name, etc.
+
+> User
+>    email Text
+>    UniqueUser email
+
+An individual blog entry (I've avoided using the word "post" due to the
+confusion with the request method POST).
+
+> Entry
+>    title Text
+>    posted UTCTime
+>    content Html
+
+We need to tack on this "deriving" line since Html doesn't specify instances
+for Read, Show or Eq. If you get an error message about "cannot derive" in your
+    own code, try adding the deriving statement.
+
+>    deriving
+
+And a comment on the blog post.
+
+> Comment
+>    entry EntryId
+>    posted UTCTime
+>    user UserId
+>    name Text
+>    text Textarea
+> |]
+
+Every site has a foundation datatype. This value is initialized before
+launching your application, and is available throughout. We'll store a database
+connection pool and HTTP connection manager in ours. See the very end of this
+file for how those are initialized.
+
+> data Blog = Blog
+>    { connPool :: ConnectionPool
+>    , httpManager :: Manager
+>    }
+
+To make i18n easy and translator friendly, we have a special file format for
+translated messages. There is a single file for each language, and each file is
+named based on the language code (e.g., en, es, de-DE) and placed in that
+folder. We also specify the main language file (here, "en") as a default
+language.
+
+> mkMessage "Blog" "../messages-blog" "en"
+
+Our en message file contains the following content:
+
+    NotAnAdmin: You must be an administrator to access this page.
+
+    WelcomeHomepage: Welcome to the homepage
+    SeeArchive: See the archive
+
+    NoEntries: There are no entries in the blog
+    LoginToPost: Admins can login to post
+    NewEntry: Post to blog
+    NewEntryTitle: Title
+    NewEntryContent: Content
+
+    PleaseCorrectEntry: Your submitted entry had some errors, please correct and try again.
+    EntryCreated title@Text: Your new blog post, #{title}, has been created
+
+    EntryTitle title@Text: Blog post: #{title}
+    CommentsHeading: Comments
+    NoComments: There are no comments
+    AddCommentHeading: Add a Comment
+    LoginToComment: You must be logged in to comment
+    AddCommentButton: Add comment
+
+    CommentName: Your display name
+    CommentText: Comment
+    CommentAdded: Your comment has been added
+    PleaseCorrectComment: Your submitted comment had some errors, please correct and try again.
+
+    HomepageTitle: Yesod Blog Demo
+    BlogArchiveTitle: Blog Archive
+
+Now we're going to set up our routing table. We have four entries: a homepage,
+an entry list page (BlogR), an individual entry page (EntryR) and our
+authentication subsite. Note that BlogR and EntryR both accept GET and POST
+methods. The POST methods are for adding a new blog post and adding a new
+comment, respectively.
+
+> mkYesod "Blog" [parseRoutes|
+> / RootR GET
+> /blog BlogR GET POST
+> /blog/#EntryId EntryR GET POST
+> /auth AuthR Auth getAuth
+> |]
+
+Every foundation needs to be an instance of the Yesod typeclass. This is where
+we configure various settings.
+
+> instance Yesod Blog where
+
+The base of our application. Note that in order to make BrowserID work
+properly, this must be a valid URL.
+
+>     approot = ApprootStatic "http://localhost:3000"
+
+Our authorization scheme. We want to have the following rules:
+
+* Only admins can add a new entry.
+* Only logged in users can add a new comment.
+* All other pages can be accessed by anyone.
+
+We set up our routes in a RESTful way, where the actions that could make
+changes are always using a POST method. As a result, we can simply check for
+whether or not a request is a write request, given by the True in the second
+field.
+
+First, we'll authorize requests to add a new entry.
+
+>     isAuthorized BlogR True = do
+>         mauth <- maybeAuth
+>         case mauth of
+>             Nothing -> return AuthenticationRequired
+>             Just (Entity _ user)
+>                 | isAdmin user -> return Authorized
+>                 | otherwise    -> unauthorizedI MsgNotAnAdmin
+
+Now we'll authorize requests to add a new comment.
+
+>     isAuthorized (EntryR _) True = do
+>         mauth <- maybeAuth
+>         case mauth of
+>             Nothing -> return AuthenticationRequired
+>             Just _  -> return Authorized
+
+And for all other requests, the result is always authorized.
+
+>     isAuthorized _ _ = return Authorized
+
+Where a user should be redirected to if they get an AuthenticationRequired.
+
+>     authRoute _ = Just (AuthR LoginR)
+
+This is where we define our site look-and-feel. The function is given the
+content for the individual page, and wraps it up with a standard template.
+
+>     defaultLayout inside = do
+
+Yesod encourages the get-following-post pattern, where after a POST, the user
+is redirected to another page. In order to allow the POST page to give the user
+some kind of feedback, we have the getMessage and setMessage functions. It's a
+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
+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
+CSS for all pages. We'll use the Lucius template language to create the latter.
+
+>         pc <- widgetToPageContent $ do
+>             toWidget [lucius|
+> body {
+>     width: 760px;
+>     margin: 1em auto;
+>     font-family: sans-serif;
+> }
+> textarea {
+>     width: 400px;
+>     height: 200px;
+> }
+> #message {
+>   color: #900;
+> }
+> |]
+>             inside
+
+And finally we'll use a new Hamlet template to wrap up the individual
+components (title, head data and body data) into the final output.
+
+>         hamletToRepHtml [hamlet|
+> $doctype 5
+> <html>
+>     <head>
+>         <title>#{pageTitle pc}
+>         ^{pageHead pc}
+>     <body>
+>         $maybe msg <- mmsg
+>             <div #message>#{msg}
+>         ^{pageBody pc}
+> |]
+
+This is a simple function to check if a user is the admin. In a real
+application, we would likely store the admin bit in the database itself, or
+check with some external system. For now, I've just hard-coded my own email
+address.
+
+> isAdmin :: User -> Bool
+> isAdmin user = userEmail user == "michael@snoyman.com"
+
+In order to access the database, we need to create a YesodPersist instance,
+which says which backend we're using and how to run an action.
+
+> instance YesodPersist Blog where
+>    type YesodPersistBackend Blog = SqlPersist
+>    runDB f = do 
+>        master <- getYesod
+>        let pool = connPool master
+>        runSqlPool f pool
+
+This is a convenience synonym. It is defined automatically for you in the
+scaffolding.
+
+> type Form x = Html -> MForm Blog Blog (FormResult x, Widget)
+
+In order to use yesod-form and yesod-auth, we need an instance of RenderMessage
+for FormMessage. This allows us to control the i18n of individual form
+messages.
+
+> instance RenderMessage Blog FormMessage where
+>     renderMessage _ _ = defaultFormMessage
+
+In order to use the built-in nic HTML editor, we need this instance. We just
+take the default values, which use a CDN-hosted version of Nic.
+
+> instance YesodNic Blog
+
+In order to use yesod-auth, we need a YesodAuth instance.
+
+> instance YesodAuth Blog where
+>     type AuthId Blog = UserId
+>     loginDest _ = RootR
+>     logoutDest _ = RootR
+>     authHttpManager = httpManager
+
+We'll use [BrowserID](https://browserid.org/), which is a third-party system
+using email addresses as your identifier. This makes it easy to switch to other
+systems in the future, locally authenticated email addresses (also included
+with yesod-auth).
+
+>     authPlugins _ = [authBrowserId]
+
+This function takes someone's login credentials (i.e., his/her email address)
+and gives back a UserId.
+
+>     getAuthId creds = do
+>         let email = credsIdent creds
+>             user = User email
+>         res <- runDB $ insertBy user
+>         return $ Just $ either entityKey id res
+
+Homepage handler. The one important detail here is our usage of `setTitleI`,
+which allows us to use i18n messages for the title. We also use this message
+with a `_{Msg...}` interpolation in Hamlet.
+
+> getRootR :: Handler RepHtml
+> getRootR = defaultLayout $ do
+>     setTitleI MsgHomepageTitle
+>     [whamlet|
+> <p>_{MsgWelcomeHomepage}
+> <p>
+>    <a href=@{BlogR}>_{MsgSeeArchive}
+> |]
+
+Define a form for adding new entries. We want the user to provide the title and
+content, and then fill in the post date automatically via `getCurrentTime`.
+
+> entryForm :: Form Entry
+> entryForm = renderDivs $ Entry
+>     <$> areq textField (fieldSettingsLabel MsgNewEntryTitle) Nothing
+>     <*> aformM (liftIO getCurrentTime)
+>     <*> areq nicHtmlField (fieldSettingsLabel MsgNewEntryContent) Nothing
+
+Get the list of all blog entries, and present an admin with a form to create a
+new entry.
+
+> getBlogR :: Handler RepHtml
+> getBlogR = do
+>     muser <- maybeAuth
+>     entries <- runDB $ selectList [] [Desc EntryPosted]
+>     (entryWidget, enctype) <- generateFormPost entryForm
+>     defaultLayout $ do
+>         setTitleI MsgBlogArchiveTitle
+>         [whamlet|
+> $if null entries
+>     <p>_{MsgNoEntries}
+> $else
+>     <ul>
+>         $forall Entity entryId entry <- entries
+>             <li>
+>                 <a href=@{EntryR entryId}>#{entryTitle entry}
+
+We have three possibilities: the user is logged in as an admin, the user is
+logged in and is not an admin, and the user is not logged in. In the first
+case, we should display the entry form. In the second, we'll do nothing. In the
+third, we'll provide a login link.
+
+> $maybe Entity _ user <- muser
+>     $if isAdmin user
+>         <form method=post enctype=#{enctype}>
+>             ^{entryWidget}
+>             <div>
+>                 <input type=submit value=_{MsgNewEntry}>
+> $nothing
+>     <p>
+>         <a href=@{AuthR LoginR}>_{MsgLoginToPost}
+> |]
+
+Process an incoming entry addition. We don't do any permissions checking, since
+isAuthorized handles it for us. If the form submission was valid, we add the
+entry to the database and redirect to the new entry. Otherwise, we ask the user
+to try again.
+
+> postBlogR :: Handler RepHtml
+> postBlogR = do
+>     ((res, entryWidget), enctype) <- runFormPost entryForm
+>     case res of
+>         FormSuccess entry -> do
+>             entryId <- runDB $ insert entry
+>             setMessageI $ MsgEntryCreated $ entryTitle entry
+>             redirect $ EntryR entryId
+>         _ -> defaultLayout $ do
+>             setTitleI MsgPleaseCorrectEntry
+>             [whamlet|
+> <form method=post enctype=#{enctype}>
+>     ^{entryWidget}
+>     <div>
+>         <input type=submit value=_{MsgNewEntry}>
+> |]
+
+A form for comments, very similar to our entryForm above. It takes the
+EntryId of the entry the comment is attached to. By using pure, we embed
+this value in the resulting Comment output, without having it appear in the
+generated HTML.
+
+> commentForm :: EntryId -> Form Comment
+> commentForm entryId = renderDivs $ Comment
+>     <$> pure entryId
+>     <*> aformM (liftIO getCurrentTime)
+>     <*> aformM requireAuthId
+>     <*> areq textField (fieldSettingsLabel MsgCommentName) Nothing
+>     <*> areq textareaField (fieldSettingsLabel MsgCommentText) Nothing
+
+Show an individual entry, comments, and an add comment form if the user is
+logged in.
+
+> getEntryR :: EntryId -> Handler RepHtml
+> getEntryR entryId = do
+>     (entry, comments) <- runDB $ do
+>         entry <- get404 entryId
+>         comments <- selectList [] [Asc CommentPosted]
+>         return (entry, map entityVal comments)
+>     muser <- maybeAuth
+>     (commentWidget, enctype) <-
+>         generateFormPost (commentForm entryId)
+>     defaultLayout $ do
+>         setTitleI $ MsgEntryTitle $ entryTitle entry
+>         [whamlet|
+> <h1>#{entryTitle entry}
+> <article>#{entryContent entry}
+>     <section .comments>
+>         <h1>_{MsgCommentsHeading}
+>         $if null comments
+>             <p>_{MsgNoComments}
+>         $else
+>             $forall Comment _entry posted _user name text <- comments
+>                 <div .comment>
+>                     <span .by>#{name}
+>                     <span .at>#{show posted}
+>                     <div .content>#{text}
+>         <section>
+>             <h1>_{MsgAddCommentHeading}
+>             $maybe _ <- muser
+>                 <form method=post enctype=#{enctype}>
+>                     ^{commentWidget}
+>                     <div>
+>                         <input type=submit value=_{MsgAddCommentButton}>
+>             $nothing
+>                 <p>
+>                     <a href=@{AuthR LoginR}>_{MsgLoginToComment}
+> |]
+
+Receive an incoming comment submission.
+
+> postEntryR :: EntryId -> Handler RepHtml
+> postEntryR entryId = do
+>     ((res, commentWidget), enctype) <-
+>         runFormPost (commentForm entryId)
+>     case res of
+>         FormSuccess comment -> do
+>             _ <- runDB $ insert comment
+>             setMessageI MsgCommentAdded
+>             redirect $ EntryR entryId
+>         _ -> defaultLayout $ do
+>             setTitleI MsgPleaseCorrectComment
+>             [whamlet|
+> <form method=post enctype=#{enctype}>
+>     ^{commentWidget}
+>     <div>
+>         <input type=submit value=_{MsgAddCommentButton}>
+> |]
+
+Finally our main function.
+
+> main :: IO ()
+> main = do
+>     pool <- createSqlitePool "blog.db3" 10 -- create a new pool
+>     -- perform any necessary migration
+>     runSqlPool (runMigration migrateAll) pool
+>     manager <- newManager def -- create a new HTTP manager
+>     warpDebug 3000 $ Blog pool manager -- start our server
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/case-study-sphinx.html b/public/book-1.1/case-study-sphinx.html new file mode 100644 index 00000000..133aab10 --- /dev/null +++ b/public/book-1.1/case-study-sphinx.html @@ -0,0 +1,773 @@ + Case Study: Sphinx-based Search :: Yesod Web Framework Book- Version 1.1 +
+

Case Study: Sphinx-based Search

+ + +

+

+

+ Sphinx is a search + server, and powers the search feature on many sites, including Yesod's own site. While the actual + code necessary to integrate Yesod with Sphinx is relatively short, it touches on a number of + complicated topics, and is therefore a great case study in how to play with some of the + under-the-surface details of Yesod.

+

There are essentially three different pieces at play here:

+
    +
  • +

    Storing the content we wish to search. This is fairly straight-forward Persistent code, and + we won't dwell on it much in this chapter.

    +
  • +
  • +

    Accessing Sphinx search results from inside Yesod. Thanks to the sphinx + package, this is actually very easy.

    +
  • +
  • +

    Providing the document content to Sphinx. This is where the interesting stuff happens, and + will show how to deal with streaming content from a database directly to XML, which gets sent + directly over the wire to the client.

    +
  • +
+
+

Sphinx Setup

+

+

Unlike many of our other examples, to start with here we'll need to actually configure + and run our external Sphinx server. I'm not going to go into all the details of Sphinx, partly + because it's not relevant to our point here, and mostly because I'm not an expert on Sphinx.

+

Sphinx provides three main command line utilities: searchd + is the actual search daemon that receives requests from the client (in this case, our web app) + and returns the search results. indexer parses the set of documents and + creates the search index. search is a debugging utility that will run + simple queries against Sphinx.

+

There are two important settings: the source and the index. The source tells Sphinx + where to read document information from. It has direct support for MySQL and PostgreSQL, as well + as a more general XML format known as xmlpipe2. We're going to use the last one. This not only + will give us more flexibility with choosing Persistent backends, but will also demonstrate some + more powerful Yesod concepts.

+

The second setting is the index. Sphinx can handle multiple indices simultaneously, + which allows it to provide search for multiple services at once. Each index will have a source it + pulls from.

+

In our case, we're going to provide a URL from our application (/search/xmlpipe) that provides + the XML file required by Sphinx, and then pipe that through to the indexer. So we'll add the + following to our Sphinx config file:

+
source searcher_src
+{
+	type = xmlpipe2
+	xmlpipe_command = curl http://localhost:3000/search/xmlpipe
+}
+
+index searcher
+{
+	source = searcher_src
+	path = /var/data/searcher
+	docinfo = extern
+	charset_type = utf-8
+}
+

In order to build your search index, you would run indexer searcher. Obviously + this won't work until you have your web app running. For a production site, it would make sense + to run this command via a crontab script so the index is regularly updated.

+
+
+

Basic Yesod Setup

+

+

Let's get our basic Yesod setup going. We're going to have a single table in the database for + holding documents, which consist of a title and content. We'll store this in a SQLite database, + and provide routes for searching, adding documents, viewing documents and providing the xmlpipe + file to Sphinx.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher ConnectionPool
+
+mkYesod "Searcher" [parseRoutes|
+/ RootR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlPersist
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+

Hopefully all of this looks pretty familiar by now. Next we'll define some forms: one for + creating documents, and one for searching:

+
addDocForm :: Html -> MForm Searcher Searcher (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Searcher Searcher (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+

The True parameter to searchField makes the field auto-focus on page load. + Finally, we have some standard handlers for the homepage (shows the add document form and the + search form), the document display, and adding a document.

+
getRootR :: Handler RepHtml
+getRootR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout [whamlet|
+<p>Welcome to the search application. #{docs}
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+|]
+
+postAddDocR :: Handler RepHtml
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout [whamlet|
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+|]
+
+getDocR :: DocId -> Handler RepHtml
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout $
+        [whamlet|
+<h1>#{docTitle doc}
+<div .content>#{docContent doc}
+|]
+
+
+
+

Searching

+

+

Now that we've got the boring stuff out of the way, let's jump into the actual + searching. We're going to need three pieces of information for displaying a result: the + document ID it comes from, the title of that document, and the excerpts. Excerpts are the highlighted portions of the document which + contain the search term.

+
+

Search Result

+ + + + + +
+

So let's start off by defining a Result datatype:

+
data Result = Result
+    { resultId :: DocId
+    , resultTitle :: Text
+    , resultExcerpt :: Html
+    }
+

Next we'll look at the search handler:

+
getSearchR :: Handler RepHtml
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget [lucius|
+.excerpt {
+    color: green; font-style: italic
+}
+.match {
+    background-color: yellow;
+}
+|]
+        [whamlet|
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+$if not $ null searchResults
+    <h1>Results
+    $forall result <- searchResults
+        <div .result>
+            <a href=@{DocR $ resultId result}>#{resultTitle result}
+            <div .excerpt>#{resultExcerpt result}
+|]
+
+

Nothing magical here, we're just relying on the searchForm defined above, and the getResults + function which hasn't been defined yet. This function just takes a search string, and + returns a list of results. This is where we first interact with the Sphinx API. We'll be + using two functions: query will return a list of matches, and + buildExcerpts will return the highlighted excerpts. Let's + first look at getResults:

+
getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" (unpack qstring)
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (Key . PersistInt64 . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+

+ query takes three parameters: the configuration + options, the index to search against (searcher in this case) and the search string. It + returns a list of document IDs that contain the search string. The tricky bit here is + that those documents are returned as Int64 values, whereas we + need DocIds. We're taking advantage of the fact that the SQL + Persistent backends use a PersistInt64 constructor for their + IDs, and simply wrap up the values appropriately.

+ +

We then loop over the resulting IDs to get a [Maybe + Result] value, and use catMaybes to turn it into + a [Result]. In the where clause, we define our local + settings, which override the default port and set up the search to work when any term matches the document.

+

Let's finally look at the getResult function:

+
getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [T.unpack $ escape $ docContent doc]
+        "searcher"
+        (unpack qstring)
+    let excerpt =
+            case excerpt' of
+                ST.Ok bss -> preEscapedLazyText $ decodeUtf8With ignore $ L.concat bss
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+

+ buildExcerpts takes four parameters: the + configuration options, the textual contents of the document, the search index and the + search term. The interesting bit is that we entity escape the text content. Sphinx won't + automatically escape these for us, so we must do it explicitly.

+

Similarly, the result from Sphinx is a list of lazy ByteStrings. But of course, + we'd rather have Html. So we concat that list into a single lazy ByteString, decode it + to a lazy text (ignoring invalid UTF-8 character sequences), and use preEscapedLazyText + to make sure that the tags inserted for matches are not escaped. A sample of this HTML + is:

+
&#8230; Departments.  The President shall have <span class='match'>Power</span> to fill up all Vacancies
+&#8230;  people. Amendment 11 The Judicial <span class='match'>power</span> of the United States shall
+&#8230; jurisdiction. 2. Congress shall have <span class='match'>power</span> to enforce this article by
+&#8230; 5. The Congress shall have <span class='match'>power</span> to enforce, by appropriate legislation
+&#8230;
+
+
+

Streaming xmlpipe output

+

+

We've saved the best for last. For the majority of Yesod handlers, the recommended approach is + to load up the database results into memory and then produce the output document based on that. + It's simpler to work with, but more importantly it's more resilient to exceptions. If there's a + problem loading the data from the database, the user will get a proper 500 response code.

+ +

However, generating the xmlpipe output is a perfect example of the alternative. There are + potentially a huge number of documents (the yesodweb.com code handles tens of thousands of + these), and documents could easily be 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? As we cover in the WAI chapter, we have a ResponseSource constructor that + uses a stream of blaze-builder Builders. From the Yesod side, we can + avoid the normal Yesod response procedure and send a WAI response directly using the sendWaiResponse function. So there are at least two of the pieces of this + puzzle.

+

Now we know we want to create a stream of Builders from some XML + content. Fortunately, the xml-conduit package provides this + interface directly. xml-conduit provides some high-level interfaces for dealing + with documents as a whole, but in our case, we're going to need to use the low-level Event + interface to ensure minimal memory impact. So the function we're interested in is:

+
renderBuilder :: Resource m => RenderSettings -> Conduit Event m Builder b
+

In plain English, that means renderBuilder takes some settings (we'll just use the + defaults), and will then convert a stream of Events to a stream of + Builders. This is looking pretty good, all we need now is a stream of + Events.

+

Speaking of which, what should our XML document actually look like? It's pretty + simple, we have a sphinx:docset root element, a sphinx:schema element containing a single sphinx:field + (which defines the content field), and then a sphinx:document for each + document in our database. That last element will have an id attribute and a + child content element.

+
+

Sample xmlpipe document

+
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">
+    <sphinx:schema>
+        <sphinx:field name="content"/>
+    </sphinx:schema>
+    <sphinx:document id="1">
+        <content>bar</content>
+    </sphinx:document>
+    <sphinx:document id="2">
+        <content>foo bar baz</content>
+    </sphinx:document>
+</sphinx:docset>
+
+

Every document is going to start off with the same events (start the docset, start + the schema, etc) and end with the same event (end the docset). We'll start off by defining + those:

+
toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+

Now that we have the shell of our document, we need to get the Events for each individual document. This is actually a fairly simple function:

+
entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+

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.

+

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 also provides the (more powerful) selectSourceConn function. So we + end up with the function:

+
docSource :: Connection -> C.Source (C.ResourceT IO) X.Event
+docSource conn = selectSourceConn conn [] [] C.$= CL.concatMap entityToEvents
+

The $= operator joins together a source and a conduit into a new source. 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 Monoid instance, this is a piece + of cake:

+
fullDocSource :: Connection -> C.Source (C.ResourceT IO) X.Event
+fullDocSource conn = mconcat
+    [ CL.sourceList startEvents
+    , docSource conn
+    , CL.sourceList endEvents
+    ]
+

We're almost there, now we just need to tie it together in + getXmlpipeR. We need to get a database connection to be used. Normally, + database connections are taken and returned automatically via the runDB + function. In our case, we want to check out a connection and keep it available until the response + body is completely sent. To do this, we use the takeResource function, which + registers a cleanup action with the ResourceT monad.

+ +

By default, a resource will not be returned to the pool. This has to do with proper + exception handling, but is not relevant for our use case. Therefore, we need to force the + connection to be returned to the pool.

+
getXmlpipeR :: Handler RepXml
+getXmlpipeR = do
+    Searcher pool <- getYesod
+    let headers = [("Content-Type", "text/xml")]
+    managedConn <- lift $ takeResource pool
+    let conn = mrValue managedConn
+    lift $ mrReuse managedConn True let source = fullDocSource conn C.$= renderBuilder def
+    sendWaiResponse $ ResponseSource status200 headers source
+

We get our connection pool from the foundation variable, then send a WAI response. We + use the ResponseSource constructor, and provide it the status code, response + headers, and body.

+
+
+

Full code

+

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
+    QuasiQuotes, MultiParamTypeClasses, GADTs, FlexibleContexts
+  #-}
+import Yesod
+import Data.Text (Text, unpack)
+import Control.Applicative ((<$>), (<*>))
+import Database.Persist.Sqlite
+import Database.Persist.Query.GenericSql (selectSourceConn)
+import Database.Persist.Store (PersistValue (PersistInt64))
+import qualified Text.Search.Sphinx as S
+import qualified Text.Search.Sphinx.Types as ST
+import qualified Text.Search.Sphinx.ExcerptConfiguration as E
+import qualified Data.ByteString.Lazy as L
+import Data.Text.Lazy.Encoding (decodeUtf8With)
+import Data.Text.Encoding.Error (ignore)
+import Data.Maybe (catMaybes)
+import Control.Monad (forM)
+import qualified Data.Text as T
+import Text.Blaze (preEscapedLazyText)
+import qualified Data.Conduit as C
+import qualified Data.Conduit.List as CL
+import qualified Data.XML.Types as X
+import Network.Wai (Response (ResponseSource))
+import Network.HTTP.Types (status200)
+import Text.XML.Stream.Render (renderBuilder, def)
+import Data.Monoid (mconcat)
+import Data.Conduit.Pool (takeResource, mrValue, mrReuse)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher ConnectionPool
+
+mkYesod "Searcher" [parseRoutes|
+/ RootR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlPersist
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+addDocForm :: Html -> MForm Searcher Searcher (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Searcher Searcher (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+getRootR :: Handler RepHtml
+getRootR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout [whamlet|
+<p>Welcome to the search application. #{docs}
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+|]
+
+postAddDocR :: Handler RepHtml
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout [whamlet|
+<form method=post action=@{AddDocR}>
+    <table>
+        ^{docWidget}
+        <tr>
+            <td colspan=3>
+                <input type=submit value="Add document">
+|]
+
+getDocR :: DocId -> Handler RepHtml
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout $
+        [whamlet|
+<h1>#{docTitle doc}
+<div .content>#{docContent doc}
+|]
+
+data Result = Result
+    { resultId :: DocId
+    , resultTitle :: Text
+    , resultExcerpt :: Html
+    }
+
+getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [T.unpack $ escape $ docContent doc]
+        "searcher"
+        (unpack qstring)
+    let excerpt =
+            case excerpt' of
+                ST.Ok bss -> preEscapedLazyText $ decodeUtf8With ignore $ L.concat bss
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+
+getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" (unpack qstring)
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (Key . PersistInt64 . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+getSearchR :: Handler RepHtml
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget [lucius|
+.excerpt {
+    color: green; font-style: italic
+}
+.match {
+    background-color: yellow;
+}
+|]
+        [whamlet|
+<form method=get action=@{SearchR}>
+    ^{searchWidget}
+    <input type=submit value=Search>
+$if not $ null searchResults
+    <h1>Results
+    $forall result <- searchResults
+        <div .result>
+            <a href=@{DocR $ resultId result}>#{resultTitle result}
+            <div .excerpt>#{resultExcerpt result}
+|]
+
+getXmlpipeR :: Handler RepXml
+getXmlpipeR = do
+    Searcher pool <- getYesod
+    let headers = [("Content-Type", "text/xml")]
+    managedConn <- lift $ takeResource pool
+    let conn = mrValue managedConn
+    lift $ mrReuse managedConn True
+    let source = fullDocSource conn C.$= renderBuilder def
+        flushSource = C.mapOutput C.Chunk source
+    sendWaiResponse $ ResponseSource status200 headers flushSource
+
+entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+
+fullDocSource :: Connection -> C.Source (C.ResourceT IO) X.Event
+fullDocSource conn = mconcat
+    [ CL.sourceList startEvents
+    , docSource conn
+    , CL.sourceList endEvents
+    ]
+
+docSource :: Connection -> C.Source (C.ResourceT IO) X.Event
+docSource conn = selectSourceConn conn [] [] C.$= CL.concatMap entityToEvents
+
+toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+
+main :: IO ()
+main = withSqlitePool "searcher.db3" 10 $ \pool -> do
+    runSqlPool (runMigration migrateAll) pool
+    warpDebug 3000 $ Searcher pool
+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/conduits.html b/public/book-1.1/conduits.html new file mode 100644 index 00000000..1869d405 --- /dev/null +++ b/public/book-1.1/conduits.html @@ -0,0 +1,1685 @@ + Conduit :: Yesod Web Framework Book- Version 1.1 +
+

Conduit

+ + +

+

+

Conduits are a solution to the streaming data problem. Often times, laziness allows us + to process large amounts of data without pulling all values into memory. However, doing so in the + presence of I/O requires us to use lazy I/O. The main downside to lazy I/O is + non-determinism: we have no guarantees of when our resource finalizers will be run. For small + application, this may be acceptable, but for a high-load server, we could quickly run out of + scarce resources, such as file handles.

+

Conduits allow us to process large streams of data while still retaining deterministic resource + handling. They provide a unified interface for data streams, whether they come from files, + sockets, or memory. And when combined with ResourceT, we can safely allocate + resources, knowing that they will always be reclaimed- even in the presence of exceptions.

+

This appendix covers version 0.2 of the conduit package.

+
+

Conduits in Five Minutes

+

+

While a good understanding of the lower-level mechanics of conduits is advisable, you can get + very far without it. Let's start off with some high-level examples. Don't worry if some of the + details seem a bit magical right now. We'll cover everything in the course of this appendix. + Let's start with the terminology, and then some sample code.

+
+
Source
+

A producer of data. The data could be in a file, coming from a socket, or in memory as a + list. To access this data, we pull from the source.

+
+
Sink
+

A consumer of data. Basic examples would be a sum function (adding up a stream of numbers + fed in), a file sink (which writes all incoming bytes to a file), or a socket. We + push data into a sink. When the sink finishes processing (we'll explain that + later), it returns some value.

+
+
Conduit
+

A transformer of data. The simplest example is a map function, though there are many others. + Like a sink, we push data into a conduit. But instead of returning a single value + at the end, a conduit can return multiple outputs every time it is pushed to.

+
+
Fuse
+

(Thanks to David Mazieres for the term.) A conduit can be fused with a source + to produce a new, modified source (the $= operator). For example, you could + have a source that reads bytes from a file, and a conduit that decodes bytes into text. If you + fuse them together, you would now have a source that reads text from a file. Likewise, a + conduit and a sink can fuse into a new sink (=$), and two conduits can fuse + into a new conduit (=$=).

+
+
Connect
+

You can connect a source to a sink using the $$ operator. Doing so will + pull data from the source and push it to the sink, until either the source or sink signals that + they are "done."

+
+
+

Let's see some examples of conduit code.

+
{-# LANGUAGE OverloadedStrings #-}
+import Data.Conduit -- the core library
+import qualified Data.Conduit.List as CL -- some list-like functions
+import qualified Data.Conduit.Binary as CB -- bytes
+import qualified Data.Conduit.Text as CT
+
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Control.Monad.ST (runST)
+
+-- Let's start with the basics: connecting a source to a sink. We'll use the
+-- built in file functions to implementing efficient, constant-memory,
+-- resource-friendly file copying.
+--
+-- Two things to note: we use $$ to connect our source to our sink, and then
+-- use runResourceT.
+copyFile :: FilePath -> FilePath -> IO ()
+copyFile src dest = runResourceT $ CB.sourceFile src $$ CB.sinkFile dest
+
+
+-- The Data.Conduit.List module provides a number of helper functions for
+-- creating sources, sinks, and conduits. Let's look at a typical fold: summing
+-- numbers.
+sumSink :: Resource m => Sink Int m Int
+sumSink = CL.fold (+) 0
+
+-- If we want to go a little more low-level, we can code our sink with the
+-- sinkState function. This function takes three parameters: an initial state,
+-- a push function (receive some more data), and a close function.
+sumSink2 :: Resource m => Sink Int m Int
+sumSink2 = sinkState
+    0 -- initial value
+
+    -- update the state with the new input and
+    -- indicate that we want more input
+    (\accum i -> return $ StateProcessing (accum + i))
+    (\accum -> return accum) -- return the current accum value on close
+
+-- Another common helper function is sourceList. Let's see how we can combine
+-- that function with our sumSink to reimplement the built-in sum function.
+sum' :: [Int] -> Int
+sum' input = runST $ runResourceT $ CL.sourceList input $$ sumSink
+
+-- Since this is Haskell, let's write a source to generate all of the
+-- Fibonacci numbers. We'll use sourceState. The state will contain the next
+-- two numbers in the sequence. We also need to provide a pull function, which
+-- will return the next number and update the state.
+fibs :: Resource m => Source m Int
+fibs = sourceState
+    (0, 1) -- initial state
+    (\(x, y) -> return $ StateOpen (y, x + y) x)
+
+-- Suppose we want to get the sum of the first 10 Fibonacci numbers. We can use
+-- the isolate conduit to make sure the sum sink only consumes 10 values.
+sumTenFibs :: Int
+sumTenFibs =
+       runST -- runs fine in pure code
+     $ runResourceT
+     $ fibs
+    $= CL.isolate 10 -- fuse the source and conduit into a source
+    $$ sumSink
+
+-- We can also fuse the conduit into the sink instead, we just swap a few
+-- operators.
+sumTenFibs2 :: Int
+sumTenFibs2 =
+       runST
+     $ runResourceT
+     $ fibs
+    $$ CL.isolate 10
+    =$ sumSink
+
+-- Alright, let's make some conduits. Let's turn our numbers into text. Sounds
+-- like a job for a map...
+
+intToText :: Int -> Text -- just a helper function
+intToText = T.pack . show
+
+textify :: Resource m => Conduit Int m Text
+textify = CL.map intToText
+
+-- Like previously, we can use a conduitState helper function. But here, we
+-- don't even need state, so we provide a dummy state value.
+textify2 :: Resource m => Conduit Int m Text
+textify2 = conduitState
+    ()
+    (\() input -> return $ StateProducing () [intToText input])
+    (\() -> return [])
+
+-- Let's make the unlines conduit, that puts a newline on the end of each piece
+-- of input. We'll just use CL.map; feel free to write it with conduitState as
+-- well for practice.
+unlines' :: Resource m => Conduit Text m Text
+unlines' = CL.map $ \t -> t `T.append` "\n"
+
+-- And let's write a function that prints the first N fibs to a file. We'll
+-- use UTF8 encoding.
+writeFibs :: Int -> FilePath -> IO ()
+writeFibs count dest =
+      runResourceT
+    $ fibs
+   $= CL.isolate count
+   $= textify
+   $= unlines'
+   $= CT.encode CT.utf8
+   $$ CB.sinkFile dest
+
+-- We used the $= operator to fuse the conduits into the sources, producing a
+-- single source. We can also do the opposite: fuse the conduits into the sink. We can even combine the two.
+writeFibs2 :: Int -> FilePath -> IO ()
+writeFibs2 count dest =
+      runResourceT
+    $ fibs
+   $= CL.isolate count
+   $= textify
+   $$ unlines'
+   =$ CT.encode CT.utf8
+   =$ CB.sinkFile dest
+
+-- Or we could fuse all those inner conduits into a single conduit...
+someIntLines :: ResourceThrow m -- encoding can throw an exception
+             => Int
+             -> Conduit Int m ByteString
+someIntLines count =
+      CL.isolate count
+  =$= textify
+  =$= unlines'
+  =$= CT.encode CT.utf8
+
+-- and then use that conduit
+writeFibs3 :: Int -> FilePath -> IO ()
+writeFibs3 count dest =
+      runResourceT
+    $ fibs
+   $= someIntLines count
+   $$ CB.sinkFile dest
+
+main :: IO ()
+main = do
+    putStrLn $ "First ten fibs: " ++ show sumTenFibs
+    writeFibs 20 "fibs.txt"
+    copyFile "fibs.txt" "fibs2.txt"
+
+
+

Structure of this Chapter

+

+

The remainder of this chapter covers five major topics in conduits:

+
    +
  • +

    + ResourceT, the underlying technique that allows us to have guaranteed + resource deallocation.

    +
  • +
  • +

    Sources, our data producers

    +
  • +
  • +

    Sinks, our data consumers

    +
  • +
  • +

    Conduits, our data transformers

    +
  • +
  • +

    Buffering, which allows us to avoid Inversion of Control

    +
  • +
+
+
+

The Resource monad transformer

+

+

The Resource transformer (ResourceT) plays a vital role in proper resource + management in the conduit project. It is included within the conduit package + itself. We'll explaining ResourceT as its own entity. While some of the design + decisions clearly are biased towards conduits, ResourceT should remain a usable + tool in its own right.

+
+

Goals

+

+

What's wrong with the following code?

+
import System.IO
+
+main = do
+    output <- openFile "output.txt" WriteMode
+    input  <- openFile "input.txt"  ReadMode
+    hGetContents input >>= hPutStr output
+    hClose input
+    hClose output
+

If the file input.txt does not exist, then an exception will be thrown + when trying to open it. As a result, hClose output will never be called, and + we'll have leaked a scarce resource (a file descriptor). In our tiny program, this isn't a big + deal, but clearly we can't afford such waste in a long running, highly active server process.

+

Fortunately, solving the problem is easy:

+
import System.IO
+
+main =
+    withFile "output.txt" WriteMode $ \output ->
+    withFile "input.txt" ReadMode $ \input ->
+    hGetContents input >>= hPutStr output
+

+ withFile makes sure that the Handle is always closed, even in + the presence of exceptions. It also handles asynchronous exceptions. Overall, it's a great + approach to use... when you can use it. While often withFile is easy to use, + sometimes it can require restructuring our programs. And this restructuring can range from mildly + tedious to wildly inefficient.

+

Let's take enumerators for example. If you look in the documentation, there is an + enumFile function (for reading contents from a file), but no + iterFile (for writing contents to a file). That's because the flow of control + in an iteratee doesn't allow proper allocation of the Handle. Instead, in order to write to a + file, you need to allocate the Handle before entering the Iteratee, e.g.:

+
import System.IO
+import Data.Enumerator
+import Data.Enumerator.Binary
+
+main =
+    withFile "output.txt" WriteMode $ \output ->
+    run_ $ enumFile "input.txt" $$ iterHandle output
+

This code works fine, but imagine that, instead of simply piping data directly to the file, + there was a huge amount of computation that occurred before we need to use the output handle. We + will have allocated a file descriptor long before we needed it, and thereby locked up a scarce + resource in our application. Besides this, there are times when we can't allocate the file + before hand, such as when we won't know which file to open until we've read from the input + file.

+

One of the stated goals of conduits is to solve this problem, and it does so via + ResourceT. As a result, the above program can be written in conduit as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Data.Conduit
+import Data.Conduit.Binary
+
+main = runResourceT $ sourceFile "input.txt" $$ sinkFile "output.txt"
+
+
+

How it Works

+

+

There are essentially three base functions on ResourceT, and then a bunch of + conveniences thrown on top. The first function is:

+
register :: IO () -> ResourceT IO ReleaseKey
+ +

This function registers a piece of code that it asserts must be run. It gives back a + ReleaseKey, which is used by the next function:

+
release :: ReleaseKey -> ResourceT IO ()
+

Calling release on a ReleaseKey immediately performs the + action you previously registered. You may call release on the same + ReleaseKey as many times as you like; the first time it is called, it + unregisters the action. This means you can safely register an action like a memory + free, and have no concerns that it will be called twice.

+

Eventually, we'll want to exit our special ResourceT. To do so, we use:

+
runResourceT :: ResourceT IO a -> IO a
+

This seemingly innocuous function is where all the magic happens. It runs through all of the + registered cleanup actions and performs them. It is fully exception safe, meaning the cleanups + will be performed in the presence of both synchronous and asynchronous exceptions. And as + mentioned before, calling release will unregister an action, so there is no + concern of double-freeing.

+

Finally, as a convenience, we provide one more function for the common case of allocating a + resource and registering a release action:

+
with :: IO a -- ^ allocate
+     -> (a -> IO ()) -- ^ free resource
+     -> ResourceT IO (ReleaseKey, a)
+

So, to rework our first buggy example to use ResourceT, we would write:

+
import System.IO
+import Control.Monad.Trans.Resource
+import Control.Monad.Trans.Class (lift)
+
+main = runResourceT $ do
+    (releaseO, output) <- with (openFile "output.txt" WriteMode) hClose
+    (releaseI, input)  <- with (openFile "input.txt"  ReadMode)  hClose
+    lift $ hGetContents input >>= hPutStr output
+    release releaseI
+    release releaseO
+

Now there is no concern of any exceptions preventing the releasing of resources. We could skip + the release calls if we want to, and in an example this small, it would not make + any difference. But for larger applications, where we want processing to continue, this ensures + that the Handles are freed as early as possible, keeping our scarce resource + usage to a minimum.

+
+
+

Some Type Magic

+

+

As alluded to, there's a bit more to ResourceT than simply running in + IO. Let's cover some of the things we need from this underlying + Monad.

+
    +
  • +

    Mutable references to keep track of the registered release actions. You might think we could + just use a StateT transformer, but then our state wouldn't survive + exceptions.

    +
  • +
  • +

    We only want to register actions in the base monad. For example, if we have a + ResourceT (WriterT [Int] IO) stack, we only want to register + IO actions. This makes it easy to lift our stacks around (i.e., add an extra + transformer to the middle of an existing stack), and avoids confusing issues about the threading + of other monadic side-effects.

    +
  • +
  • +

    Some way to guarantee an action is performed, even in the presence of exceptions. This boils + down to needing a bracket-like function.

    +
  • +
+

For the first point, we define a new typeclass to represent monads that have mutable + references:

+
class Monad m => HasRef m where
+    type Ref m :: * -> *
+    newRef' :: a -> m (Ref m a)
+    readRef' :: Ref m a -> m a
+    writeRef' :: Ref m a -> a -> m ()
+    modifyRef' :: Ref m a -> (a -> (a, b)) -> m b
+    mask :: ((forall a. m a -> m a) -> m b) -> m b
+    mask_ :: m a -> m a
+    try :: m a -> m (Either SomeException a)
+

We have an associated type to signify what the reference type should be. (For fans of fundeps, + you'll see in the next section that this has to be an associated type.) Then we provide a + number of basic reference operations. Finally, there are some functions to help with exceptions, + which are needed to safely implement the functions described in the last section. The instance + for IO is very straight-forward:

+
instance HasRef IO where
+    type Ref IO = I.IORef
+    newRef' = I.newIORef
+    modifyRef' = I.atomicModifyIORef
+    readRef' = I.readIORef
+    writeRef' = I.writeIORef
+    mask = E.mask
+    mask_ = E.mask_
+    try = E.try
+

However, we have a problem when it comes to implementing the ST instance: + there is no way to deal with exceptions in the ST monad. As a result, + mask, mask_ and try are given default + implementations that do no exception checking. This gives rise to the first word of warning: + operations in the ST monad are not exception safe. You should not be allocating scarce + resources in ST when using ResourceT. You might be wondering why bother with + ResourceT at all then for ST. The answer is that there is a + lot you can do with conduits without allocating scarce resources, and ST is a + great way to do this in a pure way. But more on this later.

+

Now onto point 2: we need some way to deal with this base monad concept. Again, we use an + associated type (again explained in the next section). Our solution looks something like:

+
class (HasRef (Base m), Monad m) => Resource m where
+    type Base m :: * -> *
+
+    resourceLiftBase :: Base m a -> m a
+

But we forgot about point 3: some bracket-like function. So we need one more + method in this typeclass:

+
    resourceBracket_ :: Base m a -> Base m b -> m c -> m c
+

The reason the first two arguments to resourceBracket_ (allocation and + cleanup) live in Base m instead of m is that, in + ResourceT, all allocation and cleanup lives in the base monad.

+

So on top of our HasRef instance for IO, we now need a + Resource instance as well. This is similarly straight-forward:

+
instance Resource IO where
+    type Base IO = IO
+    resourceLiftBase = id
+    resourceBracket_ = E.bracket_
+

We have similar ST instances, with resourceBracket_ having no + exception safety. The final step is dealing with monad transformers. We don't need to provide a + HasRef instance, but we do need a Resource instance. The + tricky part is providing a valid implementation of resourceBracket_. For this, + we use some functions from monad-control:

+
instance (MonadTransControl t, Resource m, Monad (t m))
+        => Resource (t m) where
+    type Base (t m) = Base m
+
+    resourceLiftBase = lift . resourceLiftBase
+    resourceBracket_ a b c =
+        control' $ \run -> resourceBracket_ a b (run c)
+      where
+        control' f = liftWith f >>= restoreT . return
+

For any transformer, its base is the base of its inner monad. Similarly, we lift to the base by + lifting to the inner monad and then lifting to the base from there. The tricky part is the + implemetnation of resourceBracket_. I will not go into a detailed explanation, + as I would simply make a fool of myself.

+
+
+

Definition of ResourceT

+

+

We now have enough information to understand the definition of ResourceT:

+
newtype ReleaseKey = ReleaseKey Int
+
+type RefCount = Int
+type NextKey = Int
+
+data ReleaseMap base =
+    ReleaseMap !NextKey !RefCount !(IntMap (base ()))
+
+newtype ResourceT m a =
+    ResourceT (Ref (Base m) (ReleaseMap (Base m)) -> m a)
+

We see that ReleaseKey is simply an Int. If you skip a few + lines down, this will make sense, since we're using an IntMap to keep track of + the registered actions. We also define two type synonyms: RefCount and + NextKey. NextKey keeps track of the most recently assigned + value for a key, and is incremented each time register is called. We'll touch on + RefCount later.

+

The ReleaseMap is three pieces of information: the next key and the reference + count, and then the map of all registered actions. Notice that ReleaseMap takes + a type parameter base, which states which monad release actions must live + in.

+

Finally, a ResourceT is essentially a ReaderT that keeps a + mutable reference to a ReleaseMap. The reference type is determined by the base + of the monad in question, as is the cleanup monad. This is why we need to use associated + types.

+

The majority of the rest of the code in the Control.Monad.Trans.Resource + module is just providing instances for the ResourceT type.

+
+
+

Other Typeclasses

+

+

There are three other typeclasses provided by the module:

+
+
ResourceUnsafeIO
+

Any monad which can lift IO actions into it, but that this may be + considered unsafe. The prime candidate here is ST. Care should be taken to + only lift actions which do not acquire scarce resources and which don't "fire the missiles." In + other words, all the normal warnings of unsafeIOToST apply.

+
+
ResourceThrow
+

For actions that can throw exceptions. This automatically applies to all + IO-based monads. For ST-based monads, you can use the + supplied ExceptionT transformer to provide exception-throwing capabilities. + Some functions in conduit, for example, will require this (e.g., text decoding).

+
+
ResourceIO
+

A convenience class tying together a bunch of other classes, included the two mentioned + above. This is purely for convenience; you could achieve the same effect without this type + class, you'd just have to do a lot more typing.

+
+
+
+
+

Forking

+

+

It would seem that forking a thread would be inherently unsafe with ResourceT, + since the parent thread may call runResourceT while the child thread is still + accessing some of the allocated resources. This is indeed true, if you use the normal + forkIO function.

+ +

In order to solve this, ResourceT includes reference counting. When you fork a + new thread via resourceForkIO, the RefCount value of the + ReleaseMap is incremented. Every time runResourceT is called, + the value is decremented. Only when the value hits 0 are all the release actions called.

+
+
+

Convenience Exports

+

+

In addition to what's been listed so far, there are a few extra functions exported (mostly) for + convenience.

+
    +
  • +

    + newRef, writeRef, and readRef wrap up the + HasRef versions of the functions and allow them to run in any + ResourceT.

    +
  • +
  • +

    + withIO is essentially a type-restricted version of with, + but working around some of the nastiness with types you would otherwise run into. In general: + you'll want to use withIO when writing IO code.

    +
  • +
  • +

    + transResourceT let's you modify which monad your ResourceT is running in, + assuming it keeps the same + base.

    transResourceT :: (Base m ~ Base n)
    +               => (m a -> n a)
    +               -> ResourceT m a
    +               -> ResourceT n a
    +transResourceT f (ResourceT mx) = ResourceT (\r -> f (mx r))
    +

    +
  • +
+
+
+
+

Source

+

+

I think it's simplest to understand sources by looking at the types:

+
data SourceResult m a = Open (Source m a) a | Closed
+data Source m a = Source
+    { sourcePull :: ResourceT m (SourceResult m a)
+    , sourceClose :: ResourceT m ()
+    }
+

A source has just two operations on it: you can pull data from it, and you can close it (think + of closing a file handle). When you pull, you either get some data and the a new + Source (the source is still open), or nothing (the source is closed). Let's + look at some of the simplest sources:

+
import Prelude hiding (repeat)
+import Data.Conduit
+
+-- | Never give any data
+eof :: Monad m => Source m a
+eof = Source
+    { sourcePull = return Closed
+    , sourceClose = return ()
+    }
+
+-- | Always give the same value
+repeat :: Monad m => a -> Source m a
+repeat a = Source
+    { sourcePull = return $ Open (repeat a) a
+    , sourceClose = return ()
+    }
+
+

These sources are very straight-forward, since they always return the same results. + Additionally, their close records don't do anything. You might think that this is a bug: + shouldn't a call to sourcePull return Closed after it's been + closed? This isn't required, since one of the rules of sources is that they can never be + reused. In other words:

+
    +
  • +

    If a Source returns Open, it has provided you with a new + Source which you should use in place of the original one.

    +
  • +
  • +

    If it returns Closed, then you cannot perform any more operations on + it.

    +
  • +
+

Don't worry too much about the invariant. In practice, you will almost never call + sourcePull or sourceClose yourself. In fact, you hardly + even write them yourself either (that's what sourceState and + sourceIO are for). The point is that we can make some assumptions when we + implement our sources.

+
+

State

+

+

There is something similar about the two sources mentioned above: they never change. They + always return the same value. In other words, they have no state. For almost all serious + sources, we'll need some kind of state.

+ +

The way we store state in a source is by updating the returned Source value in + the Open constructor. This is best seen with an example.

+
import Data.Conduit
+import Control.Monad.Trans.Resource
+
+-- | Provide data from the list, one element at a time.
+sourceList :: Resource m => [a] -> Source m a
+sourceList list = Source
+    { sourcePull =
+        case list of
+            [] -> return Closed -- no more data
+
+            -- This is where we store our state: by return a new
+            -- Source with the rest of the list
+            x:xs -> return $ Open (sourceList xs) x
+        , sourceClose = return ()
+        }
+
+

Each time we pull from the source, it checks the input list. If the list is empty, pulling + returns Closed, which makes sense. If the list is not empty, pulling returns + Open with both the next value in the list, and a new Source + value containing the rest of the input list.

+
+
+

sourceState and sourceIO

+

+

In addition to being able to manually create Sources, we also have a few + convenience functions that allow us to create most sources in a more high-level fashion. + sourceState let's you write code similar to how you would use the + State monad. You provide an initial state, your pull function is provided with + the current state, and it returns a new state and a return value. Let's use this to reimplement + sourceList.

+
import Data.Conduit
+import Control.Monad.Trans.Resource
+
+-- | Provide data from the list, one element at a time.
+sourceList :: Resource m => [a] -> Source m a
+sourceList state0 = sourceState
+    state0
+    pull
+  where
+    pull [] = return StateClosed
+    pull (x:xs) = return $ StateOpen xs x
+
+

Notice the usage of the StateClosed and StateOpen + constructors. These are very similar to Closed and Open, except + that instead of specifying the next Source to be used, you provide the next + state (here, the remainder of the list).

+

The other common activity is to perform some I/O allocation (like opening a file), registering + some cleanup action (closing that file), and having a function for pulling data from that + resource. conduit comes built-in with a sourceFile function + that gives a stream of ByteStrings. Let's write a wildly inefficient alternative + that returns a stream of characters.

+
import Data.Conduit
+import Control.Monad.Trans.Resource
+import System.IO
+import Control.Monad.IO.Class (liftIO)
+
+sourceFile :: ResourceIO m => FilePath -> Source m Char
+sourceFile fp = sourceIO
+    (openFile fp ReadMode)
+    hClose
+    (\h -> liftIO $ do
+        eof <- hIsEOF h
+        if eof
+            then return IOClosed
+            else fmap IOOpen $ hGetChar h)
+
+

Like sourceState, it uses a variant on the Open and + Closed constructors. sourceIO does a number of things for + us:

+
    +
  • +

    It registers the cleanup function with the ResourceT transformer, ensuring + it gets called even in the presence of exceptions.

    +
  • +
  • +

    It sets up the sourceClose record to release the resource immediately.

    +
  • +
  • +

    As soon as you return IOClosed, it will release the resource.

    +
  • +
+
+
+
+

Sinks

+

+

A sink consumes a stream of data, and produces a result. A sink must always produce a result, + and must always produce a single result. This is encoded in the types themselves.

+

There is a Monad instance for sink, making it simple to compose multiple sinks + together into a larger sink. You can also use the built-in sink functions to perform most of your + work. Like sources, you'll rarely need to dive into the inner workings. Let's start off with an + example: getting lines from a stream of Chars (we'll assume Unix line endings + for simplicity).

+
import Data.Conduit
+import qualified Data.Conduit.List as CL
+
+-- Get a single line from the stream.
+sinkLine :: Resource m => Sink Char m String
+sinkLine = sinkState
+    id -- initial state, nothing at the beginning of the line
+    push
+    close
+  where
+    -- On a new line, return the contents up until here
+    push front '\n' =
+        return $ StateDone Nothing $ front []
+
+    -- Just another character, add it to the front and keep going
+    push front char =
+        return $ StateProcessing $ front . (char:)
+
+    -- Got an EOF before hitting a newline, just give what we have so far
+    close front = return $ front []
+
+-- Get all the lines from the stream, until we hit a blank line or EOF.
+sinkLines :: Resource m => Sink Char m [String]
+sinkLines = do
+    line <- sinkLine
+    if null line
+        then return []
+        else do
+            lines <- sinkLines
+            return $ line : lines
+
+content :: String
+content = unlines
+    [ "This is the first line."
+    , "Here's the second."
+    , ""
+    , "After the blank."
+    ]
+
+main :: IO ()
+main = do
+    lines <- runResourceT $ CL.sourceList content $$ sinkLines
+    mapM_ putStrLn lines
+

Running this sample produces the expected output:

+
This is the first line.
+Here's the second.
+

+ sinkLine demonstrates usage of the sinkState function, which + is very similar to the sourceState function we just saw. It takes three + arguments: an initial state, a push function (takes the current state and next input, and returns + a new state and result) and a close function (takes the current state and returns an output). As + opposed to sourceState- which doesn't need a close function- a sink is required + to always return a result.

+

Our push function has two clauses. When it gets a newline character, it indicates that + processing is complete via StateDone. The Nothing indicates + that there is no leftover input (we'll discuss that later). It also gives an output of all the + characters it has received. The second clause simply appends the new character to the existing + state and indicates that we are still working via StateProcessing. The close + function returns all characters.

+

+ sinkLines shows how we can use the monadic interface to produce new sinks. If + you replace sinkLine with getLine, this would look like + standard code to pull lines from standard input. This familiar interface should make it easy to + get up and running quickly.

+
+

Types

+

+

The types for sinks are just a bit more involved than sources. Let's have a look:

+
type SinkPush input m output = input -> ResourceT m (SinkResult input m output)
+type SinkClose m output = ResourceT m output
+
+data SinkResult input m output =
+    Processing (SinkPush input m output) (SinkClose m output)
+  | Done (Maybe input) output
+
+data Sink input m output =
+    SinkNoData output
+  | SinkData
+        { sinkPush :: SinkPush input m output
+        , sinkClose :: SinkClose m output
+        }
+  | SinkLift (ResourceT m (Sink input m output))
+

Whenever a sink is pushed to, it can either say it needs more data + (Processing) or say it's all done. When still processing, it must provided + updated push and close function; when done, it returns any leftover inut and the output. Fairly + straight-forward.

+

The first real "gotcha" is the three constructors for Sink. Why do we need + SinkNoData: aren't sinks all about consuming data? The answer is that we need + it to efficiently implement our Monad instance. When we use + return, we're giving back a value that requires no data in order to compute it. + We could model this with the SinkData constructor, with something like:

+
myReturn a = SinkData (\input -> return (Done (Just input) a)) (return a)
+

But doing so would force reading in an extra bit of input that we don't need right now, and + possibly will never need. (Have a look again at the sinkLines example.) So + instead, we have an extra constructor to indicate that no input is required. Likewise, + SinkLift is provided in order to implement an efficient + MonadTrans instance.

+
+
+

Sinks: no helpers

+

+

Let's try to implement some sinks on the "bare metal", without any helper functions.

+
import Data.Conduit
+import System.IO
+import Control.Monad.Trans.Resource
+import Control.Monad.IO.Class (liftIO)
+
+-- Consume all input and discard it.
+sinkNull :: Resource m => Sink a m ()
+sinkNull =
+    SinkData push close
+  where
+    push _ignored = return $ Processing push close
+    close = return ()
+
+-- Let's stream characters to a file. Here we do need some kind of
+-- initialization. We do this by initializing in a push function,
+-- and then returning a different push function for subsequent
+-- calls. By using withIO, we know that the handle will be closed even
+-- if there's an exception.
+sinkFile :: ResourceIO m => FilePath -> Sink Char m ()
+sinkFile fp =
+    SinkData pushInit closeInit
+  where
+    pushInit char = do
+        (releaseKey, handle) <- withIO (openFile fp WriteMode) hClose
+        push releaseKey handle char
+    closeInit = do
+        -- Never opened a file, so nothing to do here
+        return ()
+
+    push releaseKey handle char = do
+        liftIO $ hPutChar handle char
+        return $ Processing (push releaseKey handle) (close releaseKey handle)
+
+    close releaseKey _ = do
+        -- Close the file handle as soon as possible.
+        return ()
+
+-- And we'll count how many values were in the stream.
+count :: Resource m => Sink a m Int
+count =
+    SinkData (push 0) (close 0)
+  where
+    push count _ignored =
+        return $ Processing (push count') (close count')
+      where
+        count' = count + 1
+
+    close count = return count
+
+

Nothing is particularly complicated to implement. You should notice a common pattern here: + declaring your push and close functions in a where clause, and then + using them twice: once for the initial SinkData, and once for the + Processing constructor. This can become a bit tedious; that's why + we have helper functions.

+
+
+

Sinks: with helpers

+

+

Let's rewrite sinkFile and count to take advantage of the + helper functions sinkIO and sinkState, respectively.

+
import Data.Conduit
+import System.IO
+import Control.Monad.IO.Class (liftIO)
+
+-- We never have to touch the release key directly, sinkIO automatically
+-- releases our resource as soon as we return IODone from our push function,
+-- or sinkClose is called.
+sinkFile :: ResourceIO m => FilePath -> Sink Char m ()
+sinkFile fp = sinkIO
+    (openFile fp WriteMode)
+    hClose
+    -- push: notice that we are given the handle and the input
+    (\handle char -> do
+        liftIO $ hPutChar handle char
+        return IOProcessing)
+    -- close: we're also given the handle, but we don't use it
+    (\_handle -> return ())
+
+-- And we'll count how many values were in the stream.
+count :: Resource m => Sink a m Int
+count = sinkState
+    0
+    -- The push function gets both the current state and the next input...
+    (\state _ignored ->
+        -- and it returns the new state
+        return $ StateProcessing $ state + 1)
+    -- The close function gets the final state and returns the output.
+    (\state -> return state)
+
+

Nothing dramatic, just slightly shorter, less error-prone code. Using these two helper + functions is highly recommended, as it ensures proper resource management and state updating.

+
+
+

List functions

+

+

As easy as it is to write your own sinks, you'll likely want to take advantage of the built-in + sinks available in the Data.Conduit.List module. These provide + analogues to common list functions, like folding. (The module also has some + Conduits, like map.)

+

If you're looking for some way to practice with conduits, reimplementing the functions in the + List module- both with and without the helper functions- would be a good + start.

+

Let's look at some simple things we can make out of the built-in sinks.

+
import Data.Conduit
+import qualified Data.Conduit.List as CL
+import Control.Monad.IO.Class (liftIO)
+
+-- A sum function.
+sum' :: Resource m => Sink Int m Int
+sum' = CL.fold (+) 0
+
+-- Print every input value to standard output.
+printer :: (Show a, ResourceIO m) => Sink a m ()
+printer = CL.mapM_ (liftIO . print)
+
+-- Sum up all the values in a stream after the first five.
+sumSkipFive :: Resource m => Sink Int m Int
+sumSkipFive = do
+    CL.drop 5
+    CL.fold (+) 0
+
+-- Print each input number and sum the total
+printSum :: ResourceIO m => Sink Int m Int
+printSum = do
+    total <- CL.foldM go 0
+    liftIO $ putStrLn $ "Sum: " ++ show total
+    return total
+  where
+    go accum int = do
+        liftIO $ putStrLn $ "New input: " ++ show int
+        return $ accum + int
+
+
+
+

Connecting

+

+

At the end of the day, we're actually going to want to use our sinks. While we could manually + call sinkPush and sinkClose, it's tedious. For example:

+
main :: IO ()
+main = runResourceT $ do
+    res <-
+        case printSum of
+            SinkData push close -> loop [1..10] push close
+            SinkNoData res -> return res
+    liftIO $ putStrLn $ "Got a result: " ++ show res
+  where
+    loop [] _push close = close
+    loop (x:xs) push close = do
+        mres <- push x
+        case mres of
+            Done _leftover res -> return res
+            Processing push' close' -> loop xs push' close'
+
+

Instead, the recommended approach is to connect your sink to a source. Not only is this + simpler, it's less error prone, and means you have a lot of flexibility in where your data is + coming from. To rewrite the example above:

+
main :: IO ()
+main = runResourceT $ do
+    res <- CL.sourceList [1..10] $$ printSum
+    liftIO $ putStrLn $ "Got a result: " ++ show res
+
+

Connecting takes care of testing for the sink constructor (SinkData versus + SinkNoData versus SinkLift), pulling from the source, and + pushing to/closing the sink.

+

However, there is one thing I wanted to point out from the long-winded example. On the second + to last line, we ignore the leftover value of Done. This brings up the issue of + data loss. This is an important topic that has had a lot of thought put into it. + Unfortunately, we can't fully cover it yet, as we haven't discussed the main culprit in the + drama: Conduits (the type, not the package).

+

But as a quick note here, the leftover value from the Done constructor is not + always ignored. The Monad instance, for example, uses it to pass data from one + sink to the next in a binding. And in fact, the real connect operator doesn't always throw + away the leftovers. When we cover resumable sources later, we'll see that the leftover value is + put back on the buffer to allow later sinks reusing an existing source to pull the value.

+
+
+
+

Conduit

+

+

This section covers the final major datatype in our package, conduits. While sources produce a + stream of data and sinks consume a stream, conduits transform a stream.

+
+

Types

+

+

As we did previously, let's start off by looking at the types involved.

+
data ConduitResult input m output =
+    Producing (Conduit input m output) [output]
+  | Finished (Maybe input) [output]
+
+data Conduit input m output = Conduit
+    { conduitPush :: input -> ResourceT m (ConduitResult input m output)
+    , conduitClose :: ResourceT m [output]
+    }
+

This should look very similar to what we've seen with sinks. A conduit can be pushed to, in + which case it returns a result. A result either indicates that it is still producing data, or + that it is finished. When a conduit is closed, it returns some more output.

+

But let's examine the idiosyncracies a bit. Like sinks, we can only push one piece of input at + a time, and leftover data may be 0 or 1 pieces. However, there are a few changes:

+
    +
  • +

    When producing (the equivalent of processing for a sink), we can return output. This is + because a conduit will product a new stream of output instead of producing a single output value + at the end of processing.

    +
  • +
  • +

    A sink always returns a single output value, while a conduit returns 0 or more outputs (a + list). To understand why, consider conduits such as concatMap (produces + multiple outputs for one input) and filter (returns 0 or 1 output for each + input).

    +
  • +
  • +

    We have no special constructor like SinkNoData. That's because we provide no + Monad instance for conduits. We'll see later how you can still use a familiar + Monadic approach to creating conduits.

    +
  • +
+

Overall conduits should seem very similar to what we've covered so far.

+
+
+

Simple conduits

+

+

We'll start off by defining some simple conduits that don't have any state.

+
import Prelude hiding (map, concatMap)
+import Data.Conduit
+
+-- A simple conduit that just passes on the data as-is.
+passThrough :: Monad m => Conduit input m input
+passThrough = Conduit
+    { conduitPush = \input -> return $ Producing passThrough [input]
+    , conduitClose = return []
+    }
+
+-- map values in a stream
+map :: Monad m => (input -> output) -> Conduit input m output
+map f = Conduit
+    { conduitPush = \input -> return $ Producing (map f) [f input]
+    , conduitClose = return []
+    }
+
+-- map and concatenate
+concatMap :: Monad m => (input -> [output]) -> Conduit input m output
+concatMap f = Conduit
+    { conduitPush = \input -> return $ Producing (concatMap f) $ f input
+    , conduitClose = return []
+    }
+
+
+
+

Stateful conduits

+

+

Of course, not all conduits can be declared without state. Doing so on the bare metal is not + too difficult.

+
import Prelude hiding (reverse)
+import qualified Data.List
+import Data.Conduit
+import Control.Monad.Trans.Resource
+
+-- Reverse the elements in the stream. Note that this has the same downside as
+-- the standard reverse function: you have to read the entire stream into
+-- memory before producing any output.
+reverse :: Resource m => Conduit input m input
+reverse =
+    mkConduit []
+  where
+    mkConduit state = Conduit (push state) (close state)
+    push state input = return $ Producing (mkConduit $ input : state) []
+    close state = return state
+
+-- Same thing with sort: it will pull everything into memory
+sort :: (Ord input, Resource m) => Conduit input m input
+sort =
+    mkConduit []
+  where
+    mkConduit state = Conduit (push state) (close state)
+    push state input = return $ Producing (mkConduit $ input : state) []
+    close state = return $ Data.List.sort state
+
+

But we can do better. Just like sourceState and sinkState, we + have conduitState to simplify things.

+
import Prelude hiding (reverse)
+import qualified Data.List
+import Data.Conduit
+
+-- Reverse the elements in the stream. Note that this has the same downside as
+-- the standard reverse function: you have to read the entire stream into
+-- memory before producing any output.
+reverse :: Resource m => Conduit input m input
+reverse =
+    conduitState [] push close
+  where
+    push state input = return $ StateProducing (input : state) []
+    close state = return state
+
+-- Same thing with sort: it will pull everything into memory
+sort :: (Ord input, Resource m) => Conduit input m input
+sort =
+    conduitState [] push close
+  where
+    push state input = return $ StateProducing (input : state) []
+    close state = return $ Data.List.sort state
+
+
+
+

Using conduits

+

+

The way Conduits interact with the rest of the package is via + fusing. A conduit can be fused into a source, producing a new source, fused into a + sink to produce a new sink, or fused with another conduit to produce a new conduit. It's best to + just look at the fusion operators.

+
-- Left fusion: source + conduit = source
+($=) :: (Resource m, IsSource src) => src m a -> Conduit a m b -> Source m b
+
+-- Right fusion: conduit + sink = sink
+(=$) :: Resource m => Conduit a m b -> Sink b m c -> Sink a m c
+
+-- Middle fusion: conduit + conduit = conduit
+(=$=) :: Resource m => Conduit a m b -> Conduit b m c -> Conduit a m c
+

Using these operators is straightforward.

+
useConduits = do
+    runResourceT
+          $  CL.sourceList [1..10]
+          $= reverse
+          $= CL.map show
+          $$ CL.consume
+
+    -- equivalent to
+    runResourceT
+          $  CL.sourceList [1..10]
+          $$ reverse
+          =$ CL.map show
+          =$ CL.consume
+
+    -- and equivalent to
+    runResourceT
+          $  CL.sourceList [1..10]
+          $$ (reverse =$= CL.map show)
+          =$ CL.consume
+

There is in fact one last way of expressing the same idea. I'll leave it as an exercise to the + reader to discover it.

+

It may seem like all these different approaches are redundant. While occassionally you can in + fact choose whichever approach you feel like using, in many cases you will need a specific + approach. For example:

+
    +
  • +

    If you have a stream of numbers, and you want to apply a conduit (e.g., map + show) to only some of the stream that will be passed to a specific sink, you'll want + to use the right fusion operator.

    +
  • +
  • +

    If you're reading a file, and want to parse the entire file as textual data, you'll want to + use left fusion to convert the entire stream.

    +
  • +
  • +

    If you want to create reusable conduits that combine together individual, smaller conduits, + you'll use middle fusion.

    +
  • +
+
+
+

Data loss

+

+

Let's forget about conduits for a moment. Instead, suppose we want to write a program- using + plain old lists- that will take a list of numbers, apply some kind of transformation to them, + take the first five transformed values and do something with them, and then do something else + with the remaining non-transformed values. For example, we want something like:

+
main = do
+    let list = [1..10]
+        transformed = map show list
+        (begin, end) = splitAt 5 transformed
+        untransformed = map read end
+    mapM_ putStrLn begin
+    print $ sum untransformed
+

But clearly this isn't a good general solution, since we don't want to have to transform and + then untransform every element in the list. For one thing, we may not always have an inverse + function. Another issue is efficiency. In this case, we can write something more efficient:

+
main = do
+    let list = [1..10]
+        (begin, end) = splitAt 5 list
+        transformed = map show begin
+    mapM_ putStrLn transformed
+    print $ sum end
+

Note the change: we perform our split before transforming any elements. This works because, + with map, we have a 1-to-1 correspondence between the input and output elements. + So splitting at 5 before or after mapping show is the same thing. But what + happens if we replace map show with something more devious.

+
deviousTransform =
+    concatMap go
+  where
+    go 1 = [show 1]
+    go 2 = [show 2, "two"]
+    go 3 = replicate 5 "three"
+    go x = [show x]
+

We no longer have the 1-to-1 correspondence. As a result, we can't use the second method. But + it's even worse: we can't use the first method either, since there's no inverse of our + deviousTransform.

+

There's only one solution to the problem that I'm aware of: transform elements one at a time. + The final program looks like this:

+
deviousTransform 1 = [show 1]
+deviousTransform 2 = [show 2, "two"]
+deviousTransform 3 = replicate 5 "three"
+deviousTransform x = [show x]
+
+transform5 :: [Int] -> ([String], [Int])
+transform5 list =
+    go [] list
+  where
+    go output (x:xs)
+        | newLen >= 5 = (take 5 output', xs)
+        | otherwise = go output' xs
+      where
+        output' = output ++ deviousTransform x
+        newLen = length output'
+
+    -- Degenerate case: not enough input to make 5 outputs
+    go output [] = (output, [])
+
+main = do
+    let list = [1..10]
+        (begin, end) = transform5 list
+    mapM_ putStrLn begin
+    print $ sum end
+

The final output of this program is

1
+2
+two
+three
+three
+49
What's important + to note is that the number 3 is converted into five copies of the word "three", yet only two of + them show up in the output. The rest are discarded in the take 5 call.

+

This whole exercise is just to demonstrate the issue of data loss in conduits. By forcing + conduits to accept only one input at a time, we avoid the issue of transforming too many elements + at once. That doesn't mean we don't lose any data: if a conduit produces too much output + for the receiving sink to handle, some of it may be lost.

+

To put all this another way: conduits avoid chunking to get away from data loss. This is not an + issue unique to conduits. If you look in the implementation of concatMapM for + enumerator, you'll see that it forces elements to be handled one at a time. + In conduits, we opted to force the issue at the type level.

+
+
+

SequencedSink

+

+

Suppose we want to be able to combine up existing conduits and sinks to produce a new, more + powerful conduit. For example, we want to write a conduit that takes a stream of numbers and sums + up every five. In other words, for the input [1..50], it should result in the + sequence [15,40,65,90,115,140,165,190,215,240]. We can definitely do this with + the low-level conduit interface.

+
sum5Raw :: Resource m => Conduit Int m Int
+sum5Raw =
+    conduitState (0, 0) push close
+  where
+    push (total, count) input
+        | newCount == 5 = return $ StateProducing (0, 0) [newTotal]
+        | otherwise     = return $ StateProducing (newTotal, newCount) []
+      where
+        newTotal = total + input
+        newCount = count + 1
+    close (total, count)
+        | count == 0 = return []
+        | otherwise  = return [total]
+
+

But this is frustrating, since we already have all the tools we need to do this at a high + level! There's the fold sink for adding up the numbers, and the + isolate conduit which will only allow up to a certain number of elements to be + passed to a sink. Can't we combine these somehow?

+

The answer is a SequencedSink. The idea is to create a normal + Sink, except it returns a special output called a + SequencedSinkResponse. This value can emit new output, stop processing data, or + transfer control to a new conduit. (See the Haddocks for more information.) Then we can turn this + into a Conduit using the sequenceSink function. This function + also takes some state value that gets passed through to the sink.

+

So we can rewrite sum5Raw in a much more high-level manner.

+
sum5 :: Resource m => Conduit Int m Int
+sum5 = sequenceSink () $ \() -> do
+    nextSum <- CL.isolate 5 =$ CL.fold (+) 0
+    return $ Emit () [nextSum]
+
+

All of the () in there are simply the unused state variable being passed + around, they can be ignored. Otherwise, we're doing exactly what we want. We fuse + isolate to fold to get the sum of the next five elements from + the stream. We then emit that value, and start all over again.

+

Let's say we want to modify this slightly. We want to get the first 8 sums, and then pass + through the remaining values, multiplied by 2. We can keep track of how many values we've + returned in our state, and then use the StartConduit constructor to pass control + to the multiply-by-2 conduit next.

+
sum5Pass :: Resource m => Conduit Int m Int
+sum5Pass = sequenceSink 0 $ \count -> do
+    if count == 8
+        then return $ StartConduit $ CL.map (* 2)
+        else do
+            nextSum <- CL.isolate 5 =$ CL.fold (+) 0
+            return $ Emit (count + 1) [nextSum]
+
+

These are obviously very contrived examples, but I hope it makes clear the power and simplicity + available from this approach.

+
+
+
+

Buffering

+

+

Buffering is one of the unique features of conduits. With buffering, conduits no longer need to + control the flow of your application. In some cases, this can lead to simpler code.

+
+

Inversion of Control

+

+

Buffering was actually one of the main motivations in the creation of the + conduit package. To see its importance, we need to consider the approach we've + seen so far, which we'll call inversion of control, or IoC.

+ +

Suppose you want to count how many newline characters there are in a file. In the standard + imperative approach, you would do someting like:

+
    +
  1. +

    Open the file

    +
  2. +
  3. +

    Pull some data into a buffer

    +
  4. +
  5. +

    Loop over the values in the buffer, incrementing a counter on each newline character

    +
  6. +
  7. +

    Return to 2

    +
  8. +
  9. +

    Close the file

    +
  10. +
+

Notice that your code is explicitly calling out to other code and that code is returning + control back to your code. You have retained full control of the flow of execution of your + program. The conduit approach we've seen so far does not work this way. Instead, you + would:

+
    +
  1. +

    Write a sink that counts newlines and adds the result to an accumulator.

    +
  2. +
  3. +

    Connect the sink to a source

    +
  4. +
+

There's no doubt in my mind that this is an easier approach. You don't have to worry about + opening and closing files or pulling data from the file. Instead, the data you need to process is + simply presented to you. This is the advantage of IoC: you can focus on specifically your piece + of the code.

+

We use this IoC approach all over Haskell: for example, instead of readMVar + and putMVar, you can use withMVar. Don't bother with + openFile and closeFile, just use withFile and + pass in a function that uses the Handle. Even C has a version of this: why + malloc and free when you could just + alloca?

+

Actually, that last one is a huge red herring. Of course you can't just use + alloca for everything. alloca only allocates memory locally on + the stack, not dynamically on the heap. There's no way to return your allocated memory outside + the current function.

+

But actually, the same restriction applies to the whole family of with + functions: you can never return an allocated resource outside of the "block". Usually this works + out just fine, but we need to recognize that this is a change in how we structure our + programs. Often times, with simple examples, this is a minor change. However, in larger settings + this can become very difficult to manage, bordering on impossible at times.

+
+

A web server

+

+

Let's say we're going to write a web server. We're going to use the following low-level + operations:

+
data Socket
+recv    :: Socket -> Int -> IO ByteString -- returns empty when the socket is closed
+sendAll :: Socket -> ByteString -> IO ()
+

We're up to the part where we need to implement the function handleConn that + handles an individual connection. It will look something like this:

+
data Request  -- request headers, HTTP version, etc
+data Response -- status code, response headers, resposne body
+type Application = Request -> IO Response
+handleConn :: Application -> Socket -> IO ()
+

What does our handleConn need to do? In broad strokes:

+
    +
  1. +

    Parse the request line

    +
  2. +
  3. +

    Parse the request headers

    +
  4. +
  5. +

    Construct the Request value

    +
  6. +
  7. +

    Pass Request to the Application and get back a + Response +

    +
  8. +
  9. +

    Send the Response over the Socket +

    +
  10. +
+

We start off by writing steps 1 and 2 manually, without using conduits. We'll do this very + simply and just assume three space-separated strings. We end up with something that looks + like:

+
data RequestLine = RequestLine ByteString ByteString ByteString
+
+parseRequestLine :: Socket -> IO RequestLine
+parseRequestLine socket = do
+    bs <- recv socket 4096
+    let (method:path:version:ignored) = S8.words bs
+    return $ RequestLine method path version
+

There are two issues here: it doesn't handle the case where there are less than three words in + the chunk of data, and it throws away any extra data. We can definitely solve both of these + issues manually, but it's very tedious. It's much easier to implement this in terms of + conduits.

+
import Data.ByteString (ByteString)
+import qualified Data.ByteString as S
+import Data.Conduit
+import qualified Data.Conduit.Binary as CB
+import qualified Data.Conduit.List as CL
+
+data RequestLine = RequestLine ByteString ByteString ByteString
+
+parseRequestLine :: Sink ByteString IO RequestLine
+parseRequestLine = do
+    let space = toEnum $ fromEnum ' '
+    let getWord = do
+            CB.dropWhile (== space)
+            bss <- CB.takeWhile (/= space) =$ CL.consume
+            return $ S.concat bss
+
+    method <- getWord
+    path <- getWord
+    version <- getWord
+    return $ RequestLine method path version
+
+

This means that our code will automatically be supplied with more data as it comes in, and any + extra data will automatically be buffered in the Source, ready for the next time + it's used. Now we can easily structure our program together, demonstrating the power of the + conduits approach:

+
import Data.ByteString (ByteString)
+import Data.Conduit
+import Data.Conduit.Network (sourceSocket)
+import Control.Monad.IO.Class (liftIO)
+import Network.Socket (Socket)
+
+data RequestLine = RequestLine ByteString ByteString ByteString
+type Headers = [(ByteString, ByteString)]
+data Request = Request RequestLine Headers
+data Response = Response
+type Application = Request -> IO Response
+
+parseRequestHeaders :: Sink ByteString IO Headers
+parseRequestHeaders = undefined
+
+parseRequestLine :: Sink ByteString IO RequestLine
+parseRequestLine = undefined
+
+sendResponse :: Socket -> Response -> IO ()
+sendResponse = undefined
+
+handleConn :: Application -> Socket -> IO ()
+handleConn app socket = do
+    req <- runResourceT $ sourceSocket socket $$ do
+        requestLine <- parseRequestLine
+        headers <- parseRequestHeaders
+        return $ Request requestLine headers
+    res <- liftIO $ app req
+    liftIO $ sendResponse socket res
+
+
+
+

Whither the request body?

+

+

This is all great, until we realize we can't read the request body. The + Application is simply given the Request, and lives in the + IO monad. It has no access whatsoever to the incoming stream of data.

+

There's an easy fix for this actually: have the Application live in the + Sink monad. This is the very approach we took with + enumerator-based WAI 0.4. However, there are two problems:

+
    +
  • +

    People find it confusing. What people expect is that the Request + value would have a requestBody value of type Source.

    +
  • +
  • +

    This makes certain kinds of usage incredibly difficult. For example, trying to write an HTTP + proxy combining WAI and http-enumerator proved to be almost impossible.

    +
  • +
+

This is the downside of inversion of control. Our code wants to be in control. It wants to be + given something to pull from, something to push to, and run with it. We need some solution to the + problem.

+ +

The simplest solution would be to just create a new Source and pass that to + the Application. Unfortunately, this will cause problems with our buffering. You + see, when we connect our source to the parseRequestLine and + parseRequestHeaders sinks, it made a call to recv. If the data + it received was not enough to cover all of the headers, it would issue another call. When it had + enough data, it would stop. However, odds are that it didn't stop exactly at the end of + the headers. It likely consumed a bit of the request body as well.

+

If we just create a new source and pass that to the request, it will be missing the beginning + of the request body. We need some way to pass that buffered data along.

+
+
+
+

BufferedSource

+

+

And so we finally get to introduce the last data type in conduits: + BufferedSource. This is an abstract data type, but all it really does is keep a + mutable reference to a buffer and an underlying Source. In order to create one + of these, you use the bufferSource function.

+
bufferSource ::Resource m => Source m a -> ResourceT m (BufferedSource m a)
+

This one little change is what allows us to easily solve our web server dilemna. Instead of + connecting a Source to our parsing Sinks, we use a + BufferedSource. At the end of each connection, any leftover data is put back on + the buffer. For our web server case, we can now create a BufferedSource, use + that to read the request line and headers, and then pass that same + BufferedSource to the application for reading the request body.

+
+
+

Typeclass

+

+

We want to be able to connect a buffered source to a sink, just like we would a regular source. + We would also like to be able to fuse it to a conduit. In order to make this convenient, conduit + has a typeclass, IsSource. There are instances provided for both + Source and BufferedSource. Both the connect + ($$) and left-fuse ($=) operators use this typeclass.

+

There's one "gotcha" in the BufferedSource instance of this typeclass, so + let's explain it. Suppose we want to write a file copy function, without any buffering. This is a + fairly standard usage of conduits:

+
sourceFile input $$ sinkFile output
+

When this line is run, both the input and output files are opened, the data is copied, and then + both files are closed. Let's change this example slightly to use buffering:

+
bsrc <- bufferSource $ sourceFile input
+bsrc $$ isolate 50 =$ sinkFile output1
+bsrc $$ sinkFile output2
+

When is the input file opened and closed? The opening occurs on the first line, when buffering + the source. And if we follow the normal rules from sources, the file should be closed after the + second line. However, if we did that, we couldn't reuse bsrc for line 3!

+

So instead, $$ does not close the file. As a result, you can pass a + buffered source to as many actions as you want, without concerns that the file handle has been + closed out from under you.

+ +

This presents one caveat: when you're finished with a buffered source, you should manually call + bsourceClose on it. However, as usual, this is merely an optimization, as the + source will automatically be closed when runResourceT is called.

+
+
+

Recapping the web server

+

+

So what exactly does our web server look like now?

+
import Data.ByteString (ByteString)
+import Data.Conduit
+import Data.Conduit.Network (sourceSocket)
+import Control.Monad.IO.Class (liftIO)
+import Network.Socket (Socket)
+
+data RequestLine = RequestLine ByteString ByteString ByteString
+type Headers = [(ByteString, ByteString)]
+data Request = Request RequestLine Headers (BufferedSource IO ByteString)
+data Response = Response
+type Application = Request -> ResourceT IO Response
+
+parseRequestHeaders :: Sink ByteString IO Headers
+parseRequestHeaders = undefined
+
+parseRequestLine :: Sink ByteString IO RequestLine
+parseRequestLine = undefined
+
+sendResponse :: Socket -> Response -> IO ()
+sendResponse = undefined
+
+handleConn :: Application -> Socket -> IO ()
+handleConn app socket = runResourceT $ do
+    bsrc <- bufferSource $ sourceSocket socket
+    requestLine <- bsrc $$ parseRequestLine
+    headers <- bsrc $$ parseRequestHeaders
+    let req = Request requestLine headers bsrc
+    res <- app req
+    liftIO $ sendResponse socket res
+
+

We've made a few minor changes. Firstly, the Application now lives in the + ResourceT IO monad. This isn't strictly necessary, but it's very convenient: + the application can now register cleanup actions that will only take place after the response has + been fully sent to the client.

+

But the major changes are in the handleConn function. We now start off by + buffering our source. This buffered source is then used twice in our function, and then passed + off to the application.

+
+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/creating-a-subsite.html b/public/book-1.1/creating-a-subsite.html new file mode 100644 index 00000000..b7ff40e4 --- /dev/null +++ b/public/book-1.1/creating-a-subsite.html @@ -0,0 +1,153 @@ + Creating a Subsite :: Yesod Web Framework Book- Version 1.1 +
+

Creating a Subsite

+ + +

+

+

How many sites provide authentication systems? Or need to provide CRUD (CRUD) management of some objects? Or a blog? Or a wiki?

+

The theme here is that many websites include common components that can be reused throughout multiple sites. However, it is often quite difficult to get code to be modular enough to be truly plug-and-play: a component will require hooks into the routing system, usually for multiple routes, and will need some way of sharing styling information with the master site.

+

In Yesod, the solution is subsites. A subsite is a collection of routes and their handlers that can be easily inserted into a master site. By using type classes, it is easy to ensure that the master site provides certain capabilities, and to access the default site layout. And with type-safe URLs, it's easy to link from the master site to subsites.

+
+

Hello World

+

+

Writing subsites is a little bit tricky, involving a number of different types. Let's start off with a simple Hello World subsite:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-}
+{-# LANGUAGE TemplateHaskell, FlexibleInstances, OverloadedStrings #-}
+import Yesod
+
+-- Subsites have foundations just like master sites.
+data HelloSub = HelloSub
+
+-- We have a familiar analogue from mkYesod, with just one extra parameter.
+-- We'll discuss that later.
+mkYesodSub "HelloSub" [] [parseRoutes|
+/ SubRootR GET
+|]
+
+-- And we'll spell out the handler type signature.
+getSubRootR :: Yesod master => GHandler HelloSub master RepHtml
+getSubRootR = defaultLayout [whamlet|Welcome to the subsite!|]
+
+-- And let's create a master site that calls it.
+data Master = Master
+    { getHelloSub :: HelloSub
+    }
+
+mkYesod "Master" [parseRoutes|
+/ RootR GET
+/subsite SubsiteR HelloSub getHelloSub
+|]
+
+instance Yesod Master
+
+-- Spelling out type signature again.
+getRootR :: GHandler sub Master RepHtml -- could also replace sub with Master
+getRootR = defaultLayout [whamlet|
+<h1>Welcome to the homepage
+<p>
+    Feel free to visit the #
+    <a href=@{SubsiteR SubRootR}>subsite
+    \ as well.
+|]
+
+main = warpDebug 3000 $ Master HelloSub
+
+

This very simple example actually shows most of the complications involved in creating a subsite. Like a normal Yesod application, everything in a subsite is centered around a foundation datatype, HelloSub in our case. We then use mkYesodSub, in much the same way that we use mkYesod, to create the route datatype and the dispatch/render functions. (We'll come back to that extra parameter in a second.)

+

What's interesting is the type signature of getSubRootR. Up until now, we have tried to ignore the GHandler datatype, or if we need to acknowledge its existence, pretend like the first two type arguments are always the same. Now we get to finally acknowledge the truth about this funny datatype.

+

A handler function always has two foundation types associated with it: the subsite and the master site. When you write a normal application, those two datatypes are the same. However, when you are working in a subsite, they will necessarily be different. So the type signature for getSubRootR uses HelloSub for the first argument and master for the second.

+

The defaultLayout function is part of the Yesod typeclass. Therefore, in order to call it, the master type argument must be an instance of Yesod. The advantage of this approach is that any modifications to the master site's defaultLayout method will automatically be reflected in subsites.

+

When we embed a subsite in our master site route definition, we need to specify four pieces of information: the route to use as the base of the subsite (in this case, /subsite), the constructor for the subsite routes (SubsiteR), the subsite foundation data type (HelloSub) and a function that takes a master foundation value and returns a subsite foundation value (getHelloSub).

+

In the definition of getRootR, we can see how the route constructor gets used. In a sense, SubsiteR promotes any subsite route to a master site route, making it possible to safely link to it from any master site template.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/deploying-your-webapp.html b/public/book-1.1/deploying-your-webapp.html new file mode 100644 index 00000000..03b3fc66 --- /dev/null +++ b/public/book-1.1/deploying-your-webapp.html @@ -0,0 +1,364 @@ + Deploying your Webapp :: Yesod Web Framework Book- Version 1.1 +
+

Deploying your Webapp

+ + +

+

+

I can't speak for others, but I personally prefer programming to system administration. But the fact is that, eventually, you need to serve your app somehow, and odds are that you'll need to be the one to set it up.

+

There are some promising initiatives in the Haskell web community towards making deployment easier. In the future, we may even have a service that allows you to deploy your app with a single command.

+

But we're not there yet. And even if we were, such a solution will never work for everyone. This chapter covers the different options you have for deployment, and gives some general recommendations on what you should choose in different situations.

+
+

Compiling

+

+

First things first: how do you build your production application? If you're using the + scaffolded site, it's as simple as cabal build. I also recommend cleaning + beforehand to make sure there is no cached information, so a simple combination to build your + executable is:

+
cabal clean && cabal configure && cabal build
+
+
+

Warp

+

+

As we have mentioned before, Yesod is built on the Web Application Interface (WAI), allowing it to run on any WAI backend. At the time of writing, the following backends are available:

+
    +
  • +

    Warp

    +
  • +
  • +

    FastCGI

    +
  • +
  • +

    SCGI

    +
  • +
  • +

    CGI

    +
  • +
  • +

    Webkit

    +
  • +
  • +

    Development server

    +
  • +
+

The last two are not intended for production deployments. Of the remaining four, all + can be used for production deployment in theory. In practice, a CGI backend will likely + be horribly inefficient, since a new process must be spawned for each connection. And + SCGI is not nearly as well supported by frontend web servers as Warp (via reverse + proxying) or FastCGI.

+

So between the two remaining choices, Warp gets a very strong recommendation because:

+
    +
  • +

    It is significantly faster.

    +
  • +
  • +

    Like FastCGI, it can run behind a frontend server like Nginx, using reverse HTTP proxy.

    +
  • +
  • +

    In addition, it is a fully capable server of its own accord, and can therefore be used without any frontend server.

    +
  • +
+

So that leaves one last question: should Warp run on its own, or via reverse proxy behind + a frontend server? For most use cases, I recommend the latter, because:

+
    +
  • +

    As fast as Warp is, it is still optimized as an application server, not a static + file server.

    +
  • +
  • +

    Using Nginx, you can set up virtual hosting to serve your static contents from a + separate domain. (It's possible to do this with Warp, but a bit more involved).

    +
  • +
  • +

    You can use Nginx as either a load balancer or a SSL proxy. (Though with + warp-tls it's entirely possible to run an https site on Warp + alone.)

    +
  • +
+

So my final recommendation is: set up Nginx to reverse proxy to Warp.

+ +
+

Configuration

+

+

In general, Nginx will listen on port 80 and your Yesod/Warp app will listen on some unprivileged port (lets say 4321). You will then need to provide a nginx.conf file, such as:

+
daemon off; # Don't run nginx in the background, good for monitoring apps
+events {
+    worker_connections 4096;
+}
+
+http {
+    server {
+        listen 80; # Incoming port for Nginx
+        server_name www.myserver.com;
+        location / {
+            proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+        }
+    }
+}
+

You can add as many server blocks as you like. A common addition is to ensure users always access your pages with the www prefix on the domain name, ensuring the RESTful principle of canonical URLs. (You could just as easily do the opposite and always strip the www, just make sure that your choice is reflected in both the nginx config and the approot of your site.) In this case, we would add the block:

+
server {
+    listen 80;
+    server_name myserver.com;
+    rewrite ^/(.*) http://www.myserver.com/$1 permanent;
+}
+

A highly recommended optimization is to serve static files from a separate domain + name, therefore bypassing the cookie transfer overhead. Assuming that our static files + are stored in the static folder within our site folder, and the + site folder is located at /home/michael/sites/mysite, this + would look like:

+
server {
+    listen 80;
+    server_name static.myserver.com;
+    root /home/michael/sites/mysite/static;
+    # Since yesod-static appends a content hash in the query string,
+    # we are free to set expiration dates far in the future without
+    # concerns of stale content.
+    expires max;
+}
+

In order for this to work, your site must properly rewrite static URLs to this + alternate domain name. The scaffolded site is set up to make this fairly simple via the + Settings.staticRoot function and the definition of + urlRenderOverride. However, if you just want to get the benefit of + nginx's faster static file serving without dealing with separate domain names, you can + instead modify your original server block like so:

+
server {
+    listen 80; # Incoming port for Nginx
+    server_name www.myserver.com;
+    location / {
+        proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+    }
+    location /static {
+        root /home/michael/sites/mysite; # Notice that we do *not* include /static
+        expires max;
+    }
+}
+
+
+

Server Process

+

+

Many people are familiar with an Apache/mod_php or Lighttpd/FastCGI kind of setup, + where the web server automatically spawns the web application. With nginx, either for + reverse proxying or FastCGI, this is not the case: you are 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.

+
+
+
+

FastCGI

+

+

Some people may prefer using FastCGI for deployment. In this case, you'll need to add an extra tool to the mix. FastCGI works by receiving new connection from a file descriptor. The C library assumes that this file descriptor will be 0 (standard input), so you need to use the spawn-fcgi program to bind your application's standard input to the correct socket.

+

It can be very convenient to use Unix named sockets for this instead of binding to a port, especially when hosting multiple applications on a single host. A possible script to load up your app could be:

+
spawn-fcgi \
+    -d /home/michael/sites/mysite \
+    -s /tmp/mysite.socket \
+    -n \
+    -M 511 \
+    -u michael \
+    -- /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi
+

You will also need to configure your frontend server to speak to your app over FastCGI. This is relatively painless in Nginx:

+
server {
+    listen 80;
+    server_name www.myserver.com;
+    location / {
+        fastcgi_pass unix:/tmp/mysite.socket;
+    }
+}
+

That should look pretty familiar from above. The only last trick is that, with Nginx, you need to manually specify all of the FastCGI variables. It is recommended to store these in a separate file (say, fastcgi.conf) and then add include fastcgi.conf; to the end of your http block. The contents of the file, to work with WAI, should be:

+
fastcgi_param  QUERY_STRING       $query_string;
+fastcgi_param  REQUEST_METHOD     $request_method;
+fastcgi_param  CONTENT_TYPE       $content_type;
+fastcgi_param  CONTENT_LENGTH     $content_length;
+fastcgi_param  PATH_INFO          $fastcgi_script_name;
+fastcgi_param  SERVER_PROTOCOL    $server_protocol;
+fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
+fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
+fastcgi_param  REMOTE_ADDR        $remote_addr;
+fastcgi_param  SERVER_ADDR        $server_addr;
+fastcgi_param  SERVER_PORT        $server_port;
+fastcgi_param  SERVER_NAME        $server_name;
+
+
+
+

Desktop

+

+

Another nifty backend is wai-handler-webkit. This backend combines Warp and QtWebkit to create an executable that a user simply double-clicks. This can be a convenient way to provide an offline version of your application.

+

One of the very nice conveniences of Yesod for this is that your templates are all compiled into the executable, and thus do not need to be distributed with your application. Static files do, however.

+ +

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 doesn't receive a ping for two minutes, it shuts + down.

+
+
+

CGI on Apache

+

+

CGI and FastCGI work almost identically on Apache, so it should be fairly straight-forward to port this configuration. You essentially need to accomplish two goals:

+
    +
  1. +

    Get the server to serve your file as (Fast)CGI.

    +
  2. +
  3. +

    Rewrite all requests to your site to go through the (Fast)CGI executable.

    +
  4. +
+

Here is a configuration file for serving a blog application, with an executable named "bloggy.cgi", living in a subfolder named "blog" of the document root. This example was taken from an application living in the path /f5/snoyman/public/blog.

+
Options +ExecCGI
+AddHandler cgi-script .cgi
+Options +FollowSymlinks
+
+RewriteEngine On
+RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1]
+RewriteCond $1 !^bloggy.cgi
+RewriteCond $1 !^static/
+RewriteRule ^(.*) bloggy.cgi/$1 [L]
+

The first RewriteRule is to deal with subfolders. In particular, it redirects a request for /blog to /blog/. The first RewriteCond prevents directly requesting the executable, the second allows Apache to serve the static files, 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." when absent and serves static files from a separate domain. However, this should serve to show the basics.

+

Here, "/home/michael/fastcgi" is the fastcgi application. The idea is to rewrite all requests to start with "/app", and then serve everything beginning with "/app" via the FastCGI executable.

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_fastcgi", "mod_rewrite")
+
+url.rewrite-once = (
+  "(.*)" => "/app/$1"
+)
+
+fastcgi.server = (
+    "/app" => ((
+        "socket" => "/tmp/test.fastcgi.socket",
+        "check-local" => "disable",
+        "bin-path" => "/home/michael/fastcgi", # full path to executable
+        "min-procs" => 1,
+        "max-procs" => 30,
+        "idle-timeout" => 30
+    ))
+)
+
+
+

CGI on lighttpd

+

+

This is basically the same as the FastCGI version, but tells lighttpd to run a file ending in ".cgi" as a CGI executable. In this case, the file lives at "/home/michael/myapp.cgi".

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_cgi", "mod_rewrite")
+
+url.rewrite-once = (
+    "(.*)" => "/myapp.cgi/$1"
+)
+
+cgi.assign = (".cgi" => "")
+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/forms.html b/public/book-1.1/forms.html new file mode 100644 index 00000000..ef962009 --- /dev/null +++ b/public/book-1.1/forms.html @@ -0,0 +1,961 @@ + Forms :: Yesod Web Framework Book- Version 1.1 +
+

Forms

+ + +

+

+

I've mentioned the boundary issue already: whenever data enters or leaves an + application, we need to validate it. Probably the most difficult place this occurs is forms. + Coding forms is complex; in an ideal world, we'd like a solution that addresses the following + problems:

+
    +
  • +

    Ensure data is valid.

    +
  • +
  • +

    Marshal string data in the form submission to Haskell datatypes.

    +
  • +
  • +

    Generate HTML code for displaying the form.

    +
  • +
  • +

    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.

    +
  • +
  • +

    Automatically assign names to our fields that are guaranteed to be unique.

    +
  • +
+

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 Haskell's type system to make sure everything + is working correctly.

+
+

Synopsis

+

+
{-# LANGUAGE QuasiQuotes, TemplateHaskell, MultiParamTypeClasses,
+    OverloadedStrings, TypeFamilies #-}
+import Yesod
+import Yesod.Form.Jquery
+import Data.Time (Day)
+import Data.Text (Text)
+import Control.Applicative ((<$>), (<*>))
+
+data Synopsis = Synopsis
+
+mkYesod "Synopsis" [parseRoutes|
+/ RootR GET
+/person PersonR POST
+|]
+
+instance Yesod Synopsis
+
+-- Tells our application to use the standard English messages.
+-- If you want i18n, then you can supply a translating function instead.
+instance RenderMessage Synopsis FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- And tell us where to find the jQuery libraries. We'll just use the defaults,
+-- which point to the Google CDN.
+instance YesodJquery Synopsis
+
+-- The datatype we wish to receive from the form
+data Person = Person
+    { personName :: Text
+    , personBirthday :: Day
+    , personFavoriteColor :: Maybe Text
+    , personEmail :: Text
+    , personWebsite :: Maybe Text
+    }
+  deriving Show
+
+-- Declare the form. The type signature is a bit intimidating, but here's the
+-- overview:
+--
+-- * The Html parameter is used for encoding some extra information. See the
+-- discussion regarding runFormGet and runFormPost below for further
+-- explanation.
+--
+-- * We have the sub and master site types, as usual.
+--
+-- * FormResult can be in three states: FormMissing (no data available),
+-- FormFailure (invalid data) and FormSuccess
+--
+-- * The Widget is the viewable form to place into the web page.
+--
+-- Note that the scaffolded site provides a convenient Form type synonym,
+-- so that our signature could be written as:
+--
+-- > personForm :: Form Person
+--
+-- For our purposes, it's good to see the long version.
+personForm :: Html -> MForm Synopsis Synopsis (FormResult Person, Widget)
+personForm = renderDivs $ Person
+    <$> areq textField "Name" Nothing
+    <*> areq (jqueryDayField def
+        { jdsChangeYear = True -- give a year dropdown
+        , jdsYearRange = "1900:-5" -- 1900 till five years ago
+        }) "Birthday" Nothing
+    <*> aopt textField "Favorite color" Nothing
+    <*> areq emailField "Email address" Nothing
+    <*> aopt urlField "Website" Nothing
+
+-- The GET handler displays the form
+getRootR :: Handler RepHtml
+getRootR = do
+    -- Generate the form to be displayed
+    (widget, enctype) <- generateFormPost personForm
+    defaultLayout [whamlet|
+<p>The widget generated contains only the contents of the form, not the form tag itself. So...
+<form method=post action=@{PersonR} enctype=#{enctype}>
+    ^{widget}
+    <p>It also doesn't include the submit button.
+    <input type=submit>
+|]
+
+-- The POST handler processes the form. If it is successful, it displays the
+-- parsed person. Otherwise, it displays the form again with error messages.
+postPersonR :: Handler RepHtml
+postPersonR = do
+    ((result, widget), enctype) <- runFormPost personForm
+    case result of
+        FormSuccess person -> defaultLayout [whamlet|<p>#{show person}|]
+        _ -> defaultLayout [whamlet|
+<p>Invalid input, let's try again.
+<form method=post action=@{PersonR} enctype=#{enctype}>
+    ^{widget}
+    <input type=submit>
+|]
+
+main :: IO ()
+main = warpDebug 3000 Synopsis
+
+
+

Kinds of Forms

+

+

Before jumping into the types themselves, we should begin with an overview of the different + kinds of forms. There are three categories:

+
+
Applicative
+

These are the most commonly used (it's what appeared in the synopsis). Applicative + gives us some nice properties of letting error messages coallesce together and keep a very + high-level, declarative approach. (For more information on applicative code, see the Haskell wiki.)

+
+
Monadic
+

A more powerful alternative to applicative. While this allows you more flexibility, + it does so at the cost of being more verbose. Useful if you want to create forms that don't fit + into the standard two-column look.

+
+
Input
+

Used only for receiving input. Does not generate any HTML for receiving the user input. + Useful for interacting with existing forms.

+
+
+

In addition, there are a number of different variables that come into play for each form and + field you will want to set up:

+
    +
  • +

    Is the field required or optional?

    +
  • +
  • +

    Should it be submitted with GET or POST?

    +
  • +
  • +

    Does it have a default value, or not?

    +
  • +
+

An overriding goal is to minimize the number of field definitions and let them work in + as many contexts as possible. One result of this is that we end up with a few extra words for + each field. In the synopsis, you may have noticed things like areq and that + extra Nothing parameter. We'll cover why all of those exist in the course of + this chapter, but for now realize that by making these parameters explicit, we are able to reuse + the individuals fields (like intField) + in many different ways.

+

A quick note on naming conventions. Each form type has a one-letter prefix (A, M and I) which + is used in a few places, such as saying MForm. We also use req and opt to mean required and + optional. Combining these, we create a required applicative field with areq, or + an optional input field with iopt.

+
+
+

Types

+

+

The Yesod.Form.Types module declares a few types. Let's start off + with some simple helpers:

+
+
Enctype
+

The encoding type, either UrlEncoded or Multipart. + This datatype declares an instance of ToHtml, so you can use the enctype + directly in Hamlet.

+
+
Env
+

Maps a parameter name to a list of values.

+
+
FileEnv
+

Maps a parameter name to the associated uploaded file.

+
+
Ints
+

As mentioned in the introduction, yesod-form automatically assigns + a unique name to each field. Ints is used to keep track of the next number to + assign.

+
+
FormResult
+

Has one of three possible states: FormMissing if no data was + submitted, FormFailure if there was an error parsing the form (e.g., missing a + required field, invalid content), or FormSuccess if everything went + smoothly.

+
+
+

Next we have three datatypes used for defining individual fields.

+ +
+
Field
+

Defines two pieces of functionality: how to parse the text input from a user into a + Haskell value, and how to create the widget to be displayed to the user. + yesod-form defines a number of individual Fields in Yesod.Form.Fields.

+
+
FieldSettings
+

Basic information on how a field should be displayed, such as the display name, an + optional tooltip, and possibly hardcoded id and name + attributes. (If none are provided, they are automatically generated.)

+

+
+
FieldView
+

An intermediate format containing a bunch of view information on a field. This is hardly + ever used directly by the user, we'll see more details later.

+
+
+

And finally, we get to the important stuff: the forms themselves. There are three + types for this: MForm is for monadic forms, AForm for + applicative and IForm (declared in IForm) for input. MForm is actually a + type synonym for a monad stack that provides the following features:

+
    +
  • +

    A Reader monad giving us the parameters (Env and + FileEnv), the master site argument and the list of languages the user + supports. The last two are used for i18n (more on this later).

    +
  • +
  • +

    A Writer monad keeping track of the Enctype. A + form will always be UrlEncoded, unless there is a file input field, which will + force us to use multipart instead.

    +
  • +
  • +

    A State monad holding an Ints to keep track of the + next unique name to produce.

    +
  • +
+

An AForm is pretty similar. However, there are a few major + differences:

+
    +
  • +

    It produces a list of FieldViews. This allows us to keep an + abstract idea of the form display, and then at the end of the day choose an appropriate function + for laying it out on the page. In the synopsis, we used renderDivs, which + creates a bunch of div tags. Another option would be renderTable.

    +
  • +
  • +

    It does not provide a Monad instance. The goal of + Applicative is to allow the entire form to run, grab as much information on + each field as possible, and then create the final result. This cannot work in the context of + Monad.

    +
  • +
+

An IForm is even simpler: it returns either a list of error messages + or a result.

+
+
+

Converting

+

+

"But wait a minute," you say. "You said the synopsis uses applicative forms, but I'm + sure the type signature said MForm. Shouldn't it be Monadic?" That's true, the + final form we produced was monadic. But what really happened is that we converted an applicative + form to a monadic one.

+

Again, our goal is to reuse code as much as possible, and minimize the number of + functions in the API. And Monadic forms are more powerful than Applicative, if more clumsy, so + anything that can be expressed in an Applicative form could also be expressed in a Monadic form. + There are two core functions that help out with this: aformToForm converts any + applicative form to a monadic one, and formToAForm converts certain kinds of + monadic forms to applicative forms.

+

"But wait another minute," you insist. "I didn't see any + aformToForm!" Also true. The renderDivs function takes care of + that for us.

+
+
+

Create AForms

+

+

Now that I've (hopefully) convinced you that in our synopsis we were really dealing with + applicative forms, let's have a look and try to understand how these things get created. Let's + take a simple example:

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    }
+  deriving Show
+
+carAForm :: AForm Synopsis Synopsis Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+
+carForm :: Html -> MForm Synopsis Synopsis (FormResult Car, Widget)
+carForm = renderTable carAForm
+
+

Here, we've explicitly split up applicative and monadic forms. In carAForm, we use the <$> and <*> operators. This should not be surprising; these are + almost always used in applicative-style code. And we have one line for each record in + our Car datatype. Perhaps unsurprisingly, we have a textField for the Text record, and an + intField for the Int + record.

+

Let's look a bit more closely at the areq function. Its (simplified) + type signature is Field a -> FieldSettings -> Maybe a -> AForm + a. So that first argument is going to determine the datatype of this field, + how to parse it, and how to render it. The next argument, + FieldSettings, tells us the label, tooltip, name and ID of the field. + In this case, we're using the previously-mentioned IsString instance of + FieldSettings.

+

And what's up with that Maybe a? It provides the optional default + value. For example, if we want our form to fill in "2007" as the default car year, we + would use areq intField "Year" (Just 2007). We can even take + this to the next level, and have a form that takes an optional parameter giving the + default values.

+
+

Form with default values

+
carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField "Year" (carYear <$> mcar)
+
+
+
+

Optional fields

+

+

Suppose we wanted to have an optional field (like the car color). All we do instead is + use the aopt function.

+
+

Optional fields

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Text
+    }
+  deriving Show
+
+carAForm :: AForm Synopsis Synopsis Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+    <*> aopt textField "Color" Nothing
+
+
+

And like required fields, the last argument is the optional default value. However, + this has two layers of Maybe wrapping. This may seem redundant (and it is), but it makes + it much easier to write code that takes an optional default form parameter, such as in + the next example.

+
+

Default optional fields

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Text
+    }
+  deriving Show
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+    <*> aopt textField "Color" (carColor <$> mcar)
+
+carForm :: Html -> MForm Synopsis Synopsis (FormResult Car, Widget)
+carForm = renderTable $ carAForm $ Just $ Car "Forte" 2010 $ Just "gray"
+
+
+
+
+
+

Validation

+

+

How would we make our form only accept cars created after 1990? If you remember, we + said above that the Field itself contained the information on what is a + valid entry. So all we need to do is write a new Field, right? Well, + that would be a bit tedious. Instead, let's just modify an existing one:

+
carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField    "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year"  (carYear  <$> mcar)
+    <*> aopt textField    "Color" (carColor <$> mcar)
+  where
+    errorMessage :: Text
+    errorMessage = "Your car is too old, get a new one!"
+
+    carYearField = check validateYear intField
+
+    validateYear y
+        | y < 1990 = Left errorMessage
+        | otherwise = Right y
+
+

The trick here is the check function. It takes a function + (validateYear) that returns either an error message or a modified + field value. In this example, we haven't modified the value at all. That is usually + going to be the case. This kind of checking is very common, so we have a shortcut:

+
    carYearField = checkBool (>= 1990) errorMessage intField
+
+

+ checkBool takes two parameters: a condition that must be fulfilled, + and an error message to be displayed if it was not.

+ +

It's great to make sure the car isn't too old. But what if we want to make sure that + the year specified is not from the future? In order to look up the current year, we'll + need to run some IO. For such circumstances, we'll need + checkM:

+
    carYearField = checkM inPast $ checkBool (>= 1990) errorMessage intField
+
+    inPast y = do
+        thisYear <- liftIO getCurrentYear
+        return $ if y <= thisYear
+            then Right y
+            else Left ("You have a time machine!" :: Text)
+
+getCurrentYear :: IO Int
+getCurrentYear = do
+    now <- getCurrentTime
+    let today = utctDay now
+    let (year, _, _) = toGregorian today
+    return $ fromInteger year
+
+

+ inPast is a function that will return an Either + result. However, it uses a Handler monad. We use liftIO getCurrentYear to get the current year and then compare it against + the user-supplied year. Also, notice how we can chain together multiple validators.

+ +
+
+

More sophisticated fields

+

+

Our color entry field is nice, but it's not exactly user-friendly. What we really want + is a drop-down list.

+
+

Drop-down lists

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectFieldList colors) "Color" (carColor <$> mcar)
+  where
+    colors :: [(Text, Color)]
+    colors = [("Red", Red), ("Blue", Blue), ("Gray", Gray), ("Black", Black)]
+
+
+

+ selectFieldList takes a list of pairs. The first item in the pair is + the text displayed to the user in the drop-down list, and the second item is the actual + Haskell value. Of course, the code above looks really repetitive; we can get the same + result using the Enum and Bounded instance GHC automatically derives for us.

+
+

Uses Enum and Bounded

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectFieldList colors) "Color" (carColor <$> mcar)
+  where
+    colors = map (pack . show &&& id) $ [minBound..maxBound]
+
+
+

+ [minBound..maxBound] gives us a list of all the different + Color values. We then apply a map and + &&& (a.k.a, the fan-out operator) to turn that into a + list of pairs.

+

Some people prefer radio buttons to drop-down lists. Fortunately, this is just a + one-word change. For example, see Radio buttons

+
+

Radio buttons

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Synopsis Synopsis Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (radioFieldList colors) "Color" (carColor <$> mcar)
+  where
+    colors = map (pack . show &&& id) $ [minBound..maxBound]
+
+
+
+
+

Running forms

+

+

At some point, we're going to need to take our beautiful forms and produce some results. There + are a number of different functions available for this, each with its own purpose. I'll go + through them, starting with the most common.

+
+
runFormPost
+

This will run your form against any submitted POST + parameters. If this is not a POST submission, it will return a + FormMissing. This automatically inserts a security token as a hidden + form field to avoid CSRF attacks.

+
+
runFormGet
+

The equivalent of runFormPost for GET parameters. In order to + distinguish a normal GET page load from a GET submission, it includes an extra _hasdata hidden field + in the form. Also, unlike runFormPost, it does not include CSRF protection.

+
+
runFormPostNoNonce
+

Same as runFormPost, but does not include (or require) the CSRF + security token.

+
+
generateFormPost
+

Instead of binding to existing POST parameters, acts as if there + are none. This can be useful when you want to generate a new form after a previous form was + submitted, such as in a wizard.

+
+
generateFormGet
+

Same as generateFormPost, but for GET.

+
+
+

The return type from the first three is ((FormResult a, Widget), + Enctype). The Widget will already have any validation errors and + previously submitted values.

+
+
+

i18n

+

+

There have been a few references to i18n in this chapter. The topic will get more + thorough coverage in its + own chapter, but since it has such a profound effect on yesod-form, I + wanted to give a brief overview. The idea behind i18n in Yesod is to have data types represent + messages. Each site can have an instance of RenderMessage for a given datatype + which will translate that message based on a list of languages the user accepts. As a result of + all this, there are a few things you should be aware of:

+
    +
  • +

    There is an automatic instance of RenderMessage for + Text in every site, so you can just use plain strings if you don't care about + i18n support. However, you may need to use explicit type signatures occassionally.

    +
  • +
  • +

    + yesod-form expresses all of its messages in terms of the + FormMessage datatype. Therefore, to use yesod-form, you'll + need to have an appropriate RenderMessage instance. A simple one that uses the + default English translations would + be:

    instance RenderMessage MyApp FormMessage where
    +    renderMessage _ _ = defaultFormMessage
    This + is provided automatically by the scaffolded site.

    +
  • + +
+
+
+

Monadic Forms

+

+

Often times, a simple form layout is adequate, and applicative forms excel at + this approach. Sometimes, however, you'll want to have a more customized look to your + form.

+
+

A non-standard form layout

+ + + + + +
+

For these use cases, monadic forms fit the bill. They are a bit more verbose + than their applicative cousins, but this verbosity allows you to have complete control + over what the form will look like. In order to generate the form above, we could code + something like this.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, MultiParamTypeClasses #-}
+import Yesod
+import Control.Applicative
+import Data.Text (Text)
+
+data MFormExample = MFormExample
+
+mkYesod "MFormExample" [parseRoutes|
+/ RootR GET
+|]
+
+instance Yesod MFormExample
+
+instance RenderMessage MFormExample FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person { personName :: Text, personAge :: Int }
+    deriving Show
+
+personForm :: Html -> MForm MFormExample MFormExample (FormResult Person, Widget)
+personForm extra = do
+    (nameRes, nameView) <- mreq textField "this is not used" Nothing
+    (ageRes, ageView) <- mreq intField "neither is this" Nothing
+    let personRes = Person <$> nameRes <*> ageRes
+    let widget = do
+            toWidget [lucius|
+##{fvId ageView} {
+    width: 3em;
+}
+|]
+            [whamlet|
+#{extra}
+<p>
+    Hello, my name is #
+    ^{fvInput nameView}
+    \ and I am #
+    ^{fvInput ageView}
+    \ years old. #
+    <input type=submit value="Introduce myself">
+|]
+    return (personRes, widget)
+
+getRootR :: Handler RepHtml
+getRootR = do
+    ((res, widget), enctype) <- runFormGet personForm
+    defaultLayout [whamlet|
+<p>Result: #{show res}
+<form enctype=#{enctype}>
+    ^{widget}
+|]
+
+main :: IO ()
+main = warpDebug 3000 MFormExample
+

Similar to the applicative areq, we use mreq for monadic forms. (And yes, there's also + mopt for optional fields.) But there's a big difference: + mreq gives us back a pair of values. Instead of hiding away the + FieldView value and + automatically inserting it into a widget, we get the control to insert it as we see + fit.

+

+ FieldView has a number of pieces of information. The most + important is fvInput, which is the actual form field. In this example, + we also use fvId, which gives us back the HTML id + attribute of the input tag. In our example, we use that to specify the width of the + field.

+

You might be wondering what the story is with the "this is not used" and + "neither is this" values. mreq takes a FieldSettings as its second argument. Since FieldSettings + provides an IsString instance, the strings are essentially expanded by + the compiler + to:

fromString "this is not used" == FieldSettings
+    { fsLabel = "this is not used"
+    , fsTooltip = Nothing
+    , fsId = Nothing
+    , fsName = Nothing
+    , fsClass = []
+    }
In + the case of applicative forms, the fsLabel and + fsTooltip values are used when constructing your HTML. In the case + of monadic forms, Yesod does not generate any of the "wrapper" HTML for you, and + therefore these values are ignored. However, we still keep the + FieldSettings parameter to allow you to override the + id and name attributes of your fields if + desired.

+

The other interesting bit is the extra value. + GET forms include an extra field to indicate that they + have been submitted, and POST forms include a security tokens + to prevent CSRF attacks. If you don't include this extra hidden field in your form, + Yesod will not accept it.

+

Other than that, things are pretty straight-forward. We create our + personRes value by combining together the nameRes + and ageRes values, and then return a tuple of the person and the + widget. And in the getRootR function, everything looks just like an + applicative form. In fact, you could swap out our monadic form with an applicative one + and the code would still work.

+
+
+

Input forms

+

+

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 forms.

+

These work mostly the same as applicative and monadic forms, with some differences:

+
    +
  • +

    You use runInputPost and runInputGet.

    +
  • +
  • +

    You use ireq and iopt. These functions now only + take two arguments: the field type and the name (i.e., HTML name attribute) of + the field in question.

    +
  • +
  • +

    After running a form, it returns the value. It doesn't return a widget or an + encoding type.

    +
  • +
  • +

    If there are any validation errors, the page returns an "invalid arguments" error page.

    +
  • +
+

You can use input forms to recreate the previous example. Note, however, that the + input version is less user friendly. If you make a mistake in an applicative or monadic form, you + will be brought back to the same page, with your previously entered values in the form, and an + error message explaning what you need to correct. With input forms, the user simply gets an error + message.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, MultiParamTypeClasses #-}
+import Yesod
+import Control.Applicative
+import Data.Text (Text)
+
+data Input = Input
+
+mkYesod "Input" [parseRoutes|
+/ RootR GET
+/input InputR GET
+|]
+
+instance Yesod Input
+
+instance RenderMessage Input FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person { personName :: Text, personAge :: Int }
+    deriving Show
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<form action=@{InputR}>
+    <p>
+        My name is #
+        <input type=text name=name>
+        \ and I am #
+        <input type=text name=age>
+        \ years old. #
+        <input type=submit value="Introduce myself">
+|]
+
+getInputR :: Handler RepHtml
+getInputR = do
+    person <- runInputGet $ Person
+                <$> ireq textField "name"
+                <*> ireq intField "age"
+    defaultLayout [whamlet|<p>#{show person}|]
+
+main :: IO ()
+main = warpDebug 3000 Input
+
+
+

Custom fields

+

+

The fields that come built-in with Yesod will likely cover the vast majority of your + form needs. But occassionally, you'll need something more specialized. Fortunately, you can + create new forms in Yesod yourself. The Field datatype has two records: + fieldParse takes a list of values submitted by the user and returns one of + three results:

+
    +
  • +

    An error message saying validation failed.

    +
  • +
  • +

    The parsed value.

    +
  • +
  • +

    Nothing, indicating that no data was supplied.

    +
  • +
+

That last case might sound surprising: shouldn't Yesod automatically know that no information + is supplied when the input list is empty? Well, no actually. Checkboxes, for instance, indicate + an unchecked state by sending in an empty list.

+

Also, what's up with the list? Shouldn't it be a Maybe? Well, that's + also not the case. With grouped checkboxes and multi-select lists, you'll have multiple widgets + with the same name. We also use this trick in our example below.

+

The second record is fieldView, and it renders a widget to display to + the user. This function has four arguments: the id attribute, the + name attribute, the result and a Bool indicating if the field + is required.

+

What did I mean by result? It's actually an Either, giving either + the unparsed input (when parsing failed) or the successfully parsed value. + intField is a great example of how this works. If you type in 42, the value of result will be Right 42. But + if you type in turtle, the result will be Left + "turtle". This lets you put in a value attribute on your input tag that will give the + user a consistent experience.

+

As a small example, we'll create a new field type that is a password confirm field. + This field has two text inputs- both with the same name attribute- and returns an error + message if the values don't match. Note that, unlike most fields, it does not provide a value attribute on the input tags, as you don't want to send back + a user-entered password in your HTML ever.

+
passwordConfirmField :: Field sub master Text
+passwordConfirmField = Field
+    { fieldParse = \rawVals ->
+        case rawVals of
+            [a, b]
+                | a == b -> return $ Right $ Just a
+                | otherwise -> return $ Left "Passwords don't match"
+            [] -> return $ Right Nothing
+            _ -> return $ Left "You must enter two values"
+    , fieldView = \idAttr nameAttr _ eResult isReq -> [whamlet|
+<input id=#{idAttr} name=#{nameAttr} type=password>
+<div>Confirm:
+<input id=#{idAttr}-confirm name=#{nameAttr} type=password>
+|]
+    }
+
+getRootR :: Handler RepHtml
+getRootR = do
+    ((res, widget), enctype) <- runFormGet $ renderDivs
+        $ areq passwordConfirmField "Password" Nothing
+    defaultLayout [whamlet|
+<p>Result: #{show res}
+<form enctype=#{enctype}>
+    ^{widget}
+    <input type=submit value="Change password">
+|]
+
+
+
+

Summary

+

+

Forms in Yesod are broken up into three groups. Applicative is the most common, as it provides + a nice user interface with an easy-to-use API. Monadic forms give you more power, but are harder + to use. Input forms are intended when you just want to read data from the user, not generate the + input widgets.

+

There are a number of different Fields provided by Yesod out-of-the-box. In + order to use these in your forms, you need to indicate the kind of form and 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 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.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/haskell.html b/public/book-1.1/haskell.html new file mode 100644 index 00000000..ac8124e1 --- /dev/null +++ b/public/book-1.1/haskell.html @@ -0,0 +1,403 @@ + Haskell :: Yesod Web Framework Book- Version 1.1 +
+

Haskell

+ + +

+

+

In order to use Yesod, you're going to have to know at least the basics of Haskell. + Additionally, Yesod uses some features of Haskell that aren't covered in most introductory texts. + While this book assumes the reader has a basic familiarity with Haskell, this chapter is intended + to fill in the gaps.

+

If you are already fluent in Haskell, feel free to completely skip this chapter. Also, if you + would prefer to start off by getting your feet wet with Yesod, you can always come back to this + chapter later as a reference.

+

If you are looking for a more thorough introduction to Haskell, I would recommend either Real + World Haskell or Learn You a Haskell.

+
+

Terminology

+

+

Even for those familiar with Haskell as a language, there can sometimes be some + confusion about terminology. Let's establish some base terms that we can use throughout this + book.

+
+
Data type
+

This is one of the core building blocks for a strongly typed language like Haskell. Some + data types, like Int, can be treated as primitive values, while other data + types will build on top of these to create more complicated values. For example, you might + represent a person + with:

data Person = Person Text Int
Here, the + Text would give the person's name, and the Int would give + the person's age. Due to its simplicity, this specific example type will recur throughout the + book. There are essentially three ways you can create a new data type:
    +
  • +

    A type declaration such as type GearCount = Int merely + creates a synonym for an existing type. The type system will do nothing to prevent you from + using an Int where you asked for a GearCount. Using this + can make your code more self-documenting.

    +
  • +
  • +

    A newtype declaration such as newtype Make = Make Text. + In this case, you cannot accidently use a Text in place of a + Make; the compiler will stop you. The newtype wrapper always disappears + during compilation, and will introduce no overhead.

    +
  • +
  • +

    A data declaration, such as Person above. You can also + create Algebraic Data Types (ADTs), such as data Vehicle = Bicycle GearCount | Car + Make Model.

    +
  • +
+

+
+
Data constructor
+

In our examples above, Person, Make, + Bicycle, and Car are all data constructors.

+
+
Type constructor
+

In our examples above, Person, Make, and + Vehicle are all type constructors.

+
+
Type variables
+

Consider the data type data Maybe a = Just a | Nothing. In this case, + a is a type variable.

+
+
+
+
+

Tools

+

+

There are two main tools you'll need to Haskell development. The Glasgow Haskell Compiler (GHC) + is the standard Haskell compiler, and the only one officially supported by Yesod. You'll also + need Cabal, which is the standard Haskell build tool. Not only do we use Cabal for building our + local code, but it can automatically download and install dependencies from Hackage, the Haskell + package repository.

+

If you're on Windows or Mac, it is strongly recommended to download the Haskell + Platform. On Linux, many distributions include the Haskell Platform in their + repositories. On Debian-based systems, for example, you can get started by running sudo + apt-get install haskell-platform. If your distribution does not include the Haskell + Platform, you can install it manually by following the instructions on the Haskell Platform's + page.

+

One important tool you'll need to update is alex. The Haskell Platform + includes version 2, while the Javascript minifier Yesod uses, hjsmin, requires + version three. Be sure to cabal install alex after getting set up with the + Haskell Platform, or you'll run into error messages about the + language-javascript package.

+ +

Regardless of how you've installed your tools, you should sure to put cabal's + bin folder in your PATH variable. On Mac and Linux, this will be + $HOME/.cabal/bin and on Windows it will be + %APPDATA%\cabal\bin.

+

+ cabal has lots of different options available, but for now, just try out two + commands:

+
    +
  • +

    + cabal update will download the most recent list of packages from + Hackage.

    +
  • +
  • +

    + cabal install yesod will install Yesod and all its dependencies.

    +
  • +
+ +
+
+

Language Pragmas

+

+

GHC will run by default in something very close to Haskell98 mode. It also ships with a large + number of language extensions, allowing more powerful type classes, syntax changes, and more. + There are multiple ways to tell GHC to turn on these extensions. For most of the code snippets in + this book, you'll see language pragmas, which look like this:

+
{-# LANGUAGE MyLanguageExtension #-}
+

These should always appear at the top of your source file. Additionally, there are two + other common approaches:

+
    +
  • +

    On the GHC command line, pass an extra argument + -XMyLanguageExtension.

    +
  • +
  • +

    In your cabal file, add an extensions block.

    +
  • +
+

I personally never use the GHC command line argument approach. It's a personal preference, but + I like to have my settings clearly stated in a file. In general it's recommended to avoid putting + extensions in your cabal file; however, in the Yesod scaffolded site we specifically use this approach to avoid the + boilerplate of specifying the same language pragmas in every source file.

+

We'll end up using quite a few language extensions in this book (the scaffolding uses 11). We + will not cover the meaning of all of them. Instead, please see the GHC documentation.

+
+
+

Overloaded Strings

+

+

What's the type of "hello"? Traditionally, it's String, which + is defined as type String = [Char]. Unfortunately, there are a number of + limitations with this:

+
    +
  • +

    It's a very inefficient implementation of textual data. We need to allocate extra memory for + each cons cell, plus the characters themselves each take up a full machine word.

    +
  • +
  • +

    Sometimes we have string-like data that's not actually text, such as + ByteStrings and HTML.

    +
  • +
+

To work around these limitations, GHC has a language extension called + OverloadedStrings. When enabled, literal strings no longer have the monomorphic + type String; instead, they have the type IsString a => a, where + IsString is defined as:

+
class IsString a where
+    fromString :: String -> a
+

There are IsString instances available for a number of types in Haskell, such + as Text (a much more efficient packed String type), + ByteString, and Html. Virtually every example in this book + will assume that this language extension is turned on.

+

Unfortunately, there is one drawback to this extension: it can sometimes confuse GHC's type + checker. Imagine we have:

+
{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}
+import Data.Text (Text)
+
+class DoSomething a where
+    something :: a -> IO ()
+
+instance DoSomething String where
+    something _ = putStrLn "String"
+
+instance DoSomething Text where
+    something _ = putStrLn "Text"
+
+myFunc :: IO ()
+myFunc = something "hello"
+

Will the program print out String or Text? It's not clear. So + instead, you'll need to give an explicit type annotation to specify whether + "hello" should be treated as a String or + Text.

+
+
+

Type Families

+

+

The basic idea of a type family is to state some association between two different types. + Suppose we want to write a function that will safely take the first element of a list. But we + don't want it to work just on lists; we'd like it to treat a ByteString like a + list of Word8s. To do so, we need to introduce some associated type + to specify what the contents of a certain type are.

+
{-# LANGUAGE TypeFamilies, OverloadedStrings #-}
+import Data.Word (Word8)
+import qualified Data.ByteString as S
+import Data.ByteString.Char8 () -- get an orphan IsString instance
+
+class SafeHead a where
+    type Content a
+    safeHead :: a -> Maybe (Content a)
+
+instance SafeHead [a] where
+    type Content [a] = a
+    safeHead [] = Nothing
+    safeHead (x:_) = Just x
+
+instance SafeHead S.ByteString where
+    type Content S.ByteString = Word8
+    safeHead bs
+        | S.null bs = Nothing
+        | otherwise = Just $ S.head bs
+
+main :: IO ()
+main = do
+    print $ safeHead ("" :: String)
+    print $ safeHead ("hello" :: String)
+
+    print $ safeHead ("" :: S.ByteString)
+    print $ safeHead ("hello" :: S.ByteString)
+

The new syntax is the ability to place a type inside of a + class and instance. We can also use data + instead, which will create a new datatype instead of reference an existing one.

+ +
+
+

Template Haskell

+

+

Template Haskell (TH) is an approach to code generation. We use it in Yesod in a number + of places to reduce boilerplate, and to ensure that the generated code is correct. Template + Haskell is essentially Haskell which generates a Haskell Abstract Syntax Tree (AST).

+ +

Writing TH code can be tricky, and unfortunately there isn't very much type safety involved. + You can easily write TH that will generate code that won't compile. This is only an issue for the + developers of Yesod, not for its users. During development, we use a large collection of unit + tests to ensure that the generated code is correct. As a user, all you need to do is call these + already existing functions. For example, to include an externally defined Hamlet template, you + can write:

+
$(hamletFile "myfile.hamlet")
+

(Hamlet is discussed in the Shakespeare chapter.) The dollar sign immediately followed by parantheses + tell GHC that what follows is a Template Haskell function. The code inside is then run by the + compiler and generates a Haskell AST, which is then compiled. And yes, it's even possible to + go meta with this.

+

A nice trick is that TH code is allowed to perform arbitrary IO actions, and + therefore we can place some input in external files and have it parsed at compile time. One + example usage is to have compile-time checked HTML, CSS, and Javascript templates.

+

If your Template Haskell code is being used to generate declarations, and is being placed at + the top level of our file, we can leave off the dollar sign and parentheses. In other words:

+
{-# LANGUAGE TemplateHaskell #-}
+
+-- Normal function declaration, nothing special
+myFunction = ...
+
+-- Include some TH code
+$(myThCode)
+
+-- Or equivalently
+myThCode
+

It can be useful to see what code is being generated by Template Haskell for you. To do so, you + should use the -ddump-splices GHC option.

+ +
+
+

QuasiQuotes

+

+

QuasiQuotes (QQ) are a minor extension of Template Haskell that let us embed arbitrary content + within our Haskell source files. For example, we mentioned previously the + hamletFile TH function, which reads the template contents from an external + file. We also have a quasi-quoter named hamlet that takes the content + inline:

+
{-# LANGUAGE QuasiQuotes #-}
+
+[hamlet|<p>This is quasi-quoted Hamlet.|]
+

The syntax is set off using square brackets and pipes. The name of the quasi-quoter is given + between the opening bracket and the first pipe, and the content is given between the pipes.

+

Throughout the book, we will often times use the QQ-approach over a TH-powered external file + since the former is simpler to copy-and-paste. However, in production, external files are + recommended for all but the shortest of inputs as it gives a nice separation of the non-Haskell + syntax from your Haskell code.

+
+
+

Summary

+

+

You don't need to be an expert in Haskell to use Yesod, a basic familiarity will suffice. This + chapter hopefully gave you just enough extra information to feel more comfortable following the + rest of the book.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/http-conduit.html b/public/book-1.1/http-conduit.html new file mode 100644 index 00000000..3ea05ef8 --- /dev/null +++ b/public/book-1.1/http-conduit.html @@ -0,0 +1,311 @@ + http-conduit :: Yesod Web Framework Book- Version 1.1 +
+

http-conduit

+ + +

+

+

Most of Yesod is about serving content over HTTP. But that's only half the story: someone has + to receive it. And even when you're writing a web app, sometimes that someone will be you. If you + want to consume content from other services or interact with RESTful APIs, you'll need to write + client code. And the recommended approach for that is http-conduit.

+

This chapter is not directly connected to Yesod, and will be generally useful for anyone + wanting to make HTTP requests.

+
+

Synopsis

+

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Conduit -- the main module
+
+-- The streaming interface uses conduits
+import Data.Conduit
+import Data.Conduit.Binary (sinkFile)
+
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    -- Simplest query: just download the information from the given URL as a
+    -- lazy ByteString.
+    simpleHttp "http://www.example.com/foo.txt" >>= L.writeFile "foo.txt"
+
+    -- Use the streaming interface instead. We need to run all of this inside a
+    -- ResourceT, to ensure that all our connections get properly cleaned up in
+    -- the case of an exception.
+    runResourceT $ do
+        -- We need a Manager, which keeps track of open connections. simpleHttp
+        -- creates a new manager on each run (i.e., it never reuses
+        -- connections).
+        manager <- liftIO $ newManager def
+
+        -- A more efficient version of the simpleHttp query above. First we
+        -- parse the URL to a request.
+        req <- liftIO $ parseUrl "http://www.example.com/foo.txt"
+
+        -- Now get the response
+        res <- http req manager
+
+        -- And finally stream the value to a file
+        responseBody res $$+- sinkFile "foo.txt"
+
+        -- Make it a POST request, don't follow redirects, and accept any
+        -- status code.
+        let req2 = req
+                { method = "POST"
+                , redirectCount = 0
+                , checkStatus = \_ _ _ -> Nothing
+                }
+        res2 <- http req2 manager
+        responseBody res2 $$+- sinkFile "post-foo.txt"
+
+
+

Concepts

+

+

The simplest way to make a request in http-conduit is with the + simpleHttp function. This function takes a String giving a URL + and returns a ByteString with the contents of that URL. But under the surface, + there are a few more steps:

+
    +
  • +

    A new connection Manager is allocated.

    +
  • +
  • +

    The URL is parsed to a Request. If the URL is invalid, then an exception is + thrown.

    +
  • +
  • +

    The HTTP request is made, following any redirects from the server.

    +
  • +
  • +

    If the response has a status code outside the 200-range, an exception is thrown.

    +
  • +
  • +

    The response body is read into memory and returned.

    +
  • +
  • +

    + runResourceT is called, which will free up any resources (e.g., the open + socket to the server).

    +
  • +
+

If you want more control of what's going on, then you can configure any of the steps above + (plus a few more) by explicitly creating a Request value, allocating your + Manager manually, and using the http and + httpLbs functions.

+
+
+

Request

+

+

The easiest way to creating a Request is with the parseUrl + function. This function will return a value in any Failure monad, such as + Maybe or IO. The last of those is the most commonly used, and + results in a runtime exception whenever an invalid URL is provided. However, you can use a + different monad if, for example, you want to validate user input.

+
import Network.HTTP.Conduit
+import System.Environment (getArgs)
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    args <- getArgs
+    case args of
+        [urlString] ->
+            case parseUrl urlString of
+                Nothing -> putStrLn "Sorry, invalid URL"
+                Just req -> withManager $ \manager -> do
+                    res <- httpLbs req manager
+                    liftIO $ L.putStr $ responseBody res
+        _ -> putStrLn "Sorry, please provide exactly one URL"
+

The Request type is abstract so that http-conduit can add new + settings in the future without breaking the API (see the Settings Type + chapter for more information). In order to make changes to individual records, you use record + notation. For example, a modification to our program that issues HEAD requests + and prints the response headers would be:

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Conduit
+import System.Environment (getArgs)
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    args <- getArgs
+    case args of
+        [urlString] ->
+            case parseUrl urlString of
+                Nothing -> putStrLn "Sorry, invalid URL"
+                Just req -> withManager $ \manager -> do
+                    let reqHead = req { method = "HEAD" }
+                    res <- http reqHead manager
+                    liftIO $ do
+                        print $ responseStatus res
+                        mapM_ print $ responseHeaders res
+        _ -> putStrLn "Sorry, please provide example one URL"
+

There are a number of different configuration settings in the API, some noteworthy ones + are:

+
+
proxy
+

Allows you to pass the request through the given proxy server.

+
+
redirectCount
+

Indicate how many redirects to follow. Default is 10.

+
+
checkStatus
+

Check the status code of the return value. By default, gives an exception for any non-2XX + response.

+
+
requestBody
+

The request body to be sent. Be sure to also update the method. For the + common case of url-encoded data, you can use the urlEncodedBody function.

+
+
+
+
+

Manager

+

+

The connection manager allows you to reuse connections. When making multiple queries to a + single server (e.g., accessing Amazon S3), this can be critical for creating efficient code. A + manager will keep track of multiple connections to a given server (taking into account port and + SSL as well), automatically reaping unused connections as needed. When you make a request, + http-conduit first tries to check out an existing connection. When you're + finished with the connection (if the server allows keep-alive), the connection is returned to the + manager. If anything goes wrong, the connection is closed.

+

To keep our code exception-safe, we use the ResourceT monad transformer. All + this means for you is that your code needs to be wrapped inside a call to + runResourceT, either implicitly or explicitly, and that code inside that block + will need to liftIO to perform normal IO actions.

+

There are two ways you can get ahold of a manager. newManager will return a + manager that will not be automatically closed (you can use closeManager to do so + manually), while withManager will start a new ResourceT block, + allow you to use the manager, and then automatically close the ResourceT when + you're done. If you want to use a ResourceT for an entire application, and have + no need to close it, you should probably use newManager.

+

One other thing to point out: you obviously don't want to create a new manager for each and + every request; that would defeat the whole purpose. You should create your + Manager early and then share it.

+
+
+

Response

+

+

The Response datatype has three pieces of information: the status code, the + response headers, and the response body. The first two are straight-forward; let's discuss the + body.

+

The Response type has a type variable to allow the response body to be of + multiple types. If you want to use http-conduit's streaming interface, you want + this to be a Source. For the simple interface, it will be a lazy + ByteString. One thing to note is that, even though we use a lazy + ByteString, the entire response is held in memory. In other words, we + perform no lazy I/O in this package.

+ +
+
+

http and httpLbs

+

+

So let's tie it together. The http function gives you access to the streaming + interface (i.e., it returns a Response using a BufferedSource) + while httpLbs returns a lazy ByteString. Both of these return + values in the ResourceT transformer so that they can access the + Manager and have connections handled properly in the case of exceptions.

+ +
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/internationalization.html b/public/book-1.1/internationalization.html new file mode 100644 index 00000000..bfd72e5e --- /dev/null +++ b/public/book-1.1/internationalization.html @@ -0,0 +1,384 @@ + Internationalization :: Yesod Web Framework Book- Version 1.1 +
+

Internationalization

+ + +

+

+

Users expect our software to speak their language. Unfortunately for us, there + will likely be more than one language involved. While doing simple string replacement + isn't too involved, correctly dealing with all the grammar issues can be tricky. After + all, who wants to see "List 1 file(s)" from a program output?

+

But a real i18n solution needs to do more than just provide a means of achieving the + correct output. It needs to make this process easy for both the programmer and the + translator and relatively error-proof. Yesod's answer to the problem gives you:

+
    +
  • +

    Intelligent guessing of the user's desired language based on request headers, with + the ability to override.

    +
  • +
  • +

    A simple syntax for giving translations which requires no Haskell knowledge. (After + all, most translators aren't programmers.)

    +
  • +
  • +

    The ability to bring in the full power of Haskell for tricky grammar issues as + necessary, along with a default selection of helper functions to cover most + needs.

    +
  • +
  • +

    Absolutely no issues at all with word order.

    +
  • +
+
+

Synopsis

+

+
-- @messages/en.msg
+Hello: Hello
+EnterItemCount: I would like to buy: 
+Purchase: Purchase
+ItemCount count@Int: You have purchased #{showInt count} #{plural count "item" "items"}.
+SwitchLanguage: Switch language to: 
+Switch: Switch
+
-- @messages/he.msg
+Hello: שלום
+EnterItemCount: אני רוצה לקנות: 
+Purchase: קנה
+ItemCount count: קנית #{showInt count} #{plural count "דבר" "דברים"}.
+SwitchLanguage: החלף שפה ל:
+Switch: החלף
+
-- @i18n-synopsis.hs
+{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell, TypeFamilies,
+    MultiParamTypeClasses #-}
+import Yesod
+
+data I18N = I18N
+
+mkMessage "I18N" "messages" "en"
+
+plural :: Int -> String -> String -> String
+plural 1 x _ = x
+plural _ _ y = y
+
+showInt :: Int -> String
+showInt = show
+
+instance Yesod I18N
+
+instance RenderMessage I18N FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+mkYesod "I18N" [parseRoutes|
+/ RootR GET
+/buy BuyR GET
+/lang LangR POST
+|]
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<h1>_{MsgHello}
+<form action=@{BuyR}>
+    _{MsgEnterItemCount}
+    <input type=text name=count>
+    <input type=submit value=_{MsgPurchase}>
+<form action=@{LangR} method=post>
+    _{MsgSwitchLanguage}
+    <select name=lang>
+        <option value=en>English
+        <option value=he>Hebrew
+    <input type=submit value=_{MsgSwitch}>
+|]
+
+getBuyR :: Handler RepHtml
+getBuyR = do
+    count <- runInputGet $ ireq intField "count"
+    defaultLayout [whamlet|
+<p>_{MsgItemCount count}
+|]
+
+postLangR :: Handler ()
+postLangR = do
+    lang <- runInputPost $ ireq textField "lang"
+    setLanguage lang
+    redirect RootR
+
+main :: IO ()
+main = warpDebug 3000 I18N
+
+
+

Overview

+

+

Most existing i18n solutions out there, like gettext or Java message bundles, work on the + principle of string lookups. Usually some form of printf-interpolation is used to interpolate + variables into the strings. In Yesod, as you might guess, we instead rely on types. This gives us + all of our normal advantages, such as the compiler automatically catching mistakes.

+

Let's take a concrete example. Suppose our application has two things it wants to say + to a user: say hello, and state how many users are logged into the system. This can be modeled + with a sum type:

+
data MyMessage = MsgHello | MsgUsersLoggedIn Int
+

I can also write a function to turn this datatype into an English representation:

+
toEnglish :: MyMessage -> String
+toEnglish MsgHello = "Hello there!"
+toEnglish (MsgUsersLoggedIn 1) = "There is 1 user logged in."
+toEnglish (MsgUsersLoggedIn i) = "There are " ++ show i ++ " users logged in."
+

We can also write similar functions for other languages. The advantage to this + inside-Haskell approach is that we have the full power of Haskell for addressing tricky grammar + issues, especially pluralization.

+ +

The downside, however, is that you have to write all of this inside of Haskell, which won't be + very translator-friendly. To solve this, Yesod introduces the concept of message files. We'll + cover that in a little bit.

+

Assuming we have this full set of translation functions, how do we go about using + them? What we need is a new function to wrap them all up together, and then choose the + appropriate translation function based on the user's selected language. Once we have that, Yesod + can automatically choose the most relevant render function and call it on the values you + provide.

+

In order to simplify things a bit, Hamlet has a special interpolation syntax, + _{...}, which handles all the calls to the render functions. And in order to + associate a render function with your application, you use the YesodMessage + typeclass.

+
+
+

Message files

+

+

The simplest approach to creating translations is via message + files. The setup is simple: there is a single folder containing all of your translation + files, with a single file for each language. Each file is named based on its language code, e.g. + en.msg. And each line in a file handles one phrase, which + correlates to a single constructor in your message data type.

+ +

So firstly, a word about language codes. There are really two choices available: using a + two-letter language code, or a language-LOCALE code. For example, when I load up a page in my web + browser, it sends two language codes: en-US and en. What my browser is saying is "if you have + American English, I like that the most. If you have English, I'll take that instead."

+

So which format should you use in your application? Most likely two-letter codes, + unless you are actually creating separate translations by locale. This ensures that someone + asking for Canadian English will still see your English. Behind the scenes, Yesod will add the + two-letter codes where relevant. For example, suppose a user has the following language + list:

pt-BR, es, he
What this means is "I like Brazilian Porteguese, then + Spanish, and then Hebrew." Suppose your application provides the languages pt (general + Porteguese) and English, with English as the default. Strictly following the user's language list + would result in the user being served English. Instead, Yesod translates that list + into:
pt-BR, es, he, pt
In other words: unless you're giving different + translations based on locale, just stick to the two-letter language codes.

+

Now what about these message files? The syntax should be very familiar after your work + with Hamlet and Persistent. The line starts off with the name of the message. Since this is a + data constructor, it must start with a capital letter. Next, you can have individual parameters, + which must be given as lower case. These will be arguments to the data constructor.

+

The argument list is terminated by a colon, and then followed by the translated string, which + allows usage of our typical variable interpolation syntax #{myVar}. By referring + to the parameters defined before the colon, and using translation helper functions to deal with + issues like pluralization, you can create all the translated messages you need.

+
+

Specifying types

+

+

Since we will be creating a datatype out of our message specifications, each parameter + to a data constructor must be given a data type. We use a @-syntax for this. For example, to + create the datatype data MyMessage = MsgHello | MsgSayAge Int, we would + write:

+
Hello: Hi there!
+SayAge age@Int: Your age is: #{show age}
+

But there are two problems with this:

+
    +
  1. +

    It's not very DRY (don't repeat yourself) to have to specify this datatype in every + file.

    +
  2. +
  3. +

    Translators will be confused having to specify these datatypes.

    +
  4. +
+

So instead, the type specification is only required in the main language file. This is + specified as the third argument in the mkMessage function. This also specifies + what the backup language will be, to be used when none of the languages provided by your + application match the user's language list.

+
+
+
+

RenderMessage typeclass

+

+

Your call to mkMessage creates an instance of the + RenderMessage typeclass, which is the core of Yesod's i18n. It is defined + as:

+
class RenderMessage master message where
+    renderMessage :: master
+                  -> [Text] -- ^ languages
+                  -> message
+                  -> Text 
+

Notice that there are two parameters to the RenderMessage class: the + master site and the message type. In theory, we could skip the master type here, but that would + mean that every site would need to have the same set of translations for each message type. When + it comes to shared libraries like forms, that would not be a workable solution.

+

The renderMessage function takes a parameter for each of the class's + type parameters: master and message. The extra parameter is a list of languages the user will + accept, in descending order of priority. The method then returns a user-ready + Text that can be displayed.

+

A simple instance of RenderMessage may involve no actual translation + of strings; instead, it will just display the same value for every language. For example:

+
data MyMessage = Hello | Greet Text
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ _ Hello = "Hello"
+    renderMessage _ _ (Greet name) = "Welcome, " <> name <> "!"
+

Notice how we ignore the first two parameters to renderMessage. We + can now extend this to support multiple languages:

+
renderEn Hello = "Hello"
+renderEn (Greet name) = "Welcome, " <> name <> "!"
+renderHe Hello = "שלום"
+renderHe (Greet name) = "ברוכים הבאים, " <> name <> "!"
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ ("en":_) = renderEn
+    renderMessage _ ("he":_) = renderHe
+    renderMessage master (_:langs) = renderMessage master langs
+    renderMessage _ [] = renderEn
+

The idea here is fairly straight-forward: we define helper functions to support each language. + We then add a clause to catch each of those languages in the renderMessage definition. We then + have two final cases: if no languages matched, continue checking with the next language in the + user's priority list. If we've exhausted all languages the user specified, then use the default + language (in our case, English).

+

But odds are that you will never need to worry about writing this stuff manually, as the + message file interface does all this for you. But it's always a good idea to have an + understanding of what's going on under the surface.

+
+
+

Interpolation

+

+

One way to use your new RenderMessage instance would be to directly + call the renderMessage function. This would work, but it's a bit + tedious: you need to pass in the foundation value and the language list manually. Instead, Hamlet + provides a specialized i18n interpolation, which looks like _{...}.

+ +

Hamlet will then automatically translate that to a call to renderMessage. Once Hamlet gets the output Text value, it uses the + toHtml function to produce an Html value, + meaning that any special characters (<, &, >) will be automatically escaped.

+
+
+

Phrases, not words

+

+

As a final note, I'd just like to give some general i18n advice. Let's say you have an + application for selling turtles. You're going to use the word "turtle" in multiple places, like + "You have added 4 turtles to your cart." and "You have purchased 4 turtles, congratulations!" As + a programmer, you'll immediately notice the code reuse potential: we have the phrase "4 turtles" + twice. So you might structure your message file as:

+
AddStart: You have added
+AddEnd: to your cart.
+PurchaseStart: You have purchased
+PurchaseEnd: , congratulations!
+Turtles count@Int: #{show count} #{plural count "turtle" "turtles"}
+

STOP RIGHT THERE! This is all well and good from a programming perspective, but + translations are not programming. There are a many things that could go wrong + with this, such as:

+
    +
  • +

    Some languages might put "to your cart" before "You have added."

    +
  • +
  • +

    Maybe "added" will be constructed differently depending whether you added 1 or more + turtles.

    +
  • +
  • +

    There are a bunch of whitespace issues as well.

    +
  • +
+

So the general rule is: translate entire phrases, not just words.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/introduction.html b/public/book-1.1/introduction.html new file mode 100644 index 00000000..545f437d --- /dev/null +++ b/public/book-1.1/introduction.html @@ -0,0 +1,177 @@ + Introduction :: Yesod Web Framework Book- Version 1.1 +
+

Introduction

+ + +

+

+

Since web programming began, people have been trying to make the development process a more pleasant one. As a community, we have continually pushed new techniques to try and solve some of the lingering difficulties of security threats, the stateless nature of HTTP, the multiple languages (HTML, CSS, Javascript) necessary to create a powerful web application, and more.

+

Yesod attempts to ease the web development process by playing to the strengths of the Haskell programming language. Haskell's strong compile-time guarantees of correctness not only encompass types; referential transparency ensures that we don't have any unintended side effects. Pattern matching on algebraic data types can help guarantee we've accounted for every possible case. By building upon Haskell, entire classes of bugs disappear.

+

Unfortunately, using Haskell isn't enough. The web, by its very nature, is not type safe. Even the simplest case of distinguishing between an integer and string is impossible: all data on the web is transferred as raw bytes, evading our best efforts at type safety. Every app writer is left with the task of validating all input. I call this problem the boundary issue: as much as your application is type safe on the inside, every boundary with the outside world still needs to be sanitized.

+
+

Type Safety

+

+

This is where Yesod comes in. By using high-level declarative techniques, you can specify the exact input types you are expecting. And the process works the other way as well: using a process of type-safe URLs, you can make sure that the data you send out is also guaranteed to be well formed.

+

The boundary issue is not just a problem when dealing with the client: the same problem exists when persisting and loading data. Once again, Yesod saves you on the boundary by performing the marshaling of data for you. You can specify your entities in a high-level definition and remain blissfully ignorant of the details.

+
+
+

Concise

+

+

We all know that there is a lot of boilerplate coding involved in web applications. Wherever possible, Yesod tries to use Haskell's features to save your fingers the work:

+
    +
  • +

    The forms library reduces the amount of code used for common cases by leveraging the Applicative type class.

    +
  • +
  • +

    Routes are declared in a very terse format, without sacrificing type safety.

    +
  • +
  • +

    Serializing your data to and from a database is handled automatically via code generation.

    +
  • +
+

In Yesod, we have two kinds of code generation. To get your project started, we provide a scaffolding tool to set up your file and folder structure. However, most code generation is done at compile time via meta programming. This means your generated code will never get stale, as a simple library upgrade will bring all your generated code up-to-date.

+

But for those who like to stay in control, and know exactly what their code is doing, you can always run closer to the compiler and write all your code yourself.

+
+
+

Performance

+

+

Haskell's main compiler, the GHC, has amazing performance characteristics, and is improving all the time. This choice of language by itself gives Yesod a large performance advantage over other offerings. But that's not enough: we need an architecture designed for performance.

+

Our approach to templates is one example: by allowing HTML, CSS and JavaScript to be + analyzed at compile time, Yesod both avoids costly disk I/O at runtime and can optimize + the rendering of this code. But the architectural decisions go deeper: we use advanced + techniques such as conduits and builders in the underlying libraries to make sure our + code runs in constant memory, without exhausting precious file handles and other + resources. By offering high-level abstractions, you can get highly compressed and + properly cached CSS and JavaScript.

+

Yesod's flagship web server, Warp, is the fastest Haskell web server around. When these two pieces of technology are combined, it produces one of the fastest web application deployment solutions available.

+
+
+

Modular

+

+

Yesod has spawned the creation of dozens of packages, most of which are usable in a context outside of Yesod itself. One of the goals of the project is to contribute back to the community as much as possible; as such, even if you are not planning on using Yesod in your next project, a large portion of this book may still be relevant for your needs.

+

Of course, these libraries have all been designed to integrate well together. Using the Yesod Framework should give you a strong feeling of consistency throughout the various APIs.

+
+
+

A solid foundation

+

+

I remember once seeing a PHP framework advertising support for UTF-8. This struck me as + surprising: you mean having UTF-8 support isn't automatic? In the Haskell world, issues + like character encoding are already well addressed and fully supported. In fact, we + usually have the opposite problem: there are a number of packages providing powerful and + well-designed support for the problem. The Haskell community is constantly pushing the + boundaries finding the cleanest, most efficient solutions for each challenge.

+

The downside of such a powerful ecosystem is the complexity of choice. By using Yesod, you will already have most of the tools chosen for you, and you can be guaranteed they work together. Of course, you always have the option of pulling in your own solution.

+

As a real-life example, Yesod and Hamlet (the default templating language) use blaze-builder for textual content generation. This choice was made because blaze provides the fastest interface for generating UTF-8 data. Anyone who wants to use one of the other great libraries out there, such as text, should have no problem dropping it in.

+
+
+

Introduction to Haskell

+

+

Haskell is a powerful, fast, type-safe, functional programming language. This book takes as an assumption that you are already familiar with most of the basics of Haskell. There are two wonderful books for learning Haskell, both of which are available for reading online:

+ +

Yesod relies on a few features in Haskell that most introductory tutorials do not + cover. Though you will rarely need to understand how these work, it's always best to + start off with a good appreciation for what your tools are doing. These are covered in + the next chapter.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/json-web-service.html b/public/book-1.1/json-web-service.html new file mode 100644 index 00000000..6343775a --- /dev/null +++ b/public/book-1.1/json-web-service.html @@ -0,0 +1,206 @@ + JSON Web Service :: Yesod Web Framework Book- Version 1.1 +
+

JSON Web Service

+ + +

+

+

Let's create a very simple web service: it takes a JSON request and returns a JSON + response. We're going to write the server in WAI/Warp, and the client in http-conduit. We'll be using aeson for + JSON parsing and rendering. We could also write the server in Yesod itself, but for such + a simple example, the extra features of Yesod don't add much.

+
+

Server

+

+

WAI uses the conduit package to handle streaming request + bodies, and efficiently generates responses using blaze-builder. aeson uses attoparsec for parsing; by using attoparsec-conduit we get easy interoperability with WAI. This plays out + as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai (Response, responseLBS, Application, requestBody)
+import Network.HTTP.Types (status200, status400)
+import Network.Wai.Handler.Warp (run)
+import Data.Aeson.Parser (json)
+import Data.Conduit.Attoparsec (sinkParser)
+import Control.Monad.IO.Class (liftIO)
+import Data.Aeson (Value, encode, object, (.=))
+import Control.Exception (SomeException)
+import Data.ByteString (ByteString)
+import Data.Conduit (ResourceT, ($$))
+import Control.Exception.Lifted (handle)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req = handle invalidJson $ do
+    value <- requestBody req $$ sinkParser json
+    newValue <- liftIO $ modValue value
+    return $ responseLBS
+        status200
+        [("Content-Type", "application/json")]
+        $ encode newValue
+
+invalidJson :: SomeException -> ResourceT IO Response
+invalidJson ex = return $ responseLBS
+    status400
+    [("Content-Type", "application/json")]
+    $ encode $ object
+        [ ("message" .= show ex)
+        ]
+
+-- Application-specific logic would go here.
+modValue :: Value -> IO Value
+modValue = return
+
+
+

Client

+

+

+ http-conduit was written as a companion to WAI. It too uses + conduit and blaze-builder pervasively, meaning we once again + get easy interop with aeson. A few extra comments for those not familiar with + http-conduit:

+
    +
  • +

    A Manager is present to keep track of open connections, so + that multiple requests to the same server use the same connection. You usually want to use the + withManager function to create and clean up this + Manager, since it is exception safe.

    +
  • +
  • +

    We need to know the size of our request body, which can't be determined directly from + a Builder. Instead, we convert the Builder into a lazy + ByteString and take the size from there.

    +
  • +
  • +

    There are a number of different functions for initiating a request. We use + http, which allows us to directly access the data stream. There are other + higher level functions (such as httpLbs) that let you ignore the issues of + sources and get the entire body directly.

    +
  • +
+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Conduit
+    ( http, parseUrl, withManager, RequestBody (RequestBodyLBS)
+    , requestBody, method, Response (..)
+    )
+import Data.Aeson (Value (Object, String))
+import Data.Aeson.Parser (json)
+import Data.Conduit (($$))
+import Data.Conduit.Attoparsec (sinkParser)
+import Control.Monad.IO.Class (liftIO)
+import Data.Aeson (encode, (.=), object)
+
+main :: IO ()
+main = withManager $ \manager -> do
+    value <- liftIO makeValue
+    -- We need to know the size of the request body, so we convert to a
+    -- ByteString
+    let valueBS = encode value
+    req' <- liftIO $ parseUrl "http://localhost:3000/"
+    let req = req' { method = "POST", requestBody = RequestBodyLBS valueBS }
+    Response status version headers body <- http req manager
+    resValue <- body $$ sinkParser json
+    liftIO $ handleResponse resValue
+
+-- Application-specific function to make the request value
+makeValue :: IO Value
+makeValue = return $ object
+    [ ("foo" .= ("bar" :: String))
+    ]
+
+-- Application-specific function to handle the response from the server
+handleResponse :: Value -> IO ()
+handleResponse = print
+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/monad-control.html b/public/book-1.1/monad-control.html new file mode 100644 index 00000000..8129954f --- /dev/null +++ b/public/book-1.1/monad-control.html @@ -0,0 +1,438 @@ + monad-control :: Yesod Web Framework Book- Version 1.1 +
+

monad-control

+ + +

+

+

+ monad-control is used in a few places within Yesod, most notably to ensure proper exception handling within Persistent. It is a general purpose package to extend standard functionality in monad transformers.

+
+

Overview

+

+

One of the powerful, and sometimes confusing, features in Haskell is monad transformers. They + allow you to take different pieces of functionality- such as mutable state, error handling, or + logging- and compose them together easily. Though I swore I'd never write a monad tutorial, I'm + going to employ a painful analogy here: monads are like onions. (Monads are not like cakes.) By + that, I mean layers.

+

We have the core monad- also known as the innermost or bottom monad. On top of this + core, we add layers, each adding a new feature and spreading outward/upward. As a motivating + example, let's consider an Error monad stacked on top of the IO + monad:

newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
+type MyStack = ErrorT MyError IO
Now + pay close attention here: ErrorT is just a simple newtype around an Either wrapped in a monad. + Getting rid of the newtype, we + have:
type ErrorTUnwrapped e m a = m (Either e a)
+

+

At some point, we'll need to actually perform some IO inside our MyStack. If we went with the + unwrapped approach, it would be trivial, since there would be no ErrorT constructor in the way. + However, we need that newtype wrapper for a whole bunch of type reasons I won't go into here + (this isn't a monad transformer tutorial after all). So the solution is the MonadTrans + typeclass:

+
class MonadTrans t where
+    lift :: Monad m => m a -> t m a
+

I'll admit, the first time I saw that type signature, my response was stunned confusion, and + incredulity that it actually meant anything. But looking at an instance helps a + bit:

instance (Error e) => MonadTrans (ErrorT e) where
+    lift m = ErrorT $ do
+        a <- m
+        return (Right a)
All + we're doing is wrapping the inside of the IO with a Right value, and then applying our newtype + wrapper. This allows us to take an action that lives in IO, and "lift" it to the outer/upper + monad.

+

But now to the point at hand. This works very well for simple functions. For example:

+
sayHi :: IO ()
+sayHi = putStrLn "Hello"
+
+sayHiError :: ErrorT MyError IO ()
+sayHiError = lift $ putStrLn "Hello"
+

But let's take something slightly more complicated, like a callback:

+
withMyFile :: (Handle -> IO a) -> IO a
+withMyFile = withFile "test.txt" WriteMode
+
+sayHi :: Handle -> IO ()
+sayHi handle = hPutStrLn handle "Hi there"
+
+useMyFile :: IO ()
+useMyFile = withMyFile sayHi
+

So far so good, right? Now let's say that we need a version of sayHi that has access to the + Error monad:

+
sayHiError :: Handle -> ErrorT MyError IO ()
+sayHiError handle = do
+    lift $ hPutStrLn handle "Hi there, error!"
+    throwError MyError
+

We would like to write a function that combines withMyFile and sayHiError. + Unfortunately, GHC doesn't like this very + much:

useMyFileErrorBad :: ErrorT MyError IO ()
+useMyFileErrorBad = withMyFile sayHiError
+
+    Couldn't match expected type `ErrorT MyError IO ()'
+                with actual type `IO ()'
Why + does this happen, and how can we work around it?

+
+
+

Intuition

+

+

Let's try and develop an external intuition of what's happening here. The ErrorT monad + transformer adds extra functionality to the IO monad. We've defined a way to "tack on" that extra + functionality to normal IO actions: we add that Right constructor and wrap it all in ErrorT. + Wrapping in Right is our way of saying "it went OK," there wasn't anything wrong with this + action.

+

Now this intuitively makes sense: since the IO monad doesn't have the concept of returning a + MyError when something goes wrong, it will always succeed in the lifting phase. (Note: This has + nothing to do with runtime exceptions, don't even think about them.) What we have is a + guaranteed one-directional translation up the monad stack.

+

Let's take another example: the Reader monad. A Reader has access to some extra piece of data + floating around. Whatever is running in the inner monad doesn't know about that extra piece of + information. So how would you do a lift? You just ignore that extra information. The Writer + monad? Don't write anything. State? Don't change anything. I'm seeing a pattern here.

+

But now let's try and go in the opposite direction: I have something in a Reader, and I'd like + to run it in the base monad (e.g., IO). Well... that's not going to work, is it? I need that + extra piece of information, I'm relying on it, and it's not there. There's simply no way to go in + the opposite direction without providing that extra value.

+

Or is there? If you remember, we'd pointed out earlier that ErrorT is just a simple wrapper + around the inner monad. In other words, if I have errorValue :: ErrorT MyError IO + MyValue, I can apply runErrorT and get a value of type IO + (Either MyError MyValue). The looks quite a bit like bi-directional translation, + doesn't it?

+

Well, not quite. We originally had an ErrorT MyError IO monad, with a value of + type MyValue. Now we have a monad of type IO with a value of + type Either MyError MyValue. So this process has in fact changed the value, + while the lifting process leaves it the same.

+

But still, with a little fancy footwork we can unwrap the ErrorT, do some processing, and then + wrap it back up again.

+
useMyFileError1 :: ErrorT MyError IO ()
+useMyFileError1 =
+    let unwrapped :: Handle -> IO (Either MyError ())
+        unwrapped handle = runErrorT $ sayHiError handle
+        applied :: IO (Either MyError ())
+        applied = withMyFile unwrapped
+        rewrapped :: ErrorT MyError IO ()
+        rewrapped = ErrorT applied
+     in rewrapped
+

This is the crucial point of this whole article, so look closely. We first unwrap our monad. + This means that, to the outside world, it's now just a plain old IO value. Internally, we've + stored all the information from our ErrorT transformer. Now that we have a plain old IO, we can + easily pass it off to withMyFile. withMyFile takes in the internal state and passes it back out + unchanged. Finally, we wrap everything back up into our original ErrorT.

+

This is the entire pattern of monad-control: we embed the extra features of our monad + transformer inside the value. Once in the value, the type system ignores it and focuses on the + inner monad. When we're done playing around with that inner monad, we can pull our state back out + and reconstruct our original monad stack.

+
+
+

Types

+

+

I purposely started with the ErrorT transformer, as it is one of the simplest for this + inversion mechanism. Unfortunately, others are a bit more complicated. Take for instance ReaderT. + It is defined as newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }. If + we apply runReaderT to it, we get a function that returns a monadic value. So + we're going to need some extra machinery to deal with all that stuff. And this is when we leave + Kansas behind.

+

There are a few approaches to solving these problems. In the past, I implemented a solution + using type families in the neither package. Anders Kaseorg implemented a much + more straight-forward solution in monad-peel. And for efficiency, in + monad-control, Bas van Dijk uses CPS (continuation passing style) and + existential types.

+ +

The first type we're going to look at + is:

type Run t = forall n o b. (Monad n, Monad o, Monad (t o)) => t n b -> n (t o b)
That's + incredibly dense, let's talk it out. The only "input" datatype to this thing is t, a monad + transformer. A Run is a function that will then work with any combination of types n, o + and b (that's what the forall means). n and o are both monads, while b is a simple value + contained by them.

+

The left hand side of the Run function, t n b, is our monad transformer + wrapped around the n monad and holding a b value. So for example, that could be a MyTrans + FirstMonad MyValue. It then returns a value with the transformer "popped" inside, with + a brand new monad at its core. In other words, FirstMonad (MyTrans NewMonad + MyValue).

+

That might sound pretty scary at first, but it actually isn't as foreign as you'd think: this + is essentially what we did with ErrorT. We started with ErrorT on the outside, wrapping around + IO, and ended up with an IO by itself containing an Either. Well guess what: another way to + represent an Either is ErrorT MyError Identity. So essentially, we pulled the IO + to the outside and plunked an Identity in its place. We're doing the same thing in a Run: pulling + the FirstMonad outside and replacing it with a NewMonad.

+

+

Alright, now we're getting somewhere. If we had access to one of those Run functions, we could + use it to peel off the ErrorT on our sayHiError function and pass it to withMyFile. With the + magic of undefined, we can play such a game:

+
errorRun :: Run (ErrorT MyError)
+errorRun = undefined
+
+useMyFileError2 :: IO (ErrorT MyError Identity ())
+useMyFileError2 =
+    let afterRun :: Handle -> IO (ErrorT MyError Identity ())
+        afterRun handle = errorRun $ sayHiError handle
+        applied :: IO (ErrorT MyError Identity ())
+        applied = withMyFile afterRun
+     in applied
+

This looks eerily similar to our previous example. In fact, errorRun is acting almost + identically to runErrorT. However, we're still left with two problems: we don't know where to get + that errorRun value from, and we still need to restructure the original ErrorT after we're + done.

+
+

MonadTransControl

+

+

Obviously in the specific case we have before us, we could use our knowledge of the ErrorT + transformer to beat the types into submission and create our Run function manually. But what we + really want is a general solution for many transformers. At this point, you know we need + a typeclass.

+

So let's review what we need: access to a Run function, and some way to restructure our + original transformer after the fact. And thus was born MonadTransControl, with its single method + liftControl:

+
class MonadTrans t => MonadTransControl t where
+    liftControl :: Monad m => (Run t -> m a) -> t m a
+

Let's look at this closely. liftControl takes a function (the one we'll be writing). That + function is provided with a Run function, and must return a value in some monad (m). liftControl + will then take the result of that function and reinstate the original transformer on top of + everything.

+
useMyFileError3 :: Monad m => ErrorT MyError IO (ErrorT MyError m ())
+useMyFileError3 =
+    liftControl inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

Close, but not exactly what I had in mind. What's up with the double monads? Well, let's start + at the end: sayHiError handle returns a value of type ErrorT MyError IO (). This + we knew already, no surprises. What might be a little surprising (it got me, at least) is the + next two steps.

+

First we apply run to that value. Like we'd discussed before, the result is that the IO inner + monad is popped to the outside, to be replaced by some arbitrary monad (represented by m here). + So we end up with an IO (ErrorT MyError m ()). Ok... We then get the same result after applying + withMyFile. Not surprising.

+

The last step took me a long time to understand correctly. Remember how we said that we + reconstruct the original transformer? Well, so we do: by plopping it right on top of everything + else we have. So our end result is the previous type- IO (ErrorT MyError m ())- + with a ErrorT MyError stuck on the front.

+

Well, that seems just about utterly worthless, right? Well, almost. But don't forget, that "m" + can be any monad, including IO. If we treat it that way, we get ErrorT MyError IO (ErrorT + MyError IO ()). That looks a lot like m (m a), and we want just plain + old m a. Fortunately, now we're in + luck:

useMyFileError4 :: ErrorT MyError IO ()
+useMyFileError4 = join useMyFileError3
And + it turns out that this usage is so common, that Bas had mercy on us and defined a helper + function:
control :: (Monad m, Monad (t m), MonadTransControl t)
+        => (Run t -> m (t m a)) -> t m a
+control = join . liftControl
So + all we need to write + is:
useMyFileError5 :: ErrorT MyError IO ()
+useMyFileError5 =
+    control inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

+

And just to make it a little + shorter:

useMyFileError6 :: ErrorT MyError IO ()
+useMyFileError6 = control $ \run -> withMyFile $ run . sayHiError
+

+
+
+

MonadControlIO

+

+

The MonadTrans class provides the lift method, which allows you to lift an action one level in + the stack. There is also the MonadIO class that provides liftIO, which lifts an IO action as far + in the stack as desired. We have the same breakdown in monad-control. But first, we need a + corrolary to + Run:

type RunInBase m base = forall b. m b -> base (m b)
Instead + of dealing with a transformer, we're dealing with two monads. base is the underlying monad, and m + is a stack built on top of it. RunInBase is a function that takes a value of the entire stack, + pops out that base, and puts in on the outside. Unlike in the Run type, we don't replace it with + an arbitrary monad, but with the original one. To use some more concrete types:

+
RunInBase (ErrorT MyError IO) IO = forall b. ErrorT MyError IO b -> IO (ErrorT MyError IO b)
+

This should look fairly similar to what we've been looking at so far, the only difference is + that we want to deal with a specific inner monad. Our MonadControlIO class is really just an + extension of MonadControlTrans using this RunInBase.

+
class MonadIO m => MonadControlIO m where
+    liftControlIO :: (RunInBase m IO -> IO a) -> m a
+

Simply put, liftControlIO takes a function which receives a RunInBase. That RunInBase can be + used to strip down our monad to just an IO, and then liftControlIO builds everything back up + again. And like MonadControlTrans, it comes with a helper function

+
controlIO :: MonadControlIO m => (RunInBase m IO -> IO (m a)) -> m a
+controlIO = join . liftControlIO
+

We can easily rewrite our previous example with + it:

useMyFileError7 :: ErrorT MyError IO ()
+useMyFileError7 = controlIO $ \run -> withMyFile $ run . sayHiError
And + as an advantage, it easily scales to multiple + transformers:
sayHiCrazy :: Handle -> ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+sayHiCrazy handle = liftIO $ hPutStrLn handle "Madness!"
+
+useMyFileCrazy :: ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+useMyFileCrazy = controlIO $ \run -> withMyFile $ run . sayHiCrazy
+

+
+
+
+

Real Life Examples

+

+

Let's solve some real-life problems with this code. Probably the biggest motivating use case is + exception handling in a transformer stack. For example, let's say that we want to automatically + run some cleanup code when an exception is thrown. If this were normal IO code, we'd + use:

onException :: IO a -> IO b -> IO a
But if we're + in the ErrorT monad, we can't pass in either the action or the cleanup. In comes controlIO to the + rescue:
onExceptionError :: ErrorT MyError IO a
+                 -> ErrorT MyError IO b
+                 -> ErrorT MyError IO a
+onExceptionError action after = controlIO $ \run ->
+    run action `onException` run after
+

+

Let's say we need to allocate some memory to store a Double in. In the IO monad, we could just + use the alloca function. Once again, our solution is + simple:

allocaError :: (Ptr Double -> ErrorT MyError IO b)
+            -> ErrorT MyError IO b
+allocaError f = controlIO $ \run -> alloca $ run . f
+

+
+
+

Lost State

+

+

Let's rewind a bit to our onExceptionError. It uses onException under the surface, + which has a type signature: IO a -> IO b -> IO a. Let me ask you + something: what happened to the b in the output? Well, it was thoroughly ignored. But that seems + to cause us a bit of a problem. After all, we store our transformer state information in the + value of the inner monad. If we ignore it, we're essentially ignoring the monadic side effects as + well!

+

And the answer is that, yes, this does happen with monad-control. Certain functions + will drop some of the monadic side effects. This is put best by Bas, in the comments on the + relevant functions:

+

Note, any monadic side effects in m of the "release" computation + will be discarded; it is run only for its side effects in IO.

+
In practice, monad-control + will usually be doing the right thing for you, but you need to be aware that some side effects + may disappear.

+
+
+

More Complicated Cases

+

+

In order to make our tricks work so far, we've needed to have functions that give us full + access to play around with their values. Sometimes, this isn't the case. Take, for instance:

+
addMVarFinalizer :: MVar a -> IO () -> IO ()
+

In this case, we are required to have no value inside our finalizer function. Intuitively, the + first thing we should notice is that there will be no way to capture our monadic side effects. So + how do we get something like this to compile? Well, we need to explicitly tell it to drop all of + its state-holding + information:

addMVarFinalizerError :: MVar a -> ErrorT MyError IO () -> ErrorT MyError IO ()
+addMVarFinalizerError mvar f = controlIO $ \run ->
+    return $ liftIO $ addMVarFinalizer mvar (run f >> return ())
+

+

Another case from the same module + is:

modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b
Here, + we have a restriction on the return type in the second argument: it must be a tuple of the value + passed to that function and the final return value. Unfortunately, I can't see a way of writing a + little wrapper around modifyMVar to make it work for ErrorT. Instead, in this case, I copied the + definition of modifyMVar and modified it:

+
modifyMVar :: MVar a
+           -> (a -> ErrorT MyError IO (a, b))
+           -> ErrorT MyError IO b
+modifyMVar m io =
+  Control.Exception.Control.mask $ \restore -> do
+    a      <- liftIO $ takeMVar m
+    (a',b) <- restore (io a) `onExceptionError` liftIO (putMVar m a)
+    liftIO $ putMVar m a'
+    return b
+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/persistent.html b/public/book-1.1/persistent.html new file mode 100644 index 00000000..5541a603 --- /dev/null +++ b/public/book-1.1/persistent.html @@ -0,0 +1,1301 @@ + Persistent :: Yesod Web Framework Book- Version 1.1 +
+

Persistent

+ + +

+

+

Forms deal with the boundary between the user and the application. Another boundary we + need to deal with is between the application and the storage layer. Whether it be a SQL + database, a YAML file, or a binary blob, odds are you have to work to get your storage + layer to accept your application datatypes. Persistent is Yesod's answer to data + storage- a type-safe, universal data store interface for Haskell.

+

Haskell has many different database bindings available. However, most of these have + little knowledge of a schema and therefore do not provide useful static guarantees. They + also force database-dependent APIs and data types on the programmer. Haskellers have + attempted a more revolutionary route of creating Haskell specific data stores to get + around these flaws that allow one to easily store any Haskell type. These options are + great for certain use cases, but they constrain one to the storage techniques provided + by the library, do not interface well with other languages, and the flexibility can also + mean one must write reams of code for querying data. In contrast, Persistent allows us + to choose among existing databases that are highly tuned for different data storage use + cases, interoperate with other programming languages, and to use a safe and productive + query interface.

+

Persistent follows the guiding principles of type safety and concise, declarative syntax. Some other nice features are:

+
    +
  • +

    Database-agnostic. There is first class support for PostgreSQL, SQLite and MongoDB, with + experimental CouchDB and MySQL support in the works.

    +
  • +
  • +

    By being non-relational in nature, we simultaneously are able to support a wider number of storage layers and are not constrained by some of the performance bottlenecks incurred through joins.

    +
  • +
  • +

    A major source of frustration in dealing with SQL databases is changes to the schema. Persistent can automatically perform database migrations.

    +
  • +
+ +
+

Synopsis

+

+
{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}
+{-# LANGUAGE GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Control.Monad.IO.Class (liftIO)
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+main :: IO ()
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+    janeId <- insert $ Person "Jane Doe" Nothing
+
+    insert $ BlogPost "My fr1st p0st" johnId
+    insert $ BlogPost "One more for good measure" johnId
+
+    oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+    liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+    john <- get johnId
+    liftIO $ print (john :: Maybe Person)
+
+    delete janeId
+    deleteWhere [BlogPostAuthorId ==. johnId]
+
+
+

Solving the boundary issue

+

+

Suppose you are storing information on people in a SQL database. Your table might look + something like:

+
CREATE TABLE Person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)
+

And if you are using a database like PostgreSQL, you can be guaranteed that the database will never store some arbitrary text in your age field. (The same cannot be said of SQLite, but let's forget about that for now.) To mirror this database table, you would likely create a Haskell datatype that looks something like:

+
data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+

It looks like everything is type safe: the database schema matches our Haskell datatypes, the database ensures that invalid data can never make it into our data store, and everything is generally awesome. Well, until:

+
    +
  • +

    You want to pull data from the database, and the database layer gives you the data in an untyped format.

    +
  • +
  • +

    You want to find everyone older than 32, and you accidently write "thirtytwo" in your SQL statement. Guess what: that will compile just fine, and you won't find out you have a problem until runtime.

    +
  • +
  • +

    You decide you want to find the first 10 people alphabetically. No problem... until you + make a typo in your SQL. Once again, you don't find out until runtime.

    +
  • +
+

In dynamic languages, the answers to these issues is unit testing. For everything that can go wrong, make sure you write a test case. But as I am sure you are aware by now, that doesn't jive well with the Yesod approach to things. We like to take advantage of Haskell's strong typing to save us wherever possible, and data storage is no exception.

+

So the question remains: how can we use Haskell's type system to save the day?

+
+

Types

+

+

Like routing, there is nothing intrinsically difficult about type-safe data access. It + just requires a lot of monotonous, error prone, boiler plate code. As usual, this means + we can use the type system to keep us honest. And to avoid some of the drudgery, we'll + use a sprinkling of Template Haskell.

+ +

+ PersistValue is the basic building block of Persistent. It is a + sum type that can represent data that gets sent to and from a database. Its definition + is:

+
data PersistValue = PersistText Text
+                  | PersistByteString ByteString
+                  | PersistInt64 Int64
+                  | PersistDouble Double
+                  | PersistBool Bool
+                  | PersistDay Day
+                  | PersistTimeOfDay TimeOfDay
+                  | PersistUTCTime UTCTime
+                  | PersistNull
+                  | PersistList [PersistValue]
+                  | PersistMap [(T.Text, PersistValue)]
+                  | PersistForeignKey ByteString -- ^ intended especially for MongoDB backend
+

Each Persistent backend needs to know how to translate the relevant values into + something the database can understand. However, it would be awkward do have to express + all of our data simply in terms of these basic types. The next layer is the PersistField typeclass, which defines how an arbitrary Haskell + datatype can be marshaled to and from a PersistValue. A + PersistField correlates to a column in a SQL database. In our + person example above, name and age would be our PersistFields.

+

To tie up the user side of the code, our last typeclass is PersistEntity. An instance of PersistEntity correlates with a table in a + SQL database. This typeclass defines a number of functions and some associated types. To + review, we have the following correspondence between Persistent and SQL:

+ +

+ + + + + + + + + + + + + + + + + + + + + + +
SQLPersistent
Datatypes (VARCHAR, INTEGER, etc)PersistValue
ColumnPersistField
TablePersistEntity
+
+
+

Code Generation

+

+

In order to ensure that the PersistEntity instances match up properly with your Haskell datatypes, Persistent takes responsibility for both. This is also good from a DRY (Don't Repeat Yourslef) perspective: you only need to define your entities once. Let's see a quick example:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+mkPersist sqlSettings [persist|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+
+

We use a combination of Template Haskell and Quasi-Quotation (like when defining + routes): persist is a + quasi-quoter which converts a whitespace-sensitive syntax into a list of entity + definitions. (You can also declare your entities in a separate file using + persistFile.) + mkPersist takes that list + of entities and declares:

+
    +
  • +

    One Haskell datatype for each entity.

    +
  • +
  • +

    A PersistEntity instance for each datatype defined.

    +
  • +
+

The example above generates code that looks like the following:

+
{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Store
+import Database.Persist.Sqlite
+import Database.Persist.EntityDef
+import Control.Monad.IO.Class (liftIO)
+import Control.Applicative
+
+data Person = Person
+    { personName :: String
+    , personAge :: Int
+    }
+  deriving (Show, Read, Eq)
+
+type PersonId = Key SqlPersist Person
+
+instance PersistEntity Person where
+    -- 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
+
+    type PersistEntityBackend Person = SqlPersist
+
+    toPersistFields (Person name age) =
+        [ SomePersistField name
+        , SomePersistField age
+        ]
+
+    fromPersistValues [nameValue, ageValue] = Person
+        <$> fromPersistValue nameValue
+        <*> fromPersistValue ageValue
+    fromPersistValues _ = Left "Invalid fromPersistValues input"
+
+    -- Information on each field, used internally to generate SQL statements
+    persistFieldDef PersonId = FieldDef
+        (HaskellName "Id")
+        (DBName "id")
+        (FTTypeCon Nothing "PersonId")
+        []
+    persistFieldDef PersonName = FieldDef
+        (HaskellName "name")
+        (DBName "name")
+        (FTTypeCon Nothing "String")
+        []
+    persistFieldDef PersonAge = FieldDef
+        (HaskellName "age")
+        (DBName "age")
+        (FTTypeCon Nothing "Int")
+        []
+
+

As you might expect, our Person datatype closely matches the definition + we gave in the original Template Haskell version. We also have a Generalized Algebraic + Datatype (GADT) which gives a separate constructor for each field. This GADT encodes + both the type of the entity and the type of the field. We use its constructors + throughout Persistent, such as to ensure that when we apply a filter, the types of the + filtering value match the field.

+

We can use the generated Person type like any other Haskell type, and + then pass it off to other Persistent functions.

+
main = withSqliteConn ":memory:" $ runSqlConn $ do
+    michaelId <- insert $ Person "Michael" 26
+    michael <- get michaelId
+    liftIO $ print michael
+
+

We start off with some standard database connection code. In this case, we used the single-connection functions. Persistent also comes built in with connection pool functions, which we will generally want to use in production.

+

In this example, we have seen two functions: insert creates a new + record in the database and returns its ID. Like everything else in Persistent, IDs are + type safe. We'll get into more details of how these IDs work later. So when you call + insert $ Person "Michael" 25, it gives you a value back of + type PersonId.

+

The next function we see is get, which attempts to load a value from + the database using an Id. In Persistent, you never need to + worry that you are using the key from the wrong table: trying to load up a different + entity (like House) using a PersonId will + never compile.

+
+
+

PersistStore

+

+

One last detail is left unexplained from the previous example: what are those + withSqliteConn and runSqlConn functions doing, and + what is that monad that our database actions are running in?

+

All database actions need to occur within an instance of PersistStore. As its name implies, every data store (PostgreSQL, SQLite, + MongoDB) has an instance of PersistStore. This is where all the + translations from PersistValue to database-specific values + occur, where SQL query generation happens, and so on.

+ +

+ withSqliteConn creates a single connection to a database using its + supplied connection string. For our test cases, we will use :memory:, + which uses an in-memory database. runSqlConn uses that connection to + run the inner action. Both SQLite and PostgreSQL share the same instance of + PersistStore: SqlPersist.

+ +

One important thing to note is that everything which occurs inside a single call to + runSqlConn runs in a single transaction. This has two important + implications:

+
    +
  • +

    For many databases, committing a transaction can be a costly activity. By putting multiple steps into a single transaction, you can speed up code dramatically.

    +
  • +
  • +

    If an exception is thrown anywhere inside a single call to runSqlConn, + all actions will be rolled back (assuming your backend has rollback support).

    +
  • +
+
+
+
+

Migrations

+

+

I'm sorry to tell you, but so far I have lied to you a bit: the example from the previous section does not actually work. If you try to run it, you will get an error message about a missing table.

+

For SQL databases, one of the major pains can be managing schema changes. Instead of leaving this to the user, Persistent steps in to help, but you have to ask it to help. Let's see what this looks like:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+share [mkPersist sqlSettings, mkSave "entityDefs"] [persist|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration $ migrate entityDefs (undefined :: Person) -- this line added: that's it!
+    michaelId <- insert $ Person "Michael" 26
+    michael <- get michaelId
+    liftIO $ print michael
+
+

With this one little code change, Persistent will automatically create your + Person table for you. This split between + runMigration and migrate allows you to migrate + multiple tables simultaneously.

+

This works when dealing with just a few entities, but can quickly get tiresome once we + are dealing with a dozen entities. Instead of repeating yourself, Persistent provides a + helper function, mkMigrate:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int
+    deriving Show
+Car
+    color String
+    make String
+    model String
+    deriving Show
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+

+ mkMigrate is a Template Haskell function which creates a new function + that will automatically call migrate on all entities defined in the + persist block. The share function is just a little + helper that passes the information from the persist block to each Template Haskell + function and concatenates the results.

+

Persistent has very conservative rules about what it will do during a migration. It + starts by loading up table information from the database, complete with all defined SQL + datatypes. It then compares that against the entity definition given in the code. For + the following cases, it will automatically alter the schema:

+
    +
  • +

    The datatype of a field changed. However, the database may object to this modification if the data cannot be translated.

    +
  • +
  • +

    A field was added. However, if the field is not null, no default value is supplied (we'll discuss defaults later) and there is already data in the database, the database will not allow this to happen.

    +
  • +
  • +

    A field is converted from not null to null. In the opposite case, Persistent will attempt the conversion, contingent upon the database's approval.

    +
  • +
  • +

    A brand new entity is added.

    +
  • +
+

However, there are some cases that Persistent will not handle:

+
    +
  • +

    Field or entity renames: Persistent has no way of knowing that "name" has now been renamed to "fullName": all it sees is an old field called name and a new field called fullName.

    +
  • +
  • +

    Field removals: since this can result in data loss, Persistent by default will refuse + to perform the action (you can force the issue by using + runMigrationUnsafe instead of runMigration, + though it is not recommended).

    +
  • +
+

+ runMigration will print out the migrations it is running on + stderr (you can bypass this by using + runMigrationSilent). Whenever possible, it uses ALTER + TABLE calls. However, in SQLite, ALTER TABLE has very + limited abilities, and therefore Persistent must resort to copying the data from one + table to another.

+

Finally, if instead of performing a migration, you want Persistent to + give you hints about what migrations are necessary, use the + printMigration function. This function will print out the + migrations which runMigration would perform for you. This may be useful + for performing migrations that Persistent is not capable of, for adding arbitrary SQL to + a migration, or just to log what migrations occurred.

+
+
+

Uniqueness

+

+

In addition to declaring fields within an entity, you can also declare uniqueness + constraints. A typical example would be requiring that a username be unique.

+
+

Unique Username

+
User
+    username Text
+    UniqueUsername username
+
+

While each field name must begin with a lowercase letter, the uniqueness constraints must + begin with an uppercase letter.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+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"] [persist|
+Person
+    firstName String
+    lastName String
+    age Int
+    PersonName firstName lastName
+    deriving Show
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+    insert $ Person "Michael" "Snoyman" 26
+    michael <- getBy $ PersonName "Michael" "Snoyman"
+    liftIO $ print michael
+
+

To declare a unique combination of fields, we add an extra line to our declaration. Persistent knows that it is defining a unique constructor, since the line begins with a capital letter. Each following word must be a field in this entity.

+

The main restriction on uniqueness is that it can only be applied non-null fields. The + reason for this is that the SQL standard is ambiguous on how uniqueness should be + applied to NULL (e.g., is NULL=NULL true or false?). + Besides that ambiguity, most SQL engines in fact implement rules which would be contrary to what the Haskell datatypes anticipate (e.g., PostgreSQL says + that NULL=NULL is false, whereas Haskell says Nothing == + Nothing is True).

+

In addition to providing nice guarantees at the database level about consistency + of your data, uniqueness constraints can also be used to perform some specific queries + within your Haskell code, like the getBy demonstrated above. + This happens via the Unique associated type. In the example + above, we end up with a new constructor:

+
PersonName :: String -> String -> Unique Person
+
+
+

Queries

+

+

Depending on what your goal is, there are different approaches to querying the database. + Some commands query based on a numeric ID, while others will filter. Queries also differ + in the number of results they return: some lookups should return no more than one result + (if the lookup key is unique) while others can return many results.

+

Persistent therefore provides a few different query functions. As usual, we try to encode + as many invariants in the types as possible. For example, a query that can return only 0 + or 1 results will use a Maybe wrapper, whereas a query returning many + results will return a list.

+
+

Fetching by ID

+

+

The simplest query you can perform in Persistent is getting based on an ID. Since this + value may or may not exist, its return type is wrapped in a Maybe.

+
+

Using get

+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    maybePerson <- get personId
+    case maybePerson of
+        Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+        Just person -> liftIO $ print person
+
+
+

This can be very useful for sites that provide URLs like /person/5. However, in such a case, we don't usually care about the + Maybe wrapper, and just want the value, returning a 404 message if it is not + found. Fortunately, the get404 function + helps us out here. We'll go into more details when we see integration with Yesod.

+
+
+

Fetching by unique constraint

+

+

+ getBy is almost identical to get, except it takes a + uniqueness constraint instead of an ID it takes a Unique value.

+
+

Using getBy

+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    maybePerson <- getBy $ UniqueName "Michael" "Snoyman"
+    case maybePerson of
+        Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+        Just person -> liftIO $ print person
+
+
+

Like get404, there is also a getBy404 function.

+
+
+

Select functions

+

+

Most likely, you're going to want more powerful queries. You'll want to find everyone + over a certain age; all cars available in blue; all users without a registered email address. For + this, you need one of the select functions.

+

All the select functions use a similar interface, with slightly different outputs:

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionReturns
selectSourceA Source containing all the IDs and values from the database. + This allows you to write streaming code. +
selectListA list containing all the IDs and values from the database. All records will + be loaded into memory.
selectFirstTakes just the first ID and value from the database, if available
selectKeysReturns only the keys, without the values, as a + Source.
+

+ selectList is the most commonly used, so we will cover it + specifically. Understanding the others should be trivial after that.

+

+ selectList takes two arguments: a list of Filters, + and a list of SelectOpts. The former is what limits your results based on + characteristics; it allows for equals, less than, is member of, and such. + SelectOpts provides for three different features: sorting, limiting output to a + certain number of rows, and offsetting results by a certain number of rows.

+ +

Let's jump straight into an example of filtering, and then analyze it.

+
    people <- selectList [PersonAge >. 25, PersonAge <=. 30] []
+    liftIO $ print people
+
+

As simple as that example is, we really need to cover three points:

+
    +
  1. +

    + PersonAge is a constructor for an associated phantom type. That + might sound scary, but what's important is that it uniquely identifies the "age" column of the + "person" table, and that it knows that the age field is an Int. (That's the + phantom part.)

    +
  2. +
  3. +

    We have a bunch of Persistent filtering operators. They're all pretty straight-forward: just + tack a period to the end of what you'd expect. There are three gotchas here, I'll explain + below.

    +
  4. +
  5. +

    The list of filters is ANDed together, so that our constraint means "age is greater than 25 + AND age is less than or equal to 30". We'll describe ORing later.

    +
  6. +
+

The one operator that's surprisingly named is "not equals." We use + !=., since /=. is used for updates (for "divide-and-set", + described later). Don't worry: if you use the wrong one, the compiler will catch you. The other + two surprising operators are the "is member" and "is not member". They are, respectively, <-. and /<-. (both end with a period).

+

And regarding ORs, we use the ||. operator. For example:

+
    people <- selectList
+        (       [PersonAge >. 25, PersonAge <=. 30]
+            ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
+            ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
+        )
+        []
+    liftIO $ print people
+
+

This (completely nonsensical) example means: find people who are 26-30, inclusive, OR whose + names are neither Adam or Bonny, OR whose age is either 50 or 60.

+
+

SelectOpt

+

+

All of our selectList calls have included an empty list as the second + parameter. That specifies no options, meaning: sort however the database wants, return all + results, and don't skip any results. A SelectOpt has four constructors that can + be used to change all that.

+
+
Asc
+

Sort by the given column in ascending order. This uses the same phantom type as + filtering, such as PersonAge.

+
+
Desc
+

Same as Asc, in descending order.

+
+
LimitTo
+

Takes an Int argument. Only return up to the specified number of + results.

+
+
OffsetBy
+

Takes an Int argument. Skip the specified number of results.

+
+
+

The following code defines a function that will break down results into pages. It returns all + people aged 18 and over, and then sorts them by age (oldest person first). For people with the + same age, they are sorted alphabetically by last name, then first name.

+
resultsForPage pageNumber = do
+    let resultsPerPage = 10
+    selectList
+        [ PersonAge >=. 18
+        ]
+        [ Desc PersonAge
+        , Asc PersonLastName
+        , Asc PersonFirstName
+        , LimitTo resultsPerPage
+        , OffsetBy $ (pageNumber - 1) * resultsPerPage
+        ]
+
+
+
+
+
+

Manipulation

+

+

Querying is only half the battle. We also need to be able to add data to and modify + existing data in the database.

+
+

Insert

+

+

It's all well and good to be able to play with data in the database, but how does it + get there in the first place? The answer is the insert function. You just give + it a value, and it gives back an ID.

+

At this point, it makes sense to explain a bit of the philosophy behind Persistent. In + many other ORM solutions, the datatypes used to hold data are opaque: you need to go through + their defined interfaces to get at and modify the data. That's not the case with Persistent: + we're using plain old Algebraic Data Types for the whole thing. This means you still get all the + great benefits of pattern matching, currying and everything else you're used to.

+

However, there are a few things we can't do. For one, there's no way to automatically + update values in the database every time the record is updated in Haskell. Of course, with + Haskell's normal stance of purity and immutability, this wouldn't make much sense anyway, so I + don't shed any tears over it.

+

However, there is one issue that newcomers are often bothered by: why are IDs and values + completely separate? It seems like it would be very logical to embed the ID inside the value. In + other words, instead of + having:

data Person = Person { name :: String }
have
data Person = Person { personId :: PersonId, name :: String }
+

+

Well, there's one problem with this right off the bat: how do we do an + insert? If a Person needs to have an ID, and we get the ID by inserting, and an + insert needs a Person, we have an impossible loop. We could solve this with + undefined, but that's just asking for trouble.

+

OK, you say, let's try something a bit + safer:

data Person = Person { personId :: Maybe PersonId, name :: String }
I + definitely prefer insert $ Person Nothing "Michael" to insert $ Person undefined "Michael". And now our types will be much simpler, right? + For example, selectList could return a simple [Person] + instead of that ugly [Entity SqlPersist Person].

+ +

The problem is that the "ugliness" is incredibly useful. Having Entity SqlPersist Person makes it obvious, at the type level, that we're dealing with + a value that exists in the database. Let's say we want to create a link to another page that + requires the PersonId (not an uncommon occurrence as we'll discuss + later). The Entity SqlPersist Person form gives us unambiguous access to that + information; embedding PersonId within Person with a + Maybe wrapper means an extra runtime check for Just, instead + of a more error-proof compile time check.

+

Finally, there's a semantic mismatch with embedding the ID within the value. The + Person is the value. Two people are identical (in the context of a + database) if all their fields are the same. By embedding the ID in the value, we're no longer + talking about a person, but about a row in the database. Equality is no longer really equality, + it's identity: is this the same person, as opposed to an equivalent person.

+

In other words, there are some annoyances with having the ID separated out, but overall, it's + the right approach, which in the grand scheme of things leads to better, less buggy + code.

+
+
+

Update

+

+

Now, in the context of that discussion, let's think about updating. The simplest way to + update + is:

let michael = Person "Michael" 26
+    michaelAfterBirthday = michael { personAge = 27 }
But + that's not actually updating anything, it's just creating a new Person + value based on the old one. When we say update, we're not talking about + modifications to the values in Haskell. (We better not be of course, since Haskell data + types are immutable.)

+

Instead, we're looking at ways of modifying rows in a table. And the simplest way to do + that is with the update function.

+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    update personId [PersonAge =. 27]
+
+

+ update takes two arguments: an ID, and a list of Updates. The simplest update is assignment, but it's not always + the best. What if you want to increase someone's age by 1, but you don't have their + current age? Persistent has you covered:

+
haveBirthday personId = update personId [PersonAge +=. 1]
+
+

And as you might expect, we have all the basic mathematical operators: + +=., -=., *=., and + /=. (full stop). These can be convenient for updating a single + record, but they are also essential for proper ACID guarantees. Imagine the alternative: + pull out a Person, increment the age, and update the new value. If you + have two threads/processes working on this database at the same time, you're in for a + world of hurt (hint: race conditions).

+

Sometimes you'll want to update many fields at once (give all your employees a 5% pay + increase, for example). updateWhere takes two parameters: a list of + filters, and a list of updates to apply.

+
    updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- it's been a long day
+
+

Occassionally, you'll just want to completely replace the value in a database with a + different value. For that, you use (surprise) the replace function.

+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    replace personId $ Person "John" "Doe" 20
+
+
+
+

Delete

+

+

As much as it pains us, sometimes we must part with our data. To do so, we have three + functions:

+
+
delete
+

Delete based on an ID

+
+
deleteBy
+

Delete based on a unique constraint

+
+
deleteWhere
+

Delete based on a set of filters

+
+
+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    delete personId
+    deleteBy $ UniqueName "Michael" "Snoyman"
+    deleteWhere [PersonFirstName ==. "Michael"]
+
+

We can even use deleteWhere to wipe out all the records in a table, we just need to give some + hints to GHC as to what table we're interested in:

+
    deleteWhere ([] :: [Filter Person])
+
+
+
+
+

Attributes

+

+

So far, we have seen a basic syntax for our persist blocks: a line + for the name of our entities, and then an indented line for each field with two words: + the name of the field and the datatype of the field. Persistent handles more than this: + you can assign an arbitrary list of attributes after the first two words on a line.

+

Suppose we want to have a Person entity with an (optional) age and + the timestamp of when he/she was added to the system. For entities already in the + database, we want to just use the current date-time for that timestamp.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+import Control.Monad.IO.Class
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=now()
+    deriving Show
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    time <- liftIO getCurrentTime
+    runMigration migrateAll
+    insert $ Person "Michael" (Just 26) time
+    insert $ Person "Greg" Nothing time
+
+

+ Maybe is a built in, single word attribute. It makes the + field optional. In Haskell, this means it is wrapped in a Maybe. In + SQL, it makes the column nullable.

+

The default attribute is backend specific, and uses + whatever syntax is understood by the database. In this case, it uses the database's + built-in now() function. Suppose that we now want to add a + field for a person's favorite programming language:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=now()
+    language String default='Haskell'
+    deriving Show
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+ +

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:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=now()
+    language String default='Haskell'
+    country String "default='El Salvador'"
+    deriving Show
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+

One last trick you can do with attributes is to specify the names to be used for + the SQL tables and columns. This can be convenient when interacting with existing + databases.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person sql=the-person-table
+    firstName String sql=first_name
+    lastName String sql=fldLastName
+    age Int Gt Desc "sql=The Age of the Person"
+    UniqueName firstName lastName
+    deriving Show
+|]
+
+
+
+

Relations

+

+

Persistent allows references between your data types in a manner that is consistent with + supporting non-SQL databases. We do this by embedding an ID in the related entity. So if + a person has many cars:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+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"] [persist|
+Person
+    name String
+    deriving Show
+Car
+    ownerId PersonId Eq
+    name String
+    deriving Show
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+    bruce <- insert $ Person "Bruce Wayne"
+    insert $ Car bruce "Bat Mobile"
+    insert $ Car bruce "Porsche"
+    -- this could go on a while
+    cars <- selectList [CarOwnerId ==. bruce] []
+    liftIO $ print cars
+
+

Using this technique, you can define one-to-many relationships. To define many-to-many + relationships, we need a join entity, which has a one-to-many relationship with each of + the original tables. It is also a good idea to use uniqueness constraints on these. For + example, to model a situation where we want to track which people have shopped in which + stores:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+Store
+    name String
+PersonStore
+    personId PersonId
+    storeId StoreId
+    UniquePersonStore personId storeId
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+    bruce <- insert $ Person "Bruce Wayne"
+    michael <- insert $ Person "Michael"
+
+    target <- insert $ Store "Target"
+    gucci <- insert $ Store "Gucci"
+    sevenEleven <- insert $ Store "7-11"
+
+    insert $ PersonStore bruce gucci
+    insert $ PersonStore bruce sevenEleven
+
+    insert $ PersonStore michael target
+    insert $ PersonStore michael sevenEleven
+
+
+
+

Closer look at types

+

+

So far, we've spoken about Person and PersonId + without really explaining what they are. In the simplest sense, for a SQL-only system, the + PersonId could just be type PersonId = Int64. However, + that means there is nothing binding a PersonId at the type level to the + Person entity. As a result, you could accidently use a + PersonId and get a Car. In order to model this relationship, + we use phantom types. So, our next naive step would be:

+
newtype Key entity = Key Int64
+type PersonId = Key Person
+ +

And that works out really well, until you get to a backend that doesn't use Int64 for + its IDs. And that's not just a theoretical question; MongoDB uses ByteStrings + instead. So what we need is a key value that can contain an Int and a + ByteString. Seems like a great time for a sum type:

+
data Key entity = KeyInt Int64 | KeyByteString ByteString
+

But that's just asking for trouble. Next we'll have a backend that uses timestamps, so + we'll need to add another constructor to Key. This could go on for a while. + Fortunately, we already have a sum type intended for representing arbitrary data: + PersistValue:

+
newtype Key entity = Key PersistValue
+

But this has another problem. Let's say we have a web application that takes an ID as a + parameter from the user. It will need to receive that parameter as Text and then + try to convert it to a Key. Well, that's simple: write a function to convert a + Text to a PersistValue, and then wrap the result in the + Key constructor, right?

+

Wrong. We tried this, and there's a big problem. We end up getting + Keys that could never be. For example, if we're dealing with SQL, a key must be + an integer. But the approach described above would allow arbitrary textual data in. The result + was a bunch of 500 server errors as the database choked on comparing an integer column to + text.

+

So what we need is a way to convert text to a Key, but have it + dependent on the rules of the backend in question. And once phrased that way, the answer is + simple: just add another phantom. The real, actual definition of Key in + Persistent is:

+
newtype Key backend entity = Key { unKey :: PersistValue }
+

This works great: we can have a Text -> Key MongoDB entity function and a + Text -> Key SqlPersist entity function, and everything runs smoothly. But now + we have a new problem: relations. Let's say we want to represent blogs and blog posts. We would + use the entity definition:

+
Blog
+    title Text
+Post
+    title Text
+    blogId BlogId
+

But what would that look like in terms of our Key datatype?

+
data Blog = Blog { blogTitle :: Text }
+data Post = Post { postTitle :: Text, postBlogId :: Key <what goes here?> Blog }
+

We need something to fill in as the backend. In theory, we could hardcode this to + SqlPersist, or Mongo, but then our datatypes will only work + for a single backend. For an individual application, that might be acceptable, but what about + libraries defining datatypes to be used by multiple applications, using multiple backends?

+

So things got a little more complicated. Our types are actually:

+
data BlogGeneric backend = Blog { blogTitle :: Text }
+data PostGeneric backend = Post { postTitle :: Text, postBlogId :: Key backend (BlogGeneric backend) }
+

Notice that we still keep the short names for the constructors and the records. Finally, to + give a simple interface for normal code, we define some type synonyms:

+
type Blog = BlogGeneric SqlPersist
+type BlogId = Key SqlPersist Blog
+type Post = PostGeneric SqlPersist
+type PostId = Key SqlPersist Post
+

And no, SqlPersist isn't hard-coded into Persistent anywhere. That + sqlSettings parameter you've been passing to mkPersist is what + tells us to use SqlPersist. Mongo code will use mongoSettings + instead.

+

This might be quite complicated under the surface, but user code hardly ever touches + this. Look back through this whole chapter: not once did we need to deal with the + Key or Generic stuff directly. The most common place for it to + pop up is in compiler error messages. So it's important to be aware that this exists, but it + shouldn't affect you on a day-to-day basis.

+
+
+

Custom Fields

+

+

Occassionally, you will want to define a custom field to be used in your datastore. The most common case is an enumeration, such as employment status. For this, Persistent provides a helper Template Haskell function:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+data Employment = Employed | Unemployed | Retired
+    deriving (Show, Read, Eq)
+derivePersistField "Employment"
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name String
+    employment Employment
+|]
+
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+
+    insert $ Person "Bruce Wayne" Retired
+    insert $ Person "Peter Parker" Unemployed
+    insert $ Person "Michael" Employed
+
+

+ derivePersistField stores the data in the database using a string + field, and performs marshaling using the Show and Read + instances of the datatype. This may not be as efficient as storing via an integer, but + it is much more future proof: even if you add extra constructors in the future, your + data will still be valid.

+
+
+

Persistent: Raw SQL

+

+

The Persistent package provides a type safe interface to data stores. It tries to be + backend-agnostic, such as not relying on relational features of SQL. My experience has + been you can easily perform 95% of what you need to do with the high-level interface. + (In fact, most of my web apps use the high level interface exclusively.)

+

But occassionally you'll want to use a feature that's specific to a backend. One feature I've + used in the past is full text search. In this case, we'll use the SQL "LIKE" operator, which is + not modeled in Persistent. We'll get all people with the last name "Snoyman" and print the + records out.

+ +
{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving, GADTs, FlexibleContexts #-}
+import Database.Persist.Sqlite (withSqliteConn)
+import Database.Persist.TH (mkPersist, persist, share, mkMigrate, sqlSettings)
+import Database.Persist.GenericSql (runSqlConn, runMigration, SqlPersist)
+import Database.Persist.GenericSql.Raw (withStmt)
+import Data.Text (Text)
+import Database.Persist
+import Database.Persist.Store (PersistValue)
+import Control.Monad.IO.Class (liftIO)
+import qualified Data.Conduit as C
+import qualified Data.Conduit.List as CL
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    name Text
+|]
+
+main :: IO ()
+main = withSqliteConn ":memory:" $ runSqlConn $ do
+    runMigration migrateAll
+    insert $ Person "Michael Snoyman"
+    insert $ Person "Miriam Snoyman"
+    insert $ Person "Eliezer Snoyman"
+    insert $ Person "Gavriella Snoyman"
+    insert $ Person "Greg Weber"
+    insert $ Person "Rick Richardson"
+
+    -- 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'"
+    C.runResourceT $ withStmt sql []
+                C.$$ CL.mapM_ $ liftIO . print
+

There is also higher-level support that allows for automated data marshaling. Please see + the Haddock API docs for more details.

+
+
+

Integration with Yesod

+

+

So you've been convinced of the power of Persistent. How do you integrate it with your + Yesod application? If you use the scaffolding, most of the work is done for you already. + But as we normally do, we'll build up everything manually here to point out how it works + under the surface.

+

The yesod-persistent package provides the meeting point + between Persistent and Yesod. It provides the YesodPersist + typeclass, which standardizes access to the database via the runDB method. Let's see this in action.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, FlexibleContexts #-}
+{-# LANGUAGE TemplateHaskell, OverloadedStrings, GADTs, MultiParamTypeClasses #-}
+import Yesod
+import Database.Persist.Sqlite
+
+-- Define our entities as usual
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
+Person
+    firstName String
+    lastName String
+    age Int Gt Desc
+    deriving Show
+|]
+
+-- We keep our connection pool in the foundation. At program initialization, we
+-- create our initial pool, and each time we need to perform an action we check
+-- out a single connection from the pool.
+data PersistTest = PersistTest ConnectionPool
+
+-- We'll create a single route, to access a person. It's a very common
+-- occurrence to use an Id type in routes.
+mkYesod "PersistTest" [parseRoutes|
+/person/#PersonId PersonR GET
+|]
+
+-- Nothing special here
+instance Yesod PersistTest
+
+-- Now we need to define a YesodPersist instance, which will keep track of
+-- which backend we're using and how to run an action.
+instance YesodPersist PersistTest where
+    type YesodPersistBackend PersistTest = SqlPersist
+
+    runDB action = do
+        PersistTest pool <- getYesod
+        runSqlPool action pool
+
+-- We'll just return the show value of a person, or a 404 if the Person doesn't
+-- exist.
+getPersonR :: PersonId -> Handler RepPlain
+getPersonR personId = do
+    person <- runDB $ get404 personId
+    return $ RepPlain $ toContent $ show person
+
+openConnectionCount :: Int
+openConnectionCount = 10
+
+main :: IO ()
+main = withSqlitePool "test.db3" openConnectionCount $ \pool -> do
+    runSqlPool (runMigration migrateAll) pool
+    runSqlPool (insert $ Person "Michael" "Snoyman" 26) pool
+    warpDebug 3000 $ PersistTest pool
+

There are two important pieces here for general use. runDB is used to + run a DB action from within a Handler. Within the + runDB, you can use any of the functions we've spoken about so far, + such as insert and selectList.

+ +

The other new feature is get404. It works just like + get, but instead of returning a Nothing when a + result can't be found, it returns a 404 message page. The getPersonR + function is a very common approach used in real-world Yesod applications: + get404 a value and then return a response based on it.

+
+
+

Summary

+

+

Persistent brings the type safety of Haskell to your data access layer. Instead of writing + error-prone, untyped data access, or manually writing boilerplate marshal code, you can rely on + Persistent to automate the process for you.

+

The goal is to provide everything you need, most of the time. For the times when you + need something a bit more powerful, Persistent gives you direct access to the underlying data + store, so you can write whatever 5-way joins you want.

+

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.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/restful-content.html b/public/book-1.1/restful-content.html new file mode 100644 index 00000000..f5efc323 --- /dev/null +++ b/public/book-1.1/restful-content.html @@ -0,0 +1,354 @@ + RESTful Content :: Yesod Web Framework Book- Version 1.1 +
+

RESTful Content

+ + +

+

+

One of the stories from the early days of the web is how search engines wiped out entire + websites. When dynamic web sites were still a new concept, developers didn't appreciate + the difference between a GET and POST request. As a + result, they created pages- accessed with the GET method- that would + delete pages. When search engines started crawling these sites, they could wipe out all + the content.

+

If these web developers had followed the HTTP spec properly, this would not have + happened. A GET request is supposed to cause no side effects + (you know, like wiping out a site). Recently, there has been a move in web development + to properly embrace Representational State Transfer, also known as REST. This chapter + describes the RESTful features in Yesod and how you can use them to create more robust + web applications.

+
+

Request methods

+

+

In many web frameworks, you write one handler function per resource. In Yesod, the default is to have a separate handler function for each request method. The two most common request methods you will deal with in creating web sites are GET and POST. These are the most well-supported methods in HTML, since they are the only ones supported by web forms. However, when creating RESTful APIs, the other methods are very useful.

+

Technically speaking, you can create whichever request methods you like, but it is strongly recommended to stick to the ones spelled out in the HTTP spec. The most common of these are:

+
+
GET
+

Read-only requests. Assuming no other changes occur on the server, calling a + GET request multiple times should result in the same + response, barring such things as "current time" or randomly assigned + results.

+
+
POST
+

A general mutating request. A POST request should never be submitted + twice by the user. A common example of this would be to transfer funds from one + 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.

+
+
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 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.

+

See the routing and handlers chapter chapter for more information on the syntax of defining handler functions for each request method.

+
+
+

Representations

+

+

Suppose we have a Haskell datatype and value:

+
data Person = Person { name :: String, age :: Int }
+michael = Person "Michael" 25
+

We could represent that data as HTML:

+
<table>
+    <tr>
+        <th>Name</th>
+        <td>Michael</td>
+    </tr>
+    <tr>
+        <th>Age</th>
+        <td>25</td>
+    </tr>
+</table>
+

or we could represent it as JSON:

+
{"name":"Michael","age":25}
+

or as XML:

+
<person>
+    <name>Michael</name>
+    <age>25</age>
+</person>
+

Often times, web applications will use a different URL to get each of these + representations; perhaps /person/michael.html, + /person/michael.json, etc. Yesod follows the RESTful principle of a + single URL for each resource. So in Yesod, all of these would be + accessed from /person/michael.

+

Then the question becomes how do we determine which representation to + serve. The answer is the HTTP Accept header: it gives a prioritized + list of content types the client is expecting. Yesod will automatically determine which + representation to serve based upon this header.

+

Let's make that last sentence a bit more concrete with some code:

+
type ChooseRep = [ContentType] -> IO (ContentType, Content)
+class HasReps a where
+    chooseRep :: a -> ChooseRep
+

The chooseRep function takes two arguments: the value we are getting + representations for, and a list of content types that the client will accept. We + determine this by reading the Accept request header. + chooseRep returns a tuple containing the content type of our + response and the actual content.

+

This typeclass is the core of Yesod's RESTful approach to representations. Every + handler function must return an instance of HasReps. When + Yesod generates the dispatch function, it automatically applies + chooseRep to each handler, essentially giving all functions the + type Handler ChooseRep. After running the Handler and + obtaining the ChooseRep result, it is applied to the list of content + types parsed from the Accept header.

+

Yesod provides a number of instances of HasReps out of the + box. When we use defaultLayout, for example, the return type + is RepHtml, which looks like:

+
newtype RepHtml = RepHtml Content
+instance HasReps RepHtml where
+    chooseRep (RepHtml content) _ = return ("text/html", content)
+

Notice that we ignore entirely the list of expected content types. A number of the + built in representations (RepHtml, RepPlain, + RepJson, RepXml) in fact only support a single + representation, and therefore what the client requests in the Accept + header is irrelevant.

+
+

RepHtmlJson

+

+

An example to the contrary is RepHtmlJson, which provides either an + HTML or JSON representation. This instance helps greatly in programming AJAX + applications that degrade nicely. Here is an example that returns either HTML or JSON + data, depending on what the client wants.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, OverloadedStrings #-}
+{-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-}
+import Yesod
+data R = R
+mkYesod "R" [parseRoutes|
+/ RootR GET
+/#String NameR GET
+|]
+instance Yesod R
+
+getRootR = defaultLayout $ do
+    setTitle "Homepage"
+    addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"
+    toWidget [julius|
+$(function(){
+    $("#ajax a").click(function(){
+        jQuery.getJSON($(this).attr("href"), function(o){
+            $("div").text(o.name);
+        });
+        return false;
+    });
+});
+|]
+    let names = words "Larry Moe Curly"
+    [whamlet|
+<h2>AJAX Version
+<div #results>
+    AJAX results will be placed here when you click #
+    the names below.
+<ul #ajax>
+    $forall name <- names
+        <li>
+            <a href=@{NameR name}>#{name}
+
+<h2>HTML Version
+<p>
+    Clicking the names below will redirect the page #
+    to an HTML version.
+<ul #html>
+    $forall name <- names
+        <li>
+            <a href=@{NameR name}>#{name}
+
+|]
+
+getNameR name = do
+    let widget = do
+            setTitle $ toHtml name
+            [whamlet|Looks like you have Javascript off. Name: #{name}|]
+    let json = object ["name" .= name]
+    defaultLayoutJson widget json
+
+main = warpDebug 4000 R
+

Our getRootR handler creates a page with three links and some + Javascript which intercept clicks on the links and performs asynchronous requests. If + the user has Javascript enabled, clicking on the link will cause a request to be sent + with an Accept header of application/json. In that case, getNameR will + return the JSON representation instead.

+

If the user disables Javascript, clicking on the link will send the user to the + appropriate URL. A web browser places priority on an HTML representation of the data, + and therefore the page defined by the widget will be returned.

+

We can of course extend this to work with XML, Atom feeds, or even binary + representations of the data. A fun exercise could be writing a web application that + serves data simply using the default Show instances of datatypes, and + then writing a web client that parses the results using the default + Read instances.

+ +
+
+

News Feeds

+

+

A great, practical example of multiple representations if the yesod-newsfeed package. There are two major formats for news feeds on the web: RSS and Atom. They contain almost exactly the same information, but are just packaged up differently.

+

The yesod-newsfeed package defines a Feed + datatype which contains information like title, description, and last updated time. It + then provides two separate sets of functions for displaying this data: one for RSS, one + for Atom. They each define their own representation datatypes:

+
newtype RepAtom = RepAtom Content
+instance HasReps RepAtom where
+    chooseRep (RepAtom c) _ = return (typeAtom, c)
+newtype RepRss = RepRss Content
+instance HasReps RepRss where
+    chooseRep (RepRss c) _ = return (typeRss, c)
+

But there's a third module which defines another datatype:

+
data RepAtomRss = RepAtomRss RepAtom RepRss
+instance HasReps RepAtomRss where
+    chooseRep (RepAtomRss (RepAtom a) (RepRss r)) = chooseRep
+        [ (typeAtom, a)
+        , (typeRss, r)
+        ]
+

This datatype will automatically serve whichever representation the client prefers, defaulting to Atom. If a client connects that only understands RSS, assuming it provides the correct HTTP headers, Yesod will provide RSS output.

+
+
+
+

Other request headers

+

+

There are a great deal of other request headers available. Some of them only affect the + transfer of data between the server and client, and should not affect the application at + all. For example, Accept-Encoding informs the server which compression + schemes the client understands, and Host informs the server which + virtual host to serve up.

+

Other headers do affect the application, but are automatically read by + Yesod. For example, the Accept-Language header specifies which human + language (English, Spanish, German, Swiss-German) the client prefers. See the i18n chapter + for details on how this header is used.

+
+
+

Stateless

+

+

I've saved this section for the last, not because it is less important, but rather because there are no specific features in Yesod to enforce this.

+

HTTP is a stateless protocol: each request is to be seen as the beginning of a conversation. This means, for instance, it doesn't matter to the server if you requested five pages previously, it will treat your sixth request as if it's your first one.

+

On the other hand, some features on websites won't work without some kind of state. For example, how can you implement a shopping cart without saving information about items in between requests?

+

The solution to this is cookies, and built on top of this, sessions. We have a whole section addressing the sessions features in Yesod. However, I cannot stress enough that this should be used sparingly.

+

Let me give you an example. There's a popular bug tracking system that I deal with on a daily basis which horribly abuses sessions. There's a little drop-down on every page to select the current project. Seems harmless, right? What that dropdown does is set the current project in your session.

+

The result of all this is that clicking on the "view issues" link is entirely dependent on the last project you selected. There's no way to create a bookmark to your "Yesod" issues and a separate link for your "Hamlet" issues.

+

The proper RESTful approach to this is to have one resource for all of the Yesod issues and a separate one for all the Hamlet issues. In Yesod, this is easily done with a route definition like:

+
/ ProjectsR GET
+/projects/#ProjectID ProjectIssuesR GET
+/issues/#IssueID IssueR GET
+

Be nice to your users: proper stateless architecture means that basic features like bookmarks, permalinks and the back/forward button will always work.

+
+
+

Summary

+

+

Yesod adheres to the following tenets of REST:

+
    +
  • +

    Use the correct request method.

    +
  • +
  • +

    Each resource should have precisely one URL.

    +
  • +
  • +

    Allow multiple representations of data on the same URL.

    +
  • +
  • +

    Inspect request headers to determine extra information about what the client wants.

    +
  • +
+

This makes it easy to use Yesod not just for building websites, but for building APIs. In fact, using techniques such as RepHtmlJson, you can serve both a user-friendly, HTML page and a machine-friendly, JSON page from the same URL.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/routing-and-handlers.html b/public/book-1.1/routing-and-handlers.html new file mode 100644 index 00000000..680a6e65 --- /dev/null +++ b/public/book-1.1/routing-and-handlers.html @@ -0,0 +1,510 @@ + Routing and Handlers :: Yesod Web Framework Book- Version 1.1 +
+

Routing and Handlers

+ + +

+

+

If we look at Yesod as a Model-View-Controller framework, routing and handlers make up the controller. For contrast, let's describe two other routing approaches used in other web development environments:

+
    +
  • +

    Dispatch based on file name. This is how PHP and ASP work, for example.

    +
  • +
  • +

    Have a centralized routing function that parses routes based on regular expressions. Django and Rails follow this approach.

    +
  • +
+

Yesod is closer in principle to the latter technique. Even so, there are significant differences. Instead of using regular expressions, Yesod matches on pieces of a route. Instead of having a one-way route-to-handler mapping, Yesod has an intermediate data type (called the route datatype, or a type-safe URL) and creates two-way conversion functions.

+

Coding this more advanced system manually is tedious and error prone. Therefore, + Yesod defines a Domain Specific Language (DSL) for specifying routes, and provides + Template Haskell functions to convert this DSL to Haskell code. This chapter will + explain the syntax of the routing declarations, give you a glimpse of what code is + generated for you, and explain the interaction between routing and handler + functions.

+
+

Route Syntax

+

+

Instead of trying to shoe-horn route declarations into an existing syntax, + Yesod's approach is to use a simplified syntax designed just for routes. This has the + advantage of making the code not only easy to write, but simple enough for someone with + no Yesod experience to read and understand the sitemap of your application.

+

A basic example of this syntax is:

+
/             RootR     GET
+/blog         BlogR     GET POST
+/blog/#BlogId BlogPostR GET POST
+
+/static       StaticR   Static getStatic
+

The next few sections will explain the full details of what goes on in the route + declaration.

+
+

Pieces

+

+

One of the first thing Yesod does when it gets a request is split up the requested + path into pieces. The pieces are tokenized at all forward slashes. For example:

+
toPieces "/" = []
+toPieces "/foo/bar/baz/" = ["foo", "bar", "baz", ""]
+

You may notice that there are some funny things going on with trailing slashes, or + double slashes ("/foo//bar//"), or a few other things. Yesod believes in having canonical URLs; if someone requests a URL with a trailing slash, or + with a double slash, they automatically get a redirect to the canonical version. This + ensures you have one URL for one resource, and can help with your search rankings.

+

What this means for you is that you needn't concern yourself + with the exact structure of your URLs: you can safely think + about pieces of a path, and Yesod automatically handles + intercalating the slashes and escaping problematic + characters.

+

If, by the way, you want more fine-tuned control of how paths are split into pieces + and joined together again, you'll want to look at the cleanPath and + joinPath methods in the Yesod typeclass chapter.

+
+

Types of Pieces

+

+

When you are declaring your routes, you have three types of pieces at your disposal:

+
+
Static
+

This is a plain string that must be matched against precisely in the URL.

+
+
Dynamic single
+

This is a single piece (ie, between two forward slashes), but can be a user-submitted value. This is the primary method of receiving extra user input on a page request. These pieces begin with a hash (#) and are followed by a data type. The datatype must be an instance of PathPiece.

+
+
Dynamic multi
+

The same as before, but can receive multiple pieces of the URL. This must always be the last piece in a resource pattern. It is specified by an asterisk (*) followed by a datatype, which must be an instance of PathMultiPiece. Multi pieces are not as common as the other two, though they are very important for implementing features like static trees representing file structure or wikis with arbitrary hierarchies.

+
+
+

Let us take a look at some standard kinds of resource patterns you may want to write. Starting simply, the root of an application will just be /. Similarly, you may want to place your FAQ at /page/faq.

+

Now let's say you are going to write a Fibonacci website. You may construct your URLs like /fib/#Int. But there's a slight problem with this: we do not want to allow negative numbers or zero to be passed into our application. Fortunately, the type system can protect us:

+
newtype Natural = Natural Int
+instance PathPiece Natural where
+    toPathPiece (Natural i) = T.pack $ show i
+    fromPathPiece s =
+        case reads $ T.unpack s of
+            (i, ""):_
+                | i < 1 -> Nothing
+                | otherwise -> Just $ Natural i
+            [] -> Nothing
+
+

On line 1 we define a simple newtype wrapper around Int to protect ourselves from + invalid input. We can see that PathPiece is a typeclass with two + methods. toPathPiece does nothing more than convert to a + Text. fromPathPiece + attempts to convert a Text to our datatype, + returning Nothing when this conversion is impossible. By using + this datatype, we can ensure that our handler function is only ever given natural + numbers, allowing us to once again use the type system to battle the boundary issue.

+ +

Defining a PathMultiPiece is just as simple. Let's say we want to + have a Wiki with at least two levels of hierarchy; we might define a datatype such + as:

+
data Page = Page Text Text [Text] -- 2 or more
+instance PathMultiPiece Page where
+    toPathMultiPiece (Page x y z) = x : y : z
+    fromPathMultiPiece (x:y:z) = Just $ Page x y z
+    fromPathMultiPiece _ = Nothing
+
+
+
+
+

Resource name

+

+

Each resource pattern also has a name associated with it. That + name will become the constructor for the type safe + URL datatype associated with your application. + Therefore, it has to start with a capital letter. By + convention, these resource names all end with a capital R. + There is nothing forcing you to do this, it is just + common practice.

+

The exact definition of our constructor depends upon the + resource pattern it is attached to. Whatever datatypes are + included in single and multi pieces of the pattern become + arguments to the datatype. This gives us a 1-to-1 + correspondence between our type safe URL values and valid URLs + in our application.

+ +

Let's get some real examples going here. If you had the resource patterns /person/#Text named PersonR, /year/#Int named YearR and /page/faq named FaqR, you would end up with a + route data type roughly looking like:

+
data MyRoute = PersonR Text
+             | YearR Int
+             | FaqR
+

If a user requests the relative URL of + /year/2009, Yesod will convert it into the value + YearR 2009. /person/Michael becomes + PersonR "Michael" and /page/faq + becomes FaqR. On the other hand, + /year/two-thousand-nine, + /person/michael/snoyman and + /page/FAQ would all result in 404 errors without + ever seeing your code.

+
+
+

Handler specification

+

+

The last piece of the puzzle when declaring your resources is how they will be handled. There are three options in Yesod:

+
    +
  • +

    A single handler function for all request methods on a given route.

    +
  • +
  • +

    A separate handler function for each request method on a given route. Any other request + method will generate a 405 Bad Method response.

    +
  • +
  • +

    You want to pass off to a subsite.

    +
  • +
+

The first two can be easily specified. A single handler function will be a line with + just a resource pattern and the resource name, such as /page/faq + FaqR. In this case, the handler function must be named handleFaqR.

+

A separate handler for each request method will be the same, plus a list of request + methods. The request methods must be all capital letters. For example, /person/#String PersonR GET POST DELETE. In this case, you would need to + define three handler functions: getPersonR, postPersonR and deletePersonR.

+

Subsites are a very useful— but complicated— topic in Yesod. We + will cover writing subsites + later, but using them is not too difficult. The + most commonly used subsite is the static subsite, which serves + static files for your application. In order to serve static + files from /static, you would need a resource + line like:

+
/static StaticR Static getStatic
+

In this line, /static just says where in your URL + structure to serve the static files from. There is nothing + magical about the word static, you could easily replace it + with /my/non-dynamic/files.

+

The next word, StaticR, gives the resource name. The next two words + are what specify that we are using a subsite. Static is the + name of the subsite foundation datatype, and getStatic is a function that gets a Static value + from a value of your master foundation datatype.

+

Let's not get too caught up in the details of subsites now. We + will look more closely at the static subsite in the scaffolded site chapter.

+
+
+
+

Dispatch

+

+

Once you have specified your routes, Yesod will take care of all the pesky details + of URL dispatch for you. You just need to make sure to provide the appropriate handler functions. For subsite routes, you do not need to write any + handler functions, but you do for the other two. We mentioned the naming rules above + (MyHandlerR GET becomes getMyHandlerR, MyOtherHandlerR becomes handleMyOtherHandlerR). Now we need the type signature.

+
+

Return Type

+

+

Let's look at a simple handler function:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler RepHtml
+getHomeR = defaultLayout [whamlet|<h1>This is simple
+|]
+
+

Look at the type signature of getHomeR. The first component is + Handler. Handler is a special monad that + all handler functions live in. It provides access to request information, let's you send + redirects, and lots of other stuff we'll get to + soon.

+

Next we have RepHtml. When we discuss + representations we will explore the why of things more; for now, we + are just interested in the how.

+

As you might guess, RepHtml is a datatype for HTML responses. + And as you also may guess, web sites need to return responses besides HTML. CSS, + Javascript, images, XML are all necessities of a website. Therefore, the return value of + a handler function can be any instance of HasReps.

+

+ HasReps is a powerful concept that allows Yesod to + automatically choose the correct representation of your data based on the client + request. For now, we will focus just on simple instances such as + RepHtml, which only provide one representation.

+
+
+

Arguments

+

+

Not every route is as simple as the HomeR we just defined. Take + for instance our PersonR route from earlier. The name of the person + needs to be passed to the handler function. This translation is very straight-forward, + and hopefully intuitive. For example:

+
mkYesod "Args" [parseRoutes|
+/person/#Text PersonR GET
+/year/#Integer/month/#Text/day/#Int DateR
+/wiki/*Texts WikiR GET
+|]
+
+getPersonR :: Text -> Handler RepHtml
+getPersonR name = defaultLayout [whamlet|<h1>Hello #{name}!|]
+
+handleDateR :: Integer -> Text -> Int -> Handler RepPlain -- text/plain
+handleDateR year month day =
+    return $ RepPlain $ toContent $
+        T.concat [month, " ", T.pack $ show day, ", ", T.pack $ show year]
+
+getWikiR :: [Text] -> Handler RepPlain
+getWikiR = return . RepPlain . toContent . T.unwords
+
+

The arguments have the types of the dynamic pieces for each route, in the order specified. Also, notice how we are able to use both RepHtml and RepPlain.

+
+
+
+

The Handler Monad

+

+

The vast majority of code you write in Yesod sits in the Handler + monad. If you are approaching this from an MVC (Model-View-Controller) background, your + Handler code is the Controller. Some important points to know about + Handler:

+
    +
  • +

    It is an instance of MonadIO, so you can run any IO + action in your handlers with liftIO. By the way, liftIO is exported by the Yesod module + for your convenience.

    +
  • +
  • +

    Like Widget, Handler is a + fake-monad-transformer. It wraps around a ResourceT IO monad. We + discuss this type at length in the conduits appendix, but for now, we'll just say it let's you + safely allocate resources.

    +
  • +
  • +

    By "fake", I mean you can't use the standard lift function + provided by the transformers package, you must use the + Yesod-supplied one (just like with widgets).

    +
  • +
  • +

    + Handler is just a type synonym around + GHandler. GHandler let's you specify exactly + which subsite and master site you're using. The Handler synonym + says that the sub and master sites are your application's type.

    +
  • +
  • +

    + Handler provides a lot of different functionality, such as:

      +
    • +

      Providing request information.

      +
    • +
    • +

      Keeping a list of the extra response headers you've added.

      +
    • +
    • +

      Allowing you to modify the user's session.

      +
    • +
    • +

      Short-circuiting responses, for redirecting, sending static files, or + reporting errors.

      +
    • +
    +

    +
  • +
+

The remainder of this chapter will give a brief introduction to some of the most + common functions living in the Handler monad. I am specifically not covering any of the session functions; that will be addressed in + the sessions chapter.

+
+

Application Information

+

+

There are a number of functions that return information about your application as a whole, and give no information about individual requests. Some of these are:

+
+
getYesod
+

Returns your applicaton foundation value. If you store configuration values in your foundation, you will probably end up using this function a lot.

+
+
getYesodSub
+

Get the subsite foundation value. Unless you are working in a subsite, this will return the same value as getYesod.

+
+
getUrlRender
+

Returns the URL rendering function, which converts a type-safe + URL into a Text. Most of the time- like with Hamlet- + Yesod calls this function for you, but you may occassionally need to call it + directly.

+
+
getUrlRenderParams
+

A variant of getUrlRender that converts both a type-safe URL and a list of query-string parameters. This function handles all percent-encoding necessary.

+
+
+
+
+

Request Information

+

+

The most common information you will want to get about the current request is the + requested path, the query string parameters and POSTed form data. The first of those is + dealt with in the routing, as described above. The other two are best dealt with using + the forms module.

+

That said, you will sometimes need to get the data in a more raw format. For this purpose, Yesod exposes the Request datatype along with the getRequest function to retrieve it. This gives you access to the full list of GET parameters, cookies, and preferred languages. There are some convenient functions to make these lookups easier, such as lookupGetParam, lookupCookie and languages. For raw access to the POST parameters, you should use runRequest.

+

If you need even more raw data, like request headers, you can use waiRequest to access the Web Application Interface (WAI) request value. + See the WAI appendix for more details.

+
+
+

Short Circuiting

+

+

The following functions immediately end execution of a handler function and return a result to the user.

+
+
redirect
+

Sends a redirect response to the user (a 303 response). If you want to use a different + response code (e.g., a permanent 301 redirect), you can use + redirectWith.

+

+
+
notFound
+

Return a 404 response. This can be useful if a user requests a database value that doesn't exist.

+
+
permissionDenied
+

Return a 403 response with a specific error message.

+
+
invalidArgs
+

A 400 response with a list of invalid arguments.

+
+
sendFile
+

Sends a file from the filesystem with a specified content type. This is the preferred way to send static files, since the underlying WAI handler may be able to optimize this to a sendfile system call. Using readFile for sending static files should not be necessary.

+
+
sendResponse
+

Send a normal HasReps response with a 200 status code. This is really + just a convenience for when you need to break out of some deeply nested code + with an immediate response.

+
+
sendWaiResponse
+

When you need to get low-level and send out a raw WAI response. This can be + especially useful for creating streaming responses or a technique like + server-sent events.

+
+
+
+
+

Response Headers

+

+
+
setCookie
+

Set a cookie on the client. Instead of taking an expiration date, this function takes a cookie duration in minutes. Remember, you won't see this cookie using lookupCookie until the following request.

+
+
deleteCookie
+

Tells the client to remove a cookie. Once again, lookupCookie will not reflect this change until the next request.

+
+
setHeader
+

Set an arbitrary response header.

+
+
setLanguage
+

Set the preferred user language, which will show up in the result of the languages function.

+
+
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 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.

+
+
alreadyExpired
+

Sets the Expires header to the past.

+
+
expiresAt
+

Sets the Expires header to the specified date/time.

+
+
+
+
+
+

Summary

+

+

Routing and dispatch is arguably the core of Yesod: it is from here that our + type-safe URLs are defined, and the majority of our code is written within the + Handler monad. This chapter covered some of the most important and + central concepts of Yesod, so it is important that you properly digest it.

+

This chapter also hinted at a number of more complex Yesod topics that we will be covering later. But you should be able to write some very sophisticated web applications with just the knowledge you have learned up until here.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/scaffolding-and-the-site-template.html b/public/book-1.1/scaffolding-and-the-site-template.html new file mode 100644 index 00000000..d5a1dad1 --- /dev/null +++ b/public/book-1.1/scaffolding-and-the-site-template.html @@ -0,0 +1,469 @@ + Scaffolding and the Site Template :: Yesod Web Framework Book- Version 1.1 +
+

Scaffolding and the Site Template

+ + +

+

+

So you're tired of running small examples, and ready to write a real site? Then you're at + the right chapter. Even with the entire Yesod library at your fingertips, there are + still a lot of steps you need to go through to get a production-quality site setup:

+
    +
  • +

    Config file parsing

    +
  • +
  • +

    Signal handling (*nix)

    +
  • +
  • +

    More efficient static file serving

    +
  • +
  • +

    A good file layout

    +
  • +
+

The scaffolded site is a combination of many Yesoders' best practices combined together + into a ready-to-use skeleton for your sites. It is highly recommended for all sites. + This chapter will explain the overall structure of the scaffolding, how to use it, and + some of its less-than-obvious features.

+

For the most part, this chapter will not contain code samples. It is recommended that you + follow along with an actual scaffolded site.

+ +
+

How to Scaffold

+

+

The yesod package installs both a library and an + executable (conveniently named yesod as well). This executable + provides a few commands (run yesod by itself to get a list). In order to + generate a scaffolding, the command is yesod init. This will start a + question-and-answer process where you get to provide basic details (your name, the project name, + etc). After answering the questions, you will have a site template in a subfolder with the name + of your project.

+

The most important of these questions is the database backend. You get four choices here: + SQLite, PostgreSQL, MongoDB, and tiny. tiny is not a database backend; instead, it is specifying + that you do not want to use any database. This option also turns off a few extra dependencies, + giving you a leaner overall site. The remainder of this chapter will focus on the scaffoldings + for one of the database backends. There will be minor differences for the tiny backend.

+

After creating your files, the scaffolder will print a message about getting started. + It gives two sets of options for commands: one using cabal, and the other + using cabal-dev. cabal-dev is basically a wrapper around + cabal which causes all dependencies to be built in a sandbox. Using it is a good way to ensure + that installing other packages will not break your site setup. It is strongly recommended. If you + don't have cabal-dev, you can install it by running cabal + install cabal-dev.

+

Note that you really do need to use the cabal install + --only-dependencies (or cabal-dev install + --only-dependencies) command. Most likely, you do not yet have all the dependencies in + place needed by your site. For example, none of the database backends, nor the Javascript + minifier (hjsmin) are installed when installing the yesod package.

+

Finally, to launch your development site, you would use yesod + devel (or yesod --dev devel). This site will + automatically rebuild and reload whenever you change your code.

+
+
+

File Structure

+

+

The scaffolded site is built as a fully cabalized Haskell package. In addition to + source files, config files, templates, and static files are produced as well.

+
+

Cabal file

+

+

Whether directly using cabal, or indirectly using yesod devel, building your code will always go through the cabal file. If + you open the file, you'll see that there are both library and executable blocks. Only one of + these is built at a time, depending on the value of the library-only + flag. If library-only is turned on, then the library is built, which + is how yesod devel calls your app. Otherwise, the executable is + built.

+

The library-only flag should only be used by + yesod devel; you should never be explicitly passing it into + cabal. There is an additional flag, dev, that + allows cabal to build an executable, but turns on some of the same features as + the library-only flag, i.e., no optimizations and reload versions of the Shakespearean + template functions.

+

In general, you will build as follows:

+
    +
  • +

    When developing, use yesod devel exclusively.

    +
  • +
  • +

    When building a production build, perform cabal clean + && cabal configure && cabal build. This will produce an + optimized executable in your dist folder.

    +

    +
  • +
+

You'll also notice that we specify all language extensions in the cabal file. The + extensions are specified twice: once for the executable, and once for the + library. If you add any extensions to the list, add it to both places.

+

You might be surprised to see the NoImplicitPrelude extension. We turn this + on since the site includes its own module, Import, with a few changes to the + Prelude that make working with Yesod a little more convenient.

+

The last thing to note is the exported-modules list. If you add any modules to your + application, you must update this list to get yesod devel to work correctly. + Unfortunately, neither Cabal nor GHC will give you a warning if you forgot to make this + update, and instead you'll get a very scary-looking error message from yesod devel.

+

+
+
+

Routes and entities

+

+

Multiple times in this book, you've seen a comment like "We're declaring our routes/entities + with quasiquotes for convenience. In a production site, you should use an external file." The + scaffolding uses such an external file.

+

Routes are defined in config/routes, and entities in + config/models. They have the exact same syntax as the quasiquoting + you've seen throughout the book, and yesod devel knows to automatically + recompile the appropriate modules when these files change.

+

The models files is referenced by Model.hs. You are free to declare whatever you like in this file, but here are some + guidelines:

+
    +
  • +

    Any data types used in entities + must be imported/declared in Model.hs, above the persistFile call.

    +
  • +
  • +

    Helper utilities should either be declared in Import.hs + or, if very model-centric, in a file within the Model folder and + imported into Import.hs.

    +
  • +
+
+
+

Foundation and Application modules

+

+

The mkYesod function which we have used throughout the book declares a few + things:

+
    +
  • +

    Route type

    +
  • +
  • +

    Route render function

    +
  • +
  • +

    Dispatch function

    +
  • +
+

The dispatch function refers to all of the handler functions, and therefore all of those must + either be defined in the same file as the dispatch function, or be imported by the dispatch + function.

+

Meanwhile, the handler functions will almost certainly refer to the route type. Therefore, + they must be either in the same file where the route type is defined, or must import that + file. If you follow the logic here, your entire application must essentially live in a single + file!

+

Clearly this isn't what we want. So instead of using mkYesod, the scaffolding + site uses a decomposed version of the function. Foundation calls + mkYesodData, which declares the route type and render function. Since it does + not declare the dispatch function, the handler functions need not be in scope. + Import.hs imports Foundation.hs, and all the handler modules + import Import.hs.

+

In Application.hs, we call mkYesodDispatch, which creates our + dispatch function. For this to work, all handler functions must be in scope, so be sure to add an + import statement for any new handler modules you create.

+

Other than that, Application.hs is pretty simple. It provides + two functions: withDevelAppPort is used by yesod + devel to launch your app, and getApplication is used by the + executable to launch.

+

+ Foundation.hs is much more exciting. It:

+
    +
  • +

    Declares your foundation datatype

    +
  • +
  • +

    Declares a number of instances, such as Yesod, + YesodAuth, and YesodPersist +

    +
  • +
  • +

    Imports the messages files. If you look for the line starting with mkMessage, you will see that it specifies the folder containing the messages + (messages) and the default language (en, for English).

    +
  • +
+

This is the right file for adding extra instances for your foundation, such as + YesodAuthEmail or YesodBreadcrumbs.

+

We'll be referring back to this file later, as we discussed some of the special + implementations of Yesod typeclass methods.

+
+
+

Import

+

+

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'm always adding the same five import statements (Data.Text, + Control.Applicative, etc) to every handler module.

    +
  • +
  • +

    I want to make sure I never use some evil function (head, + readFile, ...) from Prelude.

    +

    +
  • +
+

The solution is to turn on the NoImplicitPrelude language extension, + re-export the parts of Prelude we want, add in all the other stuff we want, + define our own functions as well, and then import this file in all handlers.

+
+
+

Handler modules

+

+

Handler modules should go inside the Handler folder. The site + template includes one module: Handler/Root.hs. How you split up your handler + functions into individual modules is your decision, but a good rule of thumb is:

+
    +
  • +

    Different methods for the same route should go in the same file, e.g. + getBlogR and postBlogR.

    +
  • +
  • +

    Related routes can also usually go in the same file, e.g., + getPeopleR and getPersonR.

    +
  • +
+

Of course, it's entirely up to you. When you add a new handler file, make sure you do the + following:

+
    +
  • +

    Add it to version control (you are using version control, right?).

    +
  • +
  • +

    Add it to the cabal file.

    +
  • +
  • +

    Add it to the Application.hs file.

    +
  • +
  • +

    Put a module statement at the top, and an import Import line below it.

    +
  • +
+ +
+
+
+

widgetFile

+

+

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.

+

If you have a handler function:

+
getRootR = defaultLayout $(widgetFile "homepage")
+

, Yesod will look for the following files:

+
    +
  • +

    + templates/homepage.hamlet +

    +
  • +
  • +

    + templates/homepage.lucius +

    +
  • +
  • +

    + templates/homepage.cassius +

    +
  • +
  • +

    + templates/homepage.julius +

    +
  • +
+

If any of those files are present, they will be automatically included in the output.

+ +
+
+

defaultLayout

+

+

One of the first things you're going to want to customize is the look of your site. The layout + is actually broken up into two files:

+
    +
  • +

    + templates/default-layout-wrapper.hamlet contains just the basic shell of + a page. This file is interpreted as plain Hamlet, not as a Widget, and therefore cannot refer + to other widgets, embed i18n strings, or add extra CSS/JS.

    +
  • +
  • +

    + templates/default-layout.hamlet is where you would put the bulk of your + page. You must remember to include the widget value in the page, as that + contains the per-page contents. This file is interpreted as a Widget.

    +
  • +
+

Also, since default-layout is included via the widgetFile function, + any Lucius, Cassius, or Julius files named default-layout.* will + automatically be included as well.

+
+
+

Static files

+

+

The scaffolded site automatically includes the static file subsite, optimized for + serving files that will not change over the lifetime of the current build. What this means is + that:

+
    +
  • +

    When your static file identifiers are generated (e.g., + static/mylogo.png becomes mylogo_png), a query-string + parameter is added to it with a hash of the contents of the file. All of this happens at compile + time.

    +
  • +
  • +

    When yesod-static serves your static files, it sets expiration + headers far in the future, and incldues an etag based on a hash of your content.

    +
  • +
  • +

    Whenever you embed a link to mylogo_png, the rendering includes the + query-string parameter. If you change the logo, recompile, and launch your new app, the query + string will have changed, causing users to ignore the cached copy and download a new + version.

    +
  • +
+

Additionally, you can set a specific static root in your + Settings.hs file to serve from a different domain name. This has the + advantage of not requiring 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 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:

+
    +
  1. +

    Caching works properly.

    +
  2. +
  3. +

    Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with + the same hash already exists.

    +
  4. +
+

Finally, all of your Javascript is automatically minified via hjsmin.

+
+
+

Conclusion

+

+

The purpose of this chapter was not to explain every line that exists in the scaffolded site, + but instead to give a general overview to how it works. The best way to become more familiar with + it is to jump right in and start writing a Yesod site with it.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/sessions.html b/public/book-1.1/sessions.html new file mode 100644 index 00000000..81510fbe --- /dev/null +++ b/public/book-1.1/sessions.html @@ -0,0 +1,431 @@ + Sessions :: Yesod Web Framework Book- Version 1.1 +
+

Sessions

+ + +

+

+

HTTP is a stateless protocol. While some view this as a disadvantage, advocates of + RESTful web development laud this as an plus. When state is removed from the picture, it + is easier to scale applications, caching can happen automatically, and many other nice + side effects occur. You can draw many parallels with the non-mutable nature of Haskell + in general.

+

As much as possible, RESTful applications should avoid storing state about an interaction + with a client. However, it is sometimes unavoidable. Features like shopping carts are + the classic example, but other more mundane interactions like proper login handling can + be greatly enhanced by proper usage of sessions.

+

This chapter will describe how Yesod stores session data, + how you can access this data, and some special functions to help + you make the most of sessions.

+
+

Clientsession

+

+

One of the earliest packages spun off from Yesod was clientsession. This package uses encryption and signatures to store data + in a client-side cookie. The encryption prevents the user from inspecting the data, and + the signature ensures that the session can be neither hijacked nor tampered with.

+

It might sound like a bad idea from an efficiency + standpoint to store data in a cookie: after all, this means that + the data must be sent on every request. However, in practice, + clientsession can be a great boon for performance.

+
    +
  • +

    No server side database lookup is required to service a request.

    +
  • +
  • +

    We can easily scale horizontally: each request contains all the information we need to send a response.

    +
  • +
  • +

    To avoid undue bandwidth overhead, production sites can serve their static content from + a separate domain name to avoid the overhead of transmitting the session cookie for + each request.

    +
  • +
+

Storing megabytes of information in the session will be a bad idea. But for that matter, + most session implementations recommend against such practices. If you really need + massive storage for a user, it is best to store a lookup key in the session, and put the + actual data in a database.

+

All of the interaction with clientsession is handled by Yesod internally, but there are a + few spots where you can tweak the behavior just a bit.

+
+
+

Controlling sessions

+

+

There are three functions in the Yesod typeclass that control how sessions work. + encryptKey returns the encryption key used. + By default, it will take this from a local file, so that sessions can persist between + database shutdowns. This file will be automatically created and filled with random data + if it does not exist. And if you override this function to return Nothing, sessions will be disabled.

+ +

The next function is clientSessionDuration. This function gives the + number of minutes that a session should be active. The default is 120 (2 hours).

+

This value ends up affecting the session cookie in two ways: + firstly, it determines the expiration date for the cookie itself. + More importantly, however, the session expiration timestamp is + encoded inside the session signature. When Yesod decodes the + signature, it checks if the date is in the past; if so, it ignores + the session values.

+ +

And this leads very nicely to the last function: sessionIpAddress. By default, Yesod also encodes + the client's IP address inside the cookie to prevent session hijacking. In general, this + is a good thing. However, some ISPs are known for putting their users behind proxies + that rewrite their IP addresses, sometimes changing the source IP in the middle of the + session. If this happens, and you have sessionIpAddress enabled, the + user's session will be reset. Turning this setting to False will allow + a session to continue under such circumstances, at the cost of exposing a user to + session hijacking.

+
+
+

Session Operations

+

+

Like most frameworks, a session in Yesod is a key-value store. The base session API boils + down to four functions: lookupSession gets a value for a key + (if available), getSession returns all of the key/value pairs, setSession sets a value for a key, and deleteSession clears a value for a key.

+
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
+import Yesod
+import Control.Applicative ((<$>), (<*>))
+import qualified Web.ClientSession as CS
+
+data SessionExample = SessionExample
+
+mkYesod "SessionExample" [parseRoutes|
+/ Root GET POST
+|]
+
+getRoot :: Handler RepHtml
+getRoot = do
+    sess <- getSession
+    hamletToRepHtml [hamlet|
+<form method=post>
+    <input type=text name=key>
+    <input type=text name=val>
+    <input type=submit>
+<h1>#{show sess}
+|]
+
+postRoot :: Handler ()
+postRoot = do
+    (key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
+    case mval of
+        Nothing -> deleteSession key
+        Just val -> setSession key val
+    liftIO $ print (key, mval)
+    redirect Root
+
+instance Yesod SessionExample where
+    -- Make the session timeout 1 minute so that it's easier to play with
+    makeSessionBackend _ = do
+        key <- CS.getKey CS.defaultKeyFile
+        return $ Just $ clientSessionBackend key 1
+
+instance RenderMessage SessionExample FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+main :: IO ()
+main = warpDebug 3000 SessionExample
+
+
+

Messages

+

+

One usage of sessions previously alluded to is messages. They come to solve a common + problem in web development: the user performs a POST request, the web + app makes a change, and then the web app wants to simultaneously redirect + the user to a new page and send the user a success message. (This is known as + Post/Redirect/Get.)

+

Yesod provides a pair of functions to make this very easy: + setMessage stores a value in the session, and + getMessage both reads the value most recently put into + the session, and clears the old value so it does not accidently get + displayed twice.

+

It is recommended to have a call to getMessage in defaultLayout so that any available message is shown to a user + immediately, without having to remember to add getMessage calls to + every handler.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
+             QuasiQuotes, MultiParamTypeClasses #-}
+import Yesod
+
+data Messages = Messages
+
+mkYesod "Messages" [parseRoutes|
+/ RootR GET
+/set-message SetMessageR POST
+|]
+
+instance Yesod Messages where
+    defaultLayout widget = do
+        pc <- widgetToPageContent widget
+        mmsg <- getMessage
+        hamletToRepHtml [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <p>Your message was: #{msg}
+        ^{pageBody pc}
+|]
+
+instance RenderMessage Messages FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<form method=post action=@{SetMessageR}>
+    My message is: #
+    <input type=text name=message>
+    <input type=submit>
+|]
+
+postSetMessageR :: Handler ()
+postSetMessageR = do
+    msg <- runInputPost $ ireq textField "message"
+    setMessage $ toHtml msg
+    redirect RootR
+
+main :: IO ()
+main = warpDebug 3000 Messages
+
+

Initial page load, no message

+ + + + + +
+
+

New message entered in text box

+ + + + + +
+
+

After form submit, message appears at top of page

+ + + + + +
+
+

After refresh, the message is cleared

+ + + + + +
+
+
+

Ultimate Destination

+

+

Not to be confused with a horror film, this concept is used internally in yesod-auth. Suppose a user requests a page that requires + authentication. If the user is not yet logged in, you need to send him/her to the login + page. A well-designed web app will then send them back to the first page they + requested. That's what we call the ultimate destination.

+

+ redirectUltDest sends the user to the ultimate destination set + in his/her session, clearing that value from the session. It takes a default destination + as well, in case there is no destination set. For setting the session, there are three + options:

    +
  • +

    + setUltDest sets the destination to the given URL

    +
  • +
  • +

    + setUltDestCurrent sets the destination to the currently + requested URL.

    +
  • +
  • +

    + setUltDestReferer sets the destination based on the + Referer header (the page that led the user to the current + page).

    +
  • +
+

+

Let's look at a small sample app. It will allow the user to set his/her name in the + session, and then tell the user his/her name from another route. If the name hasn't been + set yet, the user will be redirected to the set name page, with an ultimate destination + set to come back to the current page.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
+             QuasiQuotes, MultiParamTypeClasses #-}
+import Yesod
+
+data UltDest = UltDest
+
+mkYesod "UltDest" [parseRoutes|
+/ RootR GET
+/setname SetNameR GET POST
+/sayhello SayHelloR GET
+|]
+
+instance Yesod UltDest
+
+instance RenderMessage UltDest FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getRootR = defaultLayout [whamlet|
+<p>
+    <a href=@{SetNameR}>Set your name
+<p>
+    <a href=@{SayHelloR}>Say hello
+|]
+
+-- Display the set name form
+getSetNameR = defaultLayout [whamlet|
+<form method=post>
+    My name is #
+    <input type=text name=name>
+    . #
+    <input type=submit value="Set name">
+|]
+
+-- Retreive the submitted name from the user
+postSetNameR :: Handler ()
+postSetNameR = do
+    -- Get the submitted name and set it in the session
+    name <- runInputPost $ ireq textField "name"
+    setSession "name" name
+
+    -- After we get a name, redirect to the ultimate destination.
+    -- If no destination is set, default to the homepage
+    redirectUltDest RootR
+
+getSayHelloR = do
+    -- Lookup the name value set in the session
+    mname <- lookupSession "name"
+    case mname of
+        Nothing -> do
+            -- No name in the session, set the current page as
+            -- the ultimate destination and redirect to the
+            -- SetName page
+            setUltDestCurrent
+            setMessage "Please tell me your name"
+            redirect SetNameR
+        Just name -> defaultLayout [whamlet|
+<p>Welcome #{name}
+|]
+
+main :: IO ()
+main = warpDebug 3000 UltDest
+
+
+

Summary

+

+

Sessions are the number one way we bypass the statelessness imposed by HTTP. We + shouldn't consider this an escape hatch to perform whatever actions we want: + statelessness in web applications is a virtue, and we should respect it whenever + possible. However, there are specific cases where it is vital to retain some state.

+

The session API in Yesod is very simple. It provides a key-value store, and a few + convenience functions built on top for common use cases. If used properly, with small + payloads, sessions should be an unobtrusive part of your web development.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/settings-types.html b/public/book-1.1/settings-types.html new file mode 100644 index 00000000..a1e59cc1 --- /dev/null +++ b/public/book-1.1/settings-types.html @@ -0,0 +1,177 @@ + Settings Types :: Yesod Web Framework Book- Version 1.1 +
+

Settings Types

+ + +

+

+

Let's say you're writing a webserver. You want the server to take a port to listen on, and an + application to run. So you create the following function:

+
run :: Int -> Application -> IO ()
+

But suddenly you realize that some people will want to customize their timeout durations. So + you modify your API:

+
run :: Int -> Int -> Application -> IO ()
+

So, which Int is the timeout, and which is the port? Well, you could create + some type aliases, or comment your code. But there's another problem creeping into our code: this + run function is getting unmanageable. Soon we'll need to take an extra + parameter to indicate how exceptions should be handled, and then another one to control which + host to bind to, and so on.

+

So a more extensible solution is to introduce a settings datatype:

+
data Settings = Settings
+    { settingsPort :: Int
+    , settingsHost :: String
+    , settingsTimeout :: Int
+    }
+

And this makes the calling code almost self-documenting:

+
run Settings
+    { settingsPort = 8080
+    , settingsHost = "127.0.0.1"
+    , settingsTimeout = 30
+    } myApp
+

Great, couldn't be clearer, right? True, but what happens when you have 50 settings to your + webserver. Do you really want to have to specify all of those each time? Of course not. So + instead, the webserver should provide a set of defaults:

+
defaultSettings = Settings 3000 "127.0.0.1" 30
+

And now, instead of needing to write that long bit of code above, we can get away with:

+
run defaultSettings { settingsPort = 8080 } myApp -- (1)
+

This is great, except for one minor hitch. Let's say we now decide to add an extra record to + Settings. Any code out in the wild looking like + this:

run (Settings 8080 "127.0.0.1" 30) myApp -- (2)
will + be broken, since the Settings constructor now takes 4 arguments. The proper + thing to do would be to bump the major version number so that dependent packages don't get + broken. But having to change major versions for every minor setting you add is a nuisance. The + solution? Don't export the Settings constructor:

+
module MyServer
+    ( Settings
+    , settingsPort
+    , settingsHost
+    , settingsTimeout
+    , run
+    , defaultSettings
+    ) where
+

With this approach, no one can write code like (2), so you can freely add new records without + any fear of code breaking.

+

The one downside of this approach is that it's not immediately obvious from the Haddocks that + you can actually change the settings via record syntax. That's the point of this chapter: to + clarify what's going on in the libraries that use this technique.

+

I personally use this technique in a few places, feel free to have a look at the Haddocks to + see what I mean.

+
    +
  • +

    Warp: Settings +

    +
  • +
  • +

    http-conduit: Request and + ManagerSettings +

    +
  • +
  • +

    xml-conduit

      +
    • +

      Parsing: ParseSettings +

      +
    • +
    • +

      Rendering: RenderSettings +

      +
    • +
    +

    +
  • +
+

As a tangential issue, http-conduit and xml-conduit actually + create instances of the Default typeclass instead of + declaring a brand new identifier. This means you can just type def instead of + defaultParserSettings.

+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/shakespearean-templates.html b/public/book-1.1/shakespearean-templates.html new file mode 100644 index 00000000..d165d412 --- /dev/null +++ b/public/book-1.1/shakespearean-templates.html @@ -0,0 +1,932 @@ + Shakespearean Templates :: Yesod Web Framework Book- Version 1.1 +
+

Shakespearean Templates

+ + +

+

+

Yesod uses the Shakespearean family of template languages as its standard approach to HTML, CSS + and Javascript creation. This language family shares some common syntax, as well as overarching + principles:

    +
  • +

    As little interference to the underlying language as possible, while providing + conveniences where unobtrusive.

    +
  • +
  • +

    Compile-time guarantees on well-formed content.

    +
  • +
  • +

    Static type safety, greatly helping the prevention of XSS + (cross-site scripting) attacks.

    +
  • +
  • +

    Automated checking of valid URLs, whenever possible, through type-safe + URLs.

    +
  • +
+

+

There is nothing inherently tying Yesod to these languages, or the other way around: + each can be used independently of the other. This chapter will address these template languages + on their own, while the remainder of the book will use 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 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 Javascript; the only added feature is variable + interpolation.

+
+

Hamlet (HTML)

+

+
$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle} - My Site
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1 .page-title>#{pageTitle}
+        <p>Here is a list of your friends:
+        $if null friends
+            <p>Sorry, I lied, you don't have any friends.
+        $else
+            <ul>
+                $forall Friend name age <- friends
+                    <li>#{name} (#{age} years old)
+        <footer>^{copyright}
+
+
+

Cassius (CSS)

+

+
#myid
+    color: #{red}
+    font-size: #{bodyFontSize}
+foo bar baz
+    background-image: url(@{MyBackgroundR})
+
+
+

Lucius (CSS)

+

+
section.blog {
+    padding: 1em;
+    border: 1px solid #000;
+    h1 {
+        color: #{headingColor};
+    }
+}
+
+
+

Julius (Javascript)

+

+
$(function(){
+    $("section.#{sectionClass}").hide();
+    $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
+    ^{addBling}
+});
+
+
+
+

Types

+

+

Before we jump into syntax, let's take a look at the various types involved. We + mentioned in the introduction that types help protect us from XSS attacks. For example, let's say + that we have an HTML template that should display someone's name; it might look like + this:

<p>Hello, my name is #{name}
+ +

+

What should happen to name, and what should its datatype be? + A naive approach would be to use a Text value, and insert it verbatim. But that + would give us quite a problem when name="<script + src='http://nefarious.com/evil.js'></script>". What we want is to be able to + entity-encode the name, so that < becomes &lt;.

+

An equally naive approach is to simply entity-encode every piece of text + that gets embedded. What happens when you have some preexisting HTML generated from another + process? For example, on the Yesod website, all Haskell code snippets are run through a + colorizing function that wraps up words in appropriate span tags. If we + entity escaped everything, code snippets would be completely unreadable!

+

Instead, we have an Html datatype. In order to generate an + Html value, we have two options for APIs: the ToHtml typeclass + provides a way to convert String and Text values into + Html, via its toHtml function, automatically escaping entities + along the way. This would be the approach we'd want for the name above. For the code snippet + example, we would use the preEscaped family of functions.

+

When you use variable interpolation in Hamlet (the HTML Shakespeare language), it + automatically applies a toHtml call to the value inside. So if you interpolate a + String, it will be entity-escaped. But if you provide an Html + value, it will appear unmodified. In the code snippet example, we might interpolate with + something like #{preEscapedText myHaskellHtml}.

+ +

Similarly, we have Css/ToCss, as + well as Javascript/ToJavascript. These + provide some compile-time sanity checks that we haven't accidently stuck some HTML in our + CSS.

+ +
+

Type-safe URLs

+

+

Possibly the most unique feature in Yesod is type-safe URLs, and the ability to use + them conveniently is provided directly by Shakespeare. Usage is nearly identical to variable + interpolation, we just use the at-sign (@) instead of the hash (#). We'll cover the syntax later; + first, let's clarify the intuition.

+

Suppose we have an application with two routes: http://example.com/profile/home is the homepage, and http://example.com/display/time displays the current time. And let's say we want to + link from the homepage to the time. I can think of three different ways of constructing the + URL:

    +
  1. +

    As a relative link: ../display/time +

    +
  2. +
  3. +

    As an absolute link, without a domain: /display/time +

    +
  4. +
  5. +

    As an absolute link, with a domain: http://example.com/display/time +

    +
  6. +
+

+

There are problems with each approach: the first will break if either URL changes. Also, it's + not suitable for all use cases; RSS and Atom feeds, for instance, require absolute URLs. The + second is more resilient to change than the first, but still won't be acceptable for RSS and + Atom. And while the third works fine for all use cases, you'll need to update every single URL in + your application whenever your domain name changes. You think that doesn't happen often? Just + wait till you move from your development to staging and finally production server.

+

But more importantly, there is one huge problem with all approaches: if you change your routes + at all, the compiler won't warn you about the broken links. Not to mention that typos can wreak + havoc as well.

+

The goal of type-safe URLs is to let the compiler check things for us as much as + possible. In order to facilitate this, our first step must be to move away from plain old text, + which the compiler doesn't understand, to some well defined datatypes. For our simple + application, let's model our routes with a sum + type:

data MyRoute = Home | Time
+

+

Instead of placing a link like /display/time in our template, we can use the + Time constructor. But at the end of the day, HTML is made up of text, not data + types, so we need some way to convert these values to text. We call this a URL + rendering function, and a simple one + is:

renderMyRoute :: MyRoute -> Text
+renderMyRoute Home = "http://example.com/profile/home"
+renderMyRoute Time = "http://example.com/display/time"
+

+

+

+

+

OK, we have our render function, and we have type-safe URLs embedded in the templates. + How does this fit together exactly? Instead of generating an Html (or + Css or Javascript) value directly, Shakespearean templates + actually produce a function, which takes this render function and produces HTML. To see this + better, let's have a quick (fake) peek at how Hamlet would work under the surface. Supposing we + had a + template:

<a href=@{Time}>The time
this + would translate roughly into the Haskell + code:
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
+

+
+
+
+

Syntax

+

+

All Shakespearean languages share the same interpolation syntax, and are able to + utilize type-safe URLs. They differ in the syntax specific for their target language + (HTML, CSS, or Javascript).

+
+

Hamlet Syntax

+

+

Hamlet is the most sophisticated of the languages. Not only does it provide + syntax for generating HTML, it also allows for basic control structures: conditionals, + looping, and maybes.

+
+

Tags

+

+

Obviously tags will play an important part of any HTML template language. In Hamlet, we try to + stick very close to existing HTML syntax to make the language more comfortable. However, instead + of using closing tags to denote nesting, we use indentation. So something like this in + HTML:

<body>
+<p>Some paragraph.</p>
+<ul>
+<li>Item 1</li>
+<li>Item 2</li>
+</ul>
+</body>
would + be
<body>
+    <p>Some paragraph.
+    <ul>
+        <li>Item 1
+        <li>Item 2
+

+

In general, we find this to be easier to follow than HTML once you get accustomed to it. The + only tricky part comes with dealing with whitespace before and after tags. For example, let's say + you want to create the + HTML

<p>Paragraph <i>italic</i> end.</p>
We want to make sure + that there is a whitespace preserved after the word "Paragraph" and before the word "end". To do + so, we use two simple escape + characters:
<p>
+    Paragraph #
+    <i>italic
+    \ end.
The + whitespace escape rules are actually very simple:
    +
  1. +

    If the first non-space character in a line is a backslash, the backslash is ignored.

    +
  2. +
  3. +

    If the last character in a line is a hash, it is ignored.

    +
  4. +
+

+

One other thing. Hamlet does not escape entities within its content. + This is done on purpose to allow existing HTML to be more easily copied in. So the example above + could also be written + as:

<p>Paragraph <i>italic</i> end.
Notice + that the first tag will be automatically closed by Hamlet, while the inner "i" tag will not. You + are free to use whichever approach you want, there is no penalty for either choice. Be aware, + however, that the only time you use closing tags in Hamlet is for such inline tags; normal + tags are not closed.

+
+
+

Interpolation

+

+

What we have so far is a nice, simplified HTML, but it doesn't let us interact with our Haskell + code at all. How do we pass in variables? Simple: with + interpolation:

<head>
+    <title>#{title}
The hash followed by a pair + of braces denotes variable interpolation. In the case above, the title + variable from the scope in which the template was called will be used. Let me state that again: + Hamlet automatically has access to the variables in scope when it's called. There is no need to + specifically pass variables in.

+

You can apply functions within an interpolation. You can use string and numeric + literals in an interpolation. You can use qualified modules. Both parentheses and the dollar sign + can be used to group statements together. And at the end, the toHtml + function is applied to the result, meaning any instance of ToHtml can be interpolated. Take, for instance, the following code.

+
-- Just ignore the quasiquote stuff for now, and that shamlet thing.
+-- It will be explained later.
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (shamlet)
+import Text.Blaze.Renderer.String (renderHtml)
+import Data.Char (toLower)
+import Data.List (sort)
+
+data Person = Person
+    { name :: String
+    , age  :: Int
+    }
+
+main :: IO ()
+main = putStrLn $ renderHtml [shamlet|
+<p>Hello, my name is #{name person} and I am #{show $ age person}.
+<p>
+    Let's do some funny stuff with my name: #
+    <b>#{sort $ map toLower (name person)}
+<p>Oh, and in 5 years I'll be #{show ((+) 5 (age person))} years old.
+|]
+  where
+    person = Person "Michael" 26
+

What about our much-touted type-safe URLs? They are almost identical to variable + interpolation in every way, except they start with an at-sign (@) instead. In addition, there is + embedding via a caret (^) which allows you to embed another template of the same type. The next + code sample demonstrates both of these.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Renderer.String (renderHtml)
+import Data.Text (Text)
+
+data MyRoute = Home
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+
+footer :: HtmlUrl MyRoute
+footer = [hamlet|
+<footer>
+    Return to #
+    <a href=@{Home}>Homepage
+    .
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ [hamlet|
+<body>
+    <p>This is my page.
+    ^{footer}
+|] render
+
+
+

Attributes

+

+

In that last example, we put an href attribute on the "a" tag. Let's elaborate on the + syntax:

    +
  • +

    You can have interpolations within the attribute value.

    +
  • +
  • +

    The equals sign and value for an attribute are optional, just like in HTML. So + <input type=checkbox checked> is perfectly valid.

    +
  • +
  • +

    There are two convenience attributes: for id, you can use the hash, and for classes, + the period. In other words, <p #paragraphid .class1 + .class2>.

    +
  • +
  • +

    While quotes around the attribute value are optional, they are required if you want + to embed spaces.

    +
  • +
  • +

    You can add an attribute optionally by using colons. To make a checkbox only checked + if the variable isChecked is True, you would write <input type=checkbox + :isChecked:checked>. To have a paragraph be optionally red, you could use <p :isRed:style="color:red">.

    +
  • +
+

+
+
+

Conditionals

+

+

Eventually, you'll want to put in some logic in your page. The goal of Hamlet is to + make the logic as minimalistic as possible, pushing the heavy lifting into Haskell. As + such, our logical statements are very basic... so basic, that it's if, + elseif, and + else.

$if isAdmin
+    <p>Welcome to the admin section.
+$elseif isLoggedIn
+    <p>You are not the administrator.
+$else
+    <p>I don't know who you are. Please log in so I can decide if you get access.
All + the same rules of normal interpolation apply to the content of the conditionals.

+
+
+

Maybe

+

+

Similarly, we have a special construct for dealing with Maybe values. This could + technically be dealt with using if, isJust and + fromJust, but this is more convenient and avoids partial + functions.

$maybe name <- maybeName
+    <p>Your name is #{name}
+$nothing
+    <p>I don't know your name.
In + addition to simple identifiers, you can use a few other, more complicated values on the left hand + side, such as constructors and tuples.

+
$maybe Person firstName lastName <- maybePerson
+    <p>Your name is #{firstName} #{lastName}
+

The right-hand-side follows the same rules as interpolations, allow variables, function + application, and so on.

+
+
+

Forall

+

+

And what about looping over lists? We have you covered there + too:

$if null people
+    <p>No people.
+$else
+    <ul>
+        $forall person <- people
+            <li>#{person}
+

+
+
+

Case

+

+

Pattern matching is one of the great strengths of Haskell. Sum types let you cleanly model many + real-world types, and case statements let you safely match, letting the compiler + warn you if you missed a case. Hamlet gives you the same power.

+
$case foo
+    $of Left bar
+        <p>It was left: #{bar}
+    $of Right baz
+        <p>It was right: #{baz}
+
+
+

With

+

+

Rounding out our statements, we have with. It's basically just a + convenience for declaring a synonym for a long + expression.

$with foo <- some very (long ugly) expression that $ should only $ happen once
+    <p>But I'm going to use #{foo} multiple times. #{foo}
+

+
+
+

Doctype

+

+

Last bit of syntactic sugar: the doctype statement. We have support for a number of + different versions of a doctype, though we recommend $doctype + 5 for modern web applications, which generates <!DOCTYPE + html>.

$doctype 5
+<html>
+    <head>
+        <title>Hamlet is Awesome
+    <body>
+        <p>All done.
+ +

+
+
+
+

Cassius Syntax

+

+

Cassius is the original CSS template language. It uses simple whitespace rules to delimit + blocks, making braces and semicolons unnecessary. It supports both variable and URL + interpolation, but not embedding. The syntax is very + straight-forward:

#banner
+    border: 1px solid #{bannerColor}
+    background-image: url(@{BannerImageR})
+

+
+
+

Lucius Syntax

+

+

While Cassius uses a modified, whitespace-sensitive syntax for CSS, Lucius is true to + the original. You can take any CSS file out there and it will be a valid Lucius file. There are, + however, a few additions to Lucius:

    +
  • +

    Like Cassius, we allow both variable and URL interpolation.

    +
  • +
  • +

    CSS blocks are allowed to nest.

    +
  • +
  • +

    You can declare variables in your templates.

    +
  • +
+

+

Starting the with second point: let's say you want to have some special styling for + some tags within your article. In plain ol' CSS, you'd have to + write:

article code { background-color: grey; }
+article p { text-indent: 2em; }
+article a { text-decoration: none; }
In + this case, there aren't that many clauses, but having to type out article each time is still a + bit of a nuisance. Imagine if you had a dozen or so of these. Not the worst thing in the world, + but a bit of an annoyance. Lucius helps you out + here:
article {
+    code { background-color: grey; }
+    p { text-indent: 2em; }
+    a { text-decoration: none; }
+}
+

+

Having Lucius variables allows you to avoid repeating yourself. A simple example would be to + define a commonly used color:

+
@textcolor: #ccc; /* just because we hate our users */
+body { color: #{textcolor} }
+a:link, a:visited { color: #{textcolor} }
+

Other than that, Lucius is identical to CSS.

+
+
+

Julius Syntax

+

+

Julius is the simplest of the languages discussed here. In fact, some might even say + it's really just Javascript. Julius allows the three forms of interpolation we've mentioned so + far, and otherwise applies no transformations to your content.

+

+
+
+
+

Calling Shakespeare

+

+

The question of course arises at some point: how do I actually use this stuff? There are three + different ways to call out to Shakespeare from your Haskell code:

+
+
Quasiquotes
+

Quasiquotes allow you to embed arbitrary content within your Haskell, and for it to be + converted into Haskell code at compile time.

+
+
External file
+

In this case, the template code is in a separate file which is referenced via Template + Haskell.

+
+
Reload mode
+

Both of the above modes require a full recompile to see any changes. In reload + mode, your template is kept in a separate file and referenced via Template Haskell. But at + runtime, the external file is reparsed from scratch each time.

+

+
+
+

One of the first two approaches should be used in production. They both embed the entirety of + the template in the final executable, simplifying deployment and increasing performance. The + advantage of the quasiquoter is the simplicity: everything stays in a single file. For short + templates, this can be a very good fit. However, in general, the external file approach is + recommended because:

    +
  • +

    It follows nicely in the tradition of separate logic from presentation.

    +
  • +
  • +

    You can easily switch between external file and debug mode with some simple CPP macros, + meaning you can keep rapid development and still achieve high performance in production.

    +
  • +
+

+

Since these are special QuasiQuoters and Template Haskell functions, you need to be + sure to enable the appropriate language extensions and use correct syntax. You can see a simple + example of each in the figures.

+
+

Quasiquoter

+
{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Data.Text (Text)
+import Text.Blaze.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: Text -> HtmlUrl MyRoute
+template title = [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{title}
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1>#{title}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ template "My Title" render
+
+
+

External file

+
{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-} -- to control production versus debug
+import Text.Lucius (CssUrl, luciusFile, luciusFileDebug, renderCss)
+import Data.Text (Text)
+import qualified Data.Text.Lazy.IO as TLIO
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: CssUrl MyRoute
+#if PRODUCTION
+template = $(luciusFile "template.lucius")
+#else
+template = $(luciusFileDebug "template.lucius")
+#endif
+
+main :: IO ()
+main = TLIO.putStrLn $ renderCss $ template render
+
-- @template.lucius
+foo { bar: baz }
+
+

The naming scheme for the functions is very consistent.

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageQuasiquoterExternal fileReload
Hamlet + hamlet + + hamletFile + + N/A +
Cassius + cassius + + cassiusFile + + cassiusFileReload +
Lucius + lucius + + luciusFile + + luciusFileReload +
Julius + julius + + juliusFile + + juliusFileReload +
+
+

Alternate Hamlet Types

+

+

So far, we've seen how to generate an HtmlUrl value from Hamlet, which is a + piece of HTML with embedded type-safe URLs. There are currently three other values we can + generate using Hamlet: plain HTML, HTML with URLs and internationalized messages, and + widgets. That last one will be covered in the widgets chapter.

+

To generate plain HTML without any embedded URLs, we use "simplified Hamlet". There are a few + changes:

    +
  • +

    We use a different set of functions, prefixed with an "s". So the quasiquoter is + shamlet and the external file function is shamletFile. How + we pronounce those is still up for debate.

    +
  • +
  • +

    No URL interpolation is allowed. Doing so will result in a compile-time error.

    +
  • +
  • +

    Embedding (the caret-interpolator) no longer allows arbitrary HtmlUrl values. The rule is that the embedded value must have the same type as the + template itself, so in this case it must be Html. That means that for + shamlet, embedding can be completely replaced with normal variable + interpolation (with a hash).

    +
  • +
+

+

Dealing with internationalization (i18n) in Hamlet is a bit complicated. Hamlet + supports i18n via a message datatype, very similar in concept and implementation to a type-safe + URL. As a motivating example, let's say we want to have an application that tells you hello and + how many apples you have eaten. We could represent those messages with a + datatype.

data Msg = Hello | Apples Int
Next, + we would want to be able to convert that into something human-readable, so we define some render + functions:
renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
Now + we want to interpolate those Msg values directly in the template. For that, we use underscore + interpolation.
$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+

+

This kind of a template now needs some way to turn those values into HTML. So just + like type-safe URLs, we pass in a render function. To represent this, we define a new type + synonym:

type Render url = url -> [(Text, Text)] -> Text
+type Translate msg = msg -> Html
+type HtmlUrlI18n msg url = Translate msg -> Render url -> Html
At + this point, you can pass renderEnglish, renderSpanish, or renderKlingon to this template, and it + will generate nicely translated output (depending, of course, on the quality of your + translators). The complete program is:

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Data.Text (Text)
+import qualified Data.Text as T
+import Text.Hamlet (HtmlUrlI18n, ihamlet)
+import Text.Blaze (toHtml)
+import Text.Blaze.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+renderUrl :: MyRoute -> [(Text, Text)] -> Text
+renderUrl Home _ = "/home"
+renderUrl Time _ = "/time"
+renderUrl Stylesheet _ = "/style.css"
+
+data Msg = Hello | Apples Int
+
+renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+
+template :: Int -> HtmlUrlI18n Msg MyRoute
+template count = [ihamlet|
+$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml
+     $ (template 5) (toHtml . renderEnglish) renderUrl
+
+
+
+

Other Shakespeare

+

+

In addition to HTML, CSS and Javascript helpers, there is also some more general-purpose + Shakespeare available. shakespeare-text provides a simple way to create + interpolated strings, much like people are accustomed to in scripting languages like Ruby and + Python. This package's utility is definitely not limited to Yesod.

+
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
+import Text.Shakespeare.Text
+import qualified Data.Text.Lazy.IO as TLIO
+import Data.Text (Text)
+import Control.Monad (forM_)
+
+data Item = Item
+    { itemName :: Text
+    , itemQty :: Int
+    }
+
+items :: [Item]
+items =
+    [ Item "apples" 5
+    , Item "bananas" 10
+    ]
+
+main :: IO ()
+main = forM_ items $ \item -> TLIO.putStrLn
+    [lt|You have #{show $ itemQty item} #{itemName item}.|]
+

Some quick points about this simple example:

+
    +
  • +

    Notice that we have three different textual datatypes involved + (String, strict Text and lazy Text). They + all play together well.

    +
  • +
  • +

    We use a quasiquoter named lt, which generates lazy text. There is also + st.

    +
  • +
  • +

    Also, there are longer names for these quasiquoters (ltext and + stext).

    +
  • +
+
+
+

General Recommendations

+

+

Here are some general hints from the Yesod community on how to get the most out of + Shakespeare.

+
    +
  • +

    For actual sites, use external files. For libraries, it's OK to use quasiquoters, assuming they aren't too long.

    +
  • +
  • +

    Patrick Brisbin has put together a Vim code + highlighter that can help out immensely.

    +
  • +
  • +

    You should almost always start Hamlet tags on their own line instead of embedding + start/end tags after an existing tag. The only + exception to this is the occasional + <i> or + <b> tag inside a large + block of text.

    +
  • +
+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/web-application-interface.html b/public/book-1.1/web-application-interface.html new file mode 100644 index 00000000..324a95eb --- /dev/null +++ b/public/book-1.1/web-application-interface.html @@ -0,0 +1,203 @@ + Web Application Interface :: Yesod Web Framework Book- Version 1.1 +
+

Web Application Interface

+ + +

+

+

It is a problem almost every language used for web development has dealt with: the low level interface between the web server and the application. The earliest example of a solution is the venerable and battle-worn CGI (CGI), providing a language-agnostic interface using only standard input, standard output and environment variables.

+

Back when Perl was becoming the de facto web programming language, a major shortcoming of CGI became apparent: the process needed to be started anew for each request. When dealing with an interpretted language and application requiring database connection, this overhead became unbearable. FastCGI (and later SCGI) arose as a successor to CGI, but it seems that much of the programming world went in a different direction.

+

Each language began creating its own standard for interfacing with servers. mod_perl. + mod_python. mod_php. mod_ruby. Within the same language, multiple interfaces arose. In + some cases, we even had interfaces on top of interfaces. And all of this led to much + duplicated effort: a Python application designed to work with FastCGI wouldn't work with + mod_python; mod_python only exists for certain webservers; and these programming + language specific web server extensions need to be written for each programming + language.

+

Haskell has its own history. We originally had the cgi package, which provided a monadic interface. The fastcgi package then provided the same interface. Meanwhile, it seemed that the majority of Haskell web development focused on the standalone server. The problem is that each server comes with its own interface, meaning that you need to target a specific backend. This means that it is impossible to share common features, like GZIP encoding, development servers, and testing frameworks.

+

WAI attempts to solve this, by providing a generic and efficient interface between web servers and applications. Any handler supporting the interface can serve any WAI application, while any application using the interface can run on any handler.

+

At the time of writing, there are various backends, including Warp, FastCGI, and + development server. There are even more esoteric backends like wai-handler-webkit for creating desktop apps. wai-extra provides many common middleware components like GZIP, JSON-P + and virtual hosting. wai-test makes it easy to write unit + tests, and wai-handler-devel lets you develop your + applications without worrying about stopping to compile. Yesod targets WAI, and + Happstack is in the process of converting over as well. It's also used by some + applications that skip the framework entirely, including the new Hoogle.

+ +
+

The Interface

+

+

The interface itself is very straight-forward: an application takes a request and returns a response. A response is an HTTP status, a list of headers and a response body. A request contains various information: the requested path, query string, request body, HTTP version, and so on.

+
+

Response Body

+

+

Haskell has a datatype known as a lazy bytestring. By utilizing laziness, you can create large values without exhausting memory. Using lazy I/O, you can do such tricks as having a value which represents the entire contents of a file, yet only occupies a small memory footprint. In theory, a lazy bytestring is the only representation necessary for a response body.

+

In practice, while lazy byte strings are wonderful for generating "pure" values, the + lazy I/O necessary to read a file introduces some non-determinism into our programs. + When serving thousands of small files a second, the limiting factor is not memory, but + file handles. Using lazy I/O, file handles may not be freed immediately, leading to + resource exhaustion. To deal with this, WAI uses conduits.

+ +

The data type relevant to us now is a source. A source produces a stream of + data, producing a single chunk at a time. In the case of WAI, the request body would be + a source passed to the application, and the response body would be a source returned + from the application.

+

There are two further optimizations: many systems provide a sendfile system call, which sends a file directly to a socket, bypassing a lot of the memory copying inherent in more general I/O system calls. Additionally, there is a datatype in Haskell called Builder which allows efficient copying of bytes into buffers.

+

The WAI response body therefore has three constructors: one for pure builders + (ResponseBuilder), one for a source of builders (ResponseSource) and one for files (ResponseFile).

+
+
+

Request Body

+

+

In order to avoid the need to load the entire request body into memory, we use + sources here as well. Since the purpose of these values are for reading (not writing), + we use ByteStrings in place of Builders. There is a record inside Request called + requestBody, with type BufferedSource IO + ByteString. We can use all of the standard conduit functions to interact + with this source.

+

The request body could in theory contain any type of data, but the most common are URL encoded and multipart form data. The wai-extra package contains built-in support for parsing these in a memory-efficient manner.

+
+
+
+

Hello World

+

+

To demonstrate the simplicity of WAI, let's look at a hello world example. In this example, we're going to use the OverloadedStrings language extension to avoid explicitly packing string values into bytestrings.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.HTTP.Types (status200)
+import Network.Wai.Handler.Warp (run)
+
+application _ = return $
+  responseLBS status200 [("Content-Type", "text/plain")] "Hello World"
+
+main = run 3000 application
+

Lines 2 through 4 perform our imports. Warp is provided by the warp package, and is the premiere WAI backend. WAI is also built on top of the http-types package, which provides a number of datatypes and convenience values, including status200.

+

First we define our application. Since we don't care about the specific request parameters, we ignore the argument to the function. For any request, we are returning a response with status code 200 ("OK"), and text/plain content type and a body containing the words "Hello World". Pretty straight-forward.

+
+
+

Middleware

+

+

In addition to allowing our applications to run on multiple backends without code changes, the WAI allows us another benefits: middleware. Middleware is essentially an application transformer, taking one application and returning another one.

+

Middleware components can be used to provide lots of services: cleaning up URLs, + authentication, caching, JSON-P requests. But + perhaps the most useful and most intuitive + middleware is gzip compression. The middleware + works very simply: it parses the request headers + to determine if a client supports compression, and + if so compresses the response body and adds the + appropriate response header.

+

The great thing about middlewares is that they are unobtrusive. Let's see how we would apply the gzip middleware to our hello world application.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.Wai.Handler.Warp (run)
+import Network.Wai.Middleware.Gzip (gzip, def)
+import Network.HTTP.Types (status200)
+
+application _ = return $ responseLBS status200 [("Content-Type", "text/plain")]
+                       "Hello World"
+
+main = run 3000 $ gzip def application
+

We added an import line to actually have access to the middleware, and then simply applied gzip to our application. You can also chain together multiple middlewares: a line such as gzip False $ jsonp $ othermiddleware $ myapplication is perfectly valid. One word of warning: the order the middleware is applied can be important. For example, jsonp needs to work on uncompressed data, so if you apply it after you apply gzip, you'll have trouble.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/widgets.html b/public/book-1.1/widgets.html new file mode 100644 index 00000000..bae4556a --- /dev/null +++ b/public/book-1.1/widgets.html @@ -0,0 +1,494 @@ + Widgets :: Yesod Web Framework Book- Version 1.1 +
+

Widgets

+ + +

+

+

One of the challenges in web development is that we have to coordinate three different client-side technologies: HTML, CSS and Javascript. Worse still, we have to place these components in different locations on the page: CSS in a style tag in the head, Javascript in a script tag in the head, and HTML in the body. And never mind if you want to put your CSS and Javascript in separate files!

+

In practice, this works out fairly nicely when building a single page, because we can + separate our structure (HTML), style (CSS) and logic (Javascript). But when we want to + build modular pieces of code that can be easily composed, it can be a headache to + coordinate all three pieces separately. Widgets are Yesod's solution to the problem. + They also help with the issue of including libraries, such as jQuery, one time only.

+

Our four template languages- Hamlet, Cassius, Lucius and Julius- provide the raw tools for constructing your output. Widgets provide the glue that allows them to work together seamlessly.

+
+

Synopsis

+

+
getRootR = defaultLayout $ do
+    setTitle "My Page Title"
+    toWidget [lucius| h1 { color: green; } |]
+    addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
+    toWidget [julius|
+$(function() {
+    $("h1").click(function(){ alert("You clicked on the heading!"); });
+});
+|]
+    toWidgetHead [hamlet| <meta name=keywords content="some sample keywords">|]
+    toWidget [hamlet| <h1>Here's one way of including content |]
+    [whamlet| <h2>Here's another |]
+    toWidgetBody [julius| alert("This is included in the body itself"); |]
+
+

This produces the following HTML (indentation added):

+
<!DOCTYPE html> 
+<html>
+    <head>
+        <title>My Page Title</title>
+        <style>h1 { color : green }</style>
+        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
+        <script>
+$(function() {
+    $("h1").click(function(){ alert("You clicked on the heading!"); });
+});
+</script>
+        <meta name="keywords" content="some sample keywords">
+    </head>
+    <body>
+        <h1>Here's one way of including content </h1>
+        <h2>Here's another </h2>
+        <script> alert("This is included in the body itself"); </script>
+    </body>
+</html>
+
+
+

What's in a Widget?

+

+

At a very superficial level, an HTML document is just a + bunch of nested tags. This is the approach most HTML generation + tools take: you simply define hierarchies of tags and are done with + it. But let's imagine that I want to write a component of a page + for displaying the navbar. I want this to be "plug and play": I + simply call the function at the right time, and the navbar is + inserted at the correct point in the hierarchy.

+

This is where our superficial HTML generation breaks down. Our navbar likely consists of + some CSS and JavaScript in addition to HTML. By the time we call the navbar function, we + have already rendered the <head> tag, so it is too late to add a + new <style> tag for our CSS declarations. Under normal + strategies, we would need to break up our navbar function into three parts: HTML, CSS + and JavaScript, and make sure that we always call all three pieces.

+

Widgets take a different approach. Instead of viewing an + HTML document as a monolithic tree of tags, widgets see a number of + distinct components in the page. In particular:

+
    +
  • +

    The title

    +
  • +
  • +

    External stylesheets

    +
  • +
  • +

    External Javascript

    +
  • +
  • +

    CSS declarations

    +
  • +
  • +

    Javascript code

    +
  • +
  • +

    Arbitrary <head> content

    +
  • +
  • +

    Arbitrary <body> content

    +
  • +
+

Different components have different semantics. For example, + there can only be one title, but there can be multiple external + scripts and stylesheets. However, those external scripts and + stylesheets should only be included once. Arbitrary head and body + content, on the other hand, has no limitation (someone may want to + have five lorem ipsum blocks after all).

+

The job of a widget is to hold onto these disparate components and apply proper logic + for combining different widgets together. This consists of things like taking the first + title set and ignoring others, filtering duplicates from the list of external scripts + and stylesheets, and concatenating head and body content.

+
+
+

Constructing Widgets

+

+

In order to use widgets, you'll obviously need to be able to get your hands on them. + The most common way will be via the ToWidget typeclass, and its + toWidget method. This allows you to convert your Shakespearean templates + directly to a Widget: Hamlet code will appear in the body, Julius scripts inside + a <script> tag in the head, and Cassius and Lucius in a + <style> tag.

+ +

But what if you want to add some <meta> tags, which need to + appear in the head? Or if you want some Javascript to appear in the body instead of the head? For + these purposes, Yesod provides two additional type classes: ToWidgetHead and + ToWidgetBody. These work exactly as they seem they should.

+

In addition, there are a number of other functions for creating specific kinds of Widgets:

+
+
setTitle
+

Turns some HTML into the page title.

+
+
addCassiusMedia, addLuciusMedia
+

Works the same as toWidget, but takes an additional parameter to indicate what kind of media + this applies to. Useful for creating print stylesheets, for instance.

+
+
addStylesheet
+

Adds a reference, via a <link> tag, to an external + stylesheet. Takes a type-safe URL.

+
+
addStylesheetRemote
+

Same as addStylesheet, but takes a normal URL. Useful for + referring to files hosted on a CDN, like Google's jQuery UI CSS files.

+
+
addScript
+

Adds a reference, via a <script> tag, to an external script. + Takes a type-safe URL.

+
+
addScriptRemote
+

Same as addScript, but takes a normal URL. Useful for referring to + files hosted on a CDN, like Google's jQuery.

+
+
+
+
+

Combining Widgets

+

+

The whole idea of widgets is to increase composability. You can take these individual + pieces of HTML, CSS and Javascript, combine them together into something more complicated, and + then combine these larger entities into complete pages. This all works naturally through the + Monad instance of Widget, meaning you can use do-notation to + compose pieces together.

+
+

Combining Widgets

+
myWidget1 = do
+    toWidget [hamlet|<h1>My Title|]
+    toWidget [lucius|h1 { color: green } |]
+
+myWidget2 = do
+    setTitle "My Page Title"
+    addScriptRemote "http://www.example.com/script.js"
+
+myWidget = do
+    myWidget1
+    myWidget2
+
+-- or, if you want
+myWidget' = myWidget1 >> myWidget2
+
+ +
+
+

Generate IDs

+

+

If we're really going for true code reuse here, we're eventually going to run into name + conflicts. Let's say that there are two helper libraries that both use the class name "foo" to + affect styling. We want to avoid such a possibility. Therefore, we have the + newIdent function. This function automatically generates a word that is unique + for this handler.

+
+

Using newIdent

+
getRootR = defaultLayout $ do
+    headerClass <- lift newIdent
+    toWidget [hamlet|<h1 .#{headerClass}>My Header|]
+    toWidget [lucius| .#{headerClass} { color: green; } |]
+
+
+ +
+
+

whamlet

+

+

Let's say you've got a fairly standard Hamlet template, that embeds another Hamlet template to + represent the footer:

+
page = [hamlet|
+<p>This is my page. I hope you enjoyed it.
+^{footer}
+|]
+
+footer = [hamlet|
+<footer>
+    <p>That's all folks!
+|]
+

That works fine if the footer is plain old HTML, but what if we want to add some style? Well, + we can easily spice up the footer by turning it into a Widget:

+
footer = do
+    toWidget [lucius| footer { font-weight: bold; text-align: center } |]
+    toWidget [hamlet|
+<footer>
+    <p>That's all folks!
+|]
+

But now we've got a problem: a Hamlet template can only embed another Hamlet template; + it knows nothing about a Widget. This is where whamlet comes in. It takes + exactly the same syntax as normal Hamlet, and variable (#{...}) and URL (@{...}) interpolation + are unchanged. But embedding (^{...}) takes a Widget, and the final result is + a Widget. To use it, we can just do:

+
page = [whamlet|
+<p>This is my page. I hope you enjoyed it.
+^{footer}
+|]
+

There is also whamletFile, if you would prefer to keep your template + in a separate file.

+ +
+

Types

+

+

You may have noticed that I've been avoiding type signatures so far. That's because + there's a little bit of a complication involved here. At the most basic level, all you need to + know is that there's a type synonym called Widget which you will almost always + use. The technical details follow, but don't worry if it's a little hazy.

+

There isn't actually a Widget type defined in the Yesod libraries, since the + exact meaning of it changes between sites. Instead, we have a more general type GWidget + sub master a. The first two parameters give the sub and master foundation types, + respectively. The final parameter is the contained value, just like any Monad + has.

+

So what's the deal with that sub/master stuff? Well, when you're writing some reusable code, + such as a CRUD application, you can write it as a subsite that can be embedded within any other + Yesod application. In such a case, we need to keep track of information for both the sub and + master sites. The simplest example is for the type-safe URLs: Yesod needs to know how to take a + route for your CRUD subsite and turn it into a route for the master site so that it can be + properly rendered.

+

However, that sub/master distinction only ever matters when you're interacting with subsites. + When you're writing your standard response code, you're dealing with just your application, and + so the sub and master sites will be the same. Since this is the most common case, the scaffolded + site declares a type synonym to help you out. Let's say your foundation type is MyCoolApp, it + will define type Widget = GWidget MyCoolApp MyCoolApp (). Therefore, we can get + some very user-friendly type signatures on our widgets:

+
footer :: Widget
+footer = do
+    toWidget [lucius| footer { font-weight: bold; text-align: center } |]
+    toWidget [hamlet|
+<footer>
+    <p>That's all folks!
+|]
+
+page :: Widget
+page = [whamlet|
+<p>This is my page. I hope you enjoyed it.
+^{footer}
+|]
+

If you've been paying close attention, you might be confused. We used lift on Widget in the ID generation example above, but + GWidget isn't actually a monad transformer. What's going on here? + Well, in older versions of Yesod, it was a transformer around the Handler type. Unfortunately, this led to difficult-to-parse error messages. + As a result, GWidget is now a newtype + wrapper that hides away its monad-transformer essence. But we still want to be able to lift functions from the inner Handler monad.

+

To solve this, Yesod provides an alternate, more general lift function that + works for both standard MonadTrans instances, and special + newtype wrappers like GWidget. As a result, you can pretend + like GWidget is a standard transformer, while still getting to keep your nice + error message.

+

One last point: just like we have the breakdown between Widget and + GWidget, we have a similar breakdown between Handler and + GHandler.

+
+
+
+

Using Widgets

+

+

It's all well and good that we have these beautiful Widget datatypes, but how exactly + do we turn them into something the user can interact with? The most commonly used + function is defaultLayout, which essentially has the type signature + Widget -> Handler RepHtml. (I say "essentially" because + of the whole GHandler issue.) RepHtml is a datatype + containing some raw HTML output ready to be sent over the wire.

+

+ defaultLayout is actually a typeclass method, which can be overridden + for each application. This is how Yesod apps are themed. So we're still left with the + question: when we're inside defaultLayout, how do we unwrap a + Widget? The answer is widgetToPageContent. Let's + look at some (simplified) types:

+
widgetToPageContent :: Widget -> Handler (PageContent url)
+data PageContent url = PageContent
+    { pageTitle :: Html
+    , pageHead :: HtmlUrl url
+    , pageBody :: HtmlUrl url
+    }
+

This is getting closer to what we need. We now have direct access to the HTML making up + the head and body, as well as the title. At this point, we can use Hamlet to combine + them all together into a single document, along with our site layout, and we use + hamletToRepHtml to render that Hamlet result into actual HTML + that's ready to be shown to the user. The next figure demonstrates this process.

+
+

Using widgetToPageContent

+
myLayout :: GWidget s MyApp () -> GHandler s MyApp RepHtml
+myLayout widget = do
+    pc <- widgetToPageContent widget
+    hamletToRepHtml [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        <meta charset=utf-8>
+        <style>body { font-family: verdana }
+        ^{pageHead pc}
+    <body>
+        <article>
+            ^{pageBody pc}
+|]
+
+instance Yesod MyApp where
+    defaultLayout = myLayout
+
+
+ +

This is all well and good, but there's one thing that bothers me: that + style tag. There are a few problems with it:

+
    +
  • +

    Unlike Lucius or Cassius, it doesn't get compile-time checked for correctness.

    +
  • +
  • +

    Granted that the current example is very simple, but in something more complicated we could + get into character escaping issues.

    +
  • +
  • +

    We'll now have two style tags instead of one: the one produced by + myLayout, and the one generated in the + pageHead based on the styles set in the widget.

    +
  • +
+

We have one more trick in our bag to address this: we apply some last-minute + adjustments to the widget itself before calling widgetToPageContent. It's actually very easy to do: we just use + do-notation again, as in Last-Minute Widget Adjustment.

+
+

Last-Minute Widget Adjustment

+
myLayout :: GWidget s MyApp () -> GHandler s MyApp RepHtml
+myLayout widget = do
+    pc <- widgetToPageContent $ do
+        widget
+        toWidget [lucius| body { font-family: verdana } |]
+    hamletToRepHtml [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        <meta charset=utf-8>
+        ^{pageHead pc}
+    <body>
+        <article>
+            ^{pageBody pc}
+|]
+
+
+
+
+

Summary

+

+

The basic building block of each page is a widget. Individual snippets of HTML, CSS, and + Javascript can be turned into widgets via the polymorphic toWidget function. + Using do-notation, you can combine these individual widgets into larger widgets, eventually + containing all the content of your page.

+

Unwrapping these widgets is usually performed within the defaultLayout function, which can be + used to apply a unified look-and-feel to all your pages.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/wiki-chat-example.html b/public/book-1.1/wiki-chat-example.html new file mode 100644 index 00000000..dc1ac586 --- /dev/null +++ b/public/book-1.1/wiki-chat-example.html @@ -0,0 +1,423 @@ + Wiki: markdown, chat subsite, event source :: Yesod Web Framework Book- Version 1.1 +
+

Wiki: markdown, chat subsite, event source

+ + +

+

+

This example will tie together a few different ideas. We'll start with a chat subsite, which + allows us to embed a chat widget on any page. We'll use the HTML 5 event source API to handle + sending events from the server to the client.

+
-- @Chat.hs
+{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, FlexibleInstances, MultiParamTypeClasses,
+             FlexibleContexts
+  #-}
+-- | This modules defines a subsite that allows you to insert a chat box on
+-- any page of your site. It uses eventsource for sending the messages from
+-- the server to the browser.
+module Chat where
+
+import Yesod
+import Control.Concurrent.Chan (Chan, dupChan, writeChan)
+import Data.Text (Text)
+import Network.Wai.EventSource (ServerEvent (..), eventSourceAppChan)
+import Language.Haskell.TH.Syntax (Type (VarT), Pred (ClassP), mkName)
+import Blaze.ByteString.Builder.Char.Utf8 (fromText)
+import Data.Monoid (mappend)
+
+-- | Our subsite foundation. We keep a channel of events that all connections
+-- will share.
+data Chat = Chat (Chan ServerEvent)
+
+-- | We need to know how to check if a user is logged in and how to get
+-- his/her username (for printing messages).
+class (Yesod master, RenderMessage master FormMessage)
+        => YesodChat master where
+    getUserName :: GHandler sub master Text
+    isLoggedIn :: GHandler sub master Bool
+
+-- Now we set up our subsite. The first argument is the subsite, very similar
+-- to how we've used mkYesod in the past. The second argument is specific to
+-- subsites. What it means here is "the master site must be an instance of
+-- YesodChat".
+--
+-- We define two routes: a route for sending messages from the client to the
+-- server, and one for opening up the event stream to receive messages from
+-- the server.
+mkYesodSub "Chat"
+    [ ClassP ''YesodChat [VarT $ mkName "master"]
+    ] [parseRoutes|
+/send SendR POST
+/recv ReceiveR GET
+|]
+
+-- | Get a message from the user and send it to all listeners.
+postSendR :: YesodChat master => GHandler Chat master ()
+postSendR = do
+    from <- getUserName
+
+    -- Note that we're using GET parameters for simplicity of the Ajax code.
+    -- This could easily be switched to POST. Nonetheless, our overall
+    -- approach is still RESTful since this route can only be accessed via a
+    -- POST request.
+    body <- runInputGet $ ireq textField "message"
+
+    -- Get the channel
+    Chat chan <- getYesodSub
+
+    -- Send an event to all listeners with the user's name and message.
+    liftIO $ writeChan chan $ ServerEvent Nothing Nothing $ return $
+        fromText from `mappend` fromText ": " `mappend` fromText body
+
+-- | Send an eventstream response with all messages streamed in.
+getReceiveR :: GHandler Chat master ()
+getReceiveR = do
+    -- First we get the main channel
+    Chat chan0 <- getYesodSub
+
+    -- We duplicated the channel, which allows us to create broadcast
+    -- channels.
+    chan <- liftIO $ dupChan chan0
+
+    -- Now we use the event source API. eventSourceAppChan takes two parameters:
+    -- the channel of events to read from, and the WAI request. It returns a
+    -- WAI response, which we can return with sendWaiResponse.
+    req <- waiRequest
+    res <- lift $ eventSourceAppChan chan req
+    sendWaiResponse res
+
+-- | Provide a widget that the master site can embed on any page.
+chatWidget :: YesodChat master
+           => (Route Chat -> Route master)
+           -> GWidget sub master ()
+-- This toMaster argument tells us how to convert a Route Chat into a master
+-- route. You might think this is redundant information, but taking this
+-- approach means we can have multiple chat subsites in a single site.
+chatWidget toMaster = do
+    -- Get some unique identifiers to help in creating our HTML/CSS. Remember,
+    -- we have no idea what the master site's HTML will look like, so we
+    -- should not assume we can make up identifiers that won't be reused.
+    -- Also, it's possible that multiple chatWidgets could be embedded in the
+    -- same page.
+    chat <- lift newIdent   -- the containing div
+    output <- lift newIdent -- the box containing the messages
+    input <- lift newIdent  -- input field from the user
+
+    ili <- lift isLoggedIn  -- check if we're already logged in
+    if ili
+        then do
+            -- Logged in: show the widget
+            [whamlet|
+<div ##{chat}>
+    <h2>Chat
+    <div ##{output}>
+    <input ##{input} type=text placeholder="Enter Message">
+|]
+            -- Just some CSS
+            toWidget [lucius|
+##{chat} {
+    position: absolute;
+    top: 2em;
+    right: 2em;
+}
+##{output} {
+    width: 200px;
+    height: 300px;
+    border: 1px solid #999;
+    overflow: auto;
+}
+|]
+            -- And now that Javascript
+            toWidgetBody [julius|
+// Set up the receiving end
+var output = document.getElementById("#{output}");
+var src = new EventSource("@{toMaster ReceiveR}");
+src.onmessage = function(msg) {
+    // This function will be called for each new message.
+    var p = document.createElement("p");
+    p.appendChild(document.createTextNode(msg.data));
+    output.appendChild(p);
+
+    // And now scroll down within the output div so the most recent message
+    // is displayed.
+    output.scrollTop = output.scrollHeight;
+};
+
+// Set up the sending end: send a message via Ajax whenever the user hits
+// enter.
+var input = document.getElementById("#{input}");
+input.onkeyup = function(event) {
+    var keycode = (event.keyCode ? event.keyCode : event.which);
+    if (keycode == '13') {
+        var xhr = new XMLHttpRequest();
+        var val = input.value;
+        input.value = "";
+        var params = "?message=" + encodeURI(val);
+        xhr.open("POST", "@{toMaster SendR}" + params);
+        xhr.send(null);
+    }
+}
+|]
+        else do
+            -- User isn't logged in, give a not-logged-in message.
+            master <- lift getYesod
+            [whamlet|
+<p>
+    You must be #
+    $maybe ar <- authRoute master
+        <a href=@{ar}>logged in
+    $nothing
+        logged in
+    \ to chat.
+|]
+

This module stands on its own, and can be used in any application. Next we'll provide such a + driver application: a wiki. Our wiki will have a hard-coded homepage, and then a wiki section of + the site. We'll be using multiple dynamic pieces to allow an arbitrary hierarchy of + pages within the Wiki.

+

For storage, we'll just use a mutable reference to a Map. For a production + application, this should be replaced with a proper database. The content will be stored and + served as Markdown. yesod-auth's dummy plugin will provide us with (fake) + authentication.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, FlexibleInstances, MultiParamTypeClasses,
+             FlexibleContexts
+  #-}
+import Yesod
+import Yesod.Auth
+import Yesod.Auth.Dummy (authDummy)
+import Chat
+import Control.Concurrent.Chan (Chan, newChan)
+import Network.Wai.Handler.Warp (run)
+import Data.Text (Text)
+import qualified Data.Text.Lazy as TL
+import qualified Data.IORef as I
+import qualified Data.Map as Map
+import Text.Markdown (markdown, def)
+
+-- | Our foundation type has both the chat subsite and a mutable reference to
+-- a map of all our wiki contents. Note that the key is a list of Texts, since
+-- a wiki can have an arbitrary hierarchy.
+--
+-- In a real application, we would want to store this information in a
+-- database of some sort.
+data Wiki = Wiki
+    { getChat :: Chat
+    , wikiContent :: I.IORef (Map.Map [Text] Text)
+    }
+
+-- Set up our routes as usual.
+mkYesod "Wiki" [parseRoutes|
+/ RootR GET                 -- the homepage
+/wiki/*Texts WikiR GET POST -- note the multipiece for the wiki hierarchy
+/chat ChatR Chat getChat    -- the chat subsite
+/auth AuthR Auth getAuth    -- the auth subsite
+|]
+
+instance Yesod Wiki where
+    authRoute _ = Just $ AuthR LoginR -- get a working login link
+
+    -- Our custom defaultLayout will add the chat widget to every page.
+    -- We'll also add login and logout links to the top.
+    defaultLayout widget = do
+        pc <- widgetToPageContent $ widget >> chatWidget ChatR
+        mmsg <- getMessage
+        hamletToRepHtml [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <div .message>#{msg}
+        <nav>
+            <a href=@{AuthR LoginR}>Login
+            \ | #
+            <a href=@{AuthR LogoutR}>Logout
+        ^{pageBody pc}
+|]
+
+-- Fairly standard YesodAuth instance. We'll use the dummy plugin so that you
+-- can create any name you want, and store the login name as the AuthId.
+instance YesodAuth Wiki where
+    type AuthId Wiki = Text
+    authPlugins _ = [authDummy]
+    loginDest _ = RootR
+    logoutDest _ = RootR
+    getAuthId = return . Just . credsIdent
+    authHttpManager = error "authHttpManager" -- not used by authDummy
+
+-- Just implement authentication based on our yesod-auth usage.
+instance YesodChat Wiki where
+    getUserName = requireAuthId
+    isLoggedIn = do
+        ma <- maybeAuthId
+        return $ maybe False (const True) ma
+
+instance RenderMessage Wiki FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Nothing special here, just giving a link to the root of the wiki.
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<p>Welcome to the Wiki!
+<p>
+    <a href=@{wikiRoot}>Wiki root
+|]
+  where
+    wikiRoot = WikiR []
+
+-- A form for getting wiki content
+wikiForm mtext = renderDivs $ areq textareaField "Page body" mtext
+
+-- Show a wiki page and an edit form
+getWikiR :: [Text] -> Handler RepHtml
+getWikiR page = do
+    -- Get the reference to the contents map
+    icontent <- fmap wikiContent getYesod
+
+    -- And read the map from inside the reference
+    content <- liftIO $ I.readIORef icontent
+
+    -- Lookup the contents of the current page, if available
+    let mtext = Map.lookup page content
+
+    -- Generate a form with the current contents as the default value.
+    -- Note that we use the Textarea wrapper to get a <textarea>.
+    (form, _) <- generateFormPost $ wikiForm $ fmap Textarea mtext
+    defaultLayout $ do
+        case mtext of
+            -- We're treating the input as markdown. The markdown package
+            -- automatically handles XSS protection for us.
+            Just text -> toWidget $ markdown def $ TL.fromStrict text
+            Nothing -> [whamlet|<p>Page does not yet exist|]
+        [whamlet|
+<h2>Edit page
+<form method=post>
+    ^{form}
+    <div>
+        <input type=submit>
+|]
+
+-- Get a submitted wiki page and updated the contents.
+postWikiR :: [Text] -> Handler RepHtml
+postWikiR page = do
+    icontent <- fmap wikiContent getYesod
+    content <- liftIO $ I.readIORef icontent
+    let mtext = Map.lookup page content
+    ((res, form), _) <- runFormPost $ wikiForm $ fmap Textarea mtext
+    case res of
+        FormSuccess (Textarea t) -> do
+            liftIO $ I.atomicModifyIORef icontent $
+                \m -> (Map.insert page t m, ())
+            setMessage "Page updated"
+            redirect $ WikiR page
+        _ -> defaultLayout [whamlet|
+<form method=post>
+    ^{form}
+    <div>
+        <input type=submit>
+|]
+
+main :: IO ()
+main = do
+    -- Create our server event channel
+    chan <- newChan
+
+    -- Initially have a blank database of wiki pages
+    icontent <- I.newIORef Map.empty
+
+    -- Run our app
+    warpDebug 3000 $ Wiki (Chat chan) icontent
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/xml.html b/public/book-1.1/xml.html new file mode 100644 index 00000000..961a3b00 --- /dev/null +++ b/public/book-1.1/xml.html @@ -0,0 +1,782 @@ + xml-conduit :: Yesod Web Framework Book- Version 1.1 +
+

xml-conduit

+ + +

+

+

Many developers cringe at the thought of dealing with XML files. XML has the reputation of + having a complicated data model, with obfuscated libraries and huge layers of complexity sitting + between you and your goal. I'd like to posit that a lot of that pain is actually a language and + library issue, not inherent to XML.

+

Once again, Haskell's type system allows us to easily break down the problem to its most basic + form. The xml-types package neatly deconstructs the XML data model (both a + streaming and DOM-based approach) into some simple ADTs. Haskell's standard immutable data + structures make it easier to apply transforms to documents, and a simple set of functions makes + parsing and rendering a breeze.

+

We're going to be covering the xml-conduit package. Under + the surface, this package uses a lot of the approaches Yesod in general does for high + performance: blaze-builder, text, + conduit and attoparsec. But from a + user perspective, it provides everything from the simplest APIs + (readFile/writeFile) through full control of XML event + streams.

+

In addition to xml-conduit, there are a few related packages that + come into play, like xml-hamlet and xml2html. We'll cover both how to use all these packages, and when they should be + used.

+
+

Synopsis

+

+
+

Input XML file

+
<document title="My Title">
+    <para>This is a paragraph. It has <em>emphasized</em> and <strong>strong</strong> words.</para>
+    <image href="myimage.png"/>
+</document>
+
+
+

Haskell code

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile, writeFile)
+import Text.XML
+import Text.Hamlet.XML
+import qualified Data.Map as M
+
+main :: IO ()
+main = do
+    -- readFile will throw any parse errors as runtime exceptions
+    -- def uses the default settings
+    Document prologue root epilogue <- readFile def "input.xml"
+
+    -- root is the root element of the document, let's modify it
+    let root' = transform root
+
+    -- And now we write out. Let's indent our output
+    writeFile def
+        { rsPretty = True
+        } "output.html" $ Document prologue root' epilogue
+
+-- We'll turn out <document> into an XHTML document
+transform :: Element -> Element
+transform (Element _name attrs children) = Element "html" M.empty [xml|
+<head>
+    <title>
+        $maybe title <- M.lookup "title" attrs
+            \#{title}
+        $nothing
+            Untitled Document
+<body>
+    $forall child <- children
+        ^{goNode child}
+|]
+
+goNode :: Node -> [Node]
+goNode (NodeElement e) = [NodeElement $ goElem e]
+goNode (NodeContent t) = [NodeContent t]
+goNode (NodeComment _) = [] -- hide comments
+goNode (NodeInstruction _) = [] -- and hide processing instructions too
+
+-- convert each source element to its XHTML equivalent
+goElem :: Element -> Element
+goElem (Element "para" attrs children) =
+    Element "p" attrs $ concatMap goNode children
+goElem (Element "em" attrs children) =
+    Element "i" attrs $ concatMap goNode children
+goElem (Element "strong" attrs children) =
+    Element "b" attrs $ concatMap goNode children
+goElem (Element "image" attrs _children) =
+    Element "img" (fixAttr attrs) [] -- images can't have children
+  where
+    fixAttr mattrs
+        | "href" `M.member` mattrs  = M.delete "href" $ M.insert "src" (mattrs M.! "href") mattrs
+        | otherwise                 = mattrs
+goElem (Element name attrs children) =
+    -- don't know what to do, just pass it through...
+    Element name attrs $ concatMap goNode children
+
+
+

Output XHTML

+
<?xml version="1.0" encoding="UTF-8"?>
+<html>
+    <head>
+        <title>
+            My Title
+        </title>
+    </head>
+    <body>
+        <p>
+            This is a paragraph. It has 
+            <i>
+                emphasized
+            </i>
+            and 
+            <b>
+                strong
+            </b>
+            words.
+        </p>
+        <img src="myimage.png"/>
+    </body>
+</html>
+
+
+
+

Types

+

+

Let's take a bottom-up approach to analyzing types. This section will also serve as a primer on + the XML data model itself, so don't worry if you're not completely familiar with it.

+

I think the first place where Haskell really shows its strength is with the + Name datatype. Many languages (like Java) struggle + with properly expressing names. The issue is that there are in fact three components to a name: + its local name, its namespace (optional), and its prefix (also optional). Let's look at some XML + to explain:

+
<no-namespace/>
+<no-prefix xmlns="first-namespace" first-attr="value1"/>
+<foo:with-prefix xmlns:foo="second-namespace" foo:second-attr="value2"/>
+

The first tag has a local name of no-namespace, and no + namespace or prefix. The second tag (local name: no-prefix) also has no prefix, but it does have a namespace (first-namespace). first-attr, however, does not + inherit that namespace: attribute namespaces must always be explicitly set with a prefix.

+ +

The third tag has a local name of with-prefix, a prefix of + foo and a namespace of second-namespace. + Its attribute has a second-attr local name and the same prefix and namespace. + The xmlns and xmlns:foo attributes are part of the namespace + specification, and are not considered attributes of their respective elements.

+

So let's review what we need from a name: every name has a local name, and it can optionally + have a prefix and namespace. Seems like a simple fit for a record type:

+
data Name = Name
+    { nameLocalName :: Text
+    , nameNamespace :: Maybe Text
+    , namePrefix :: Maybe Text
+    }
+

According the the XML namespace standard, two names are considered equivalent if they + have the same localname and namespace. In other words, the prefix is not important. Therefore, + xml-types defines Eq and Ord instances that + ignore the prefix.

+

The last class instance worth mentioning is IsString. It would be + very tedious to have to manually type out Name "p" Nothing Nothing + every time we want a paragraph. If you turn on OverloadedStrings, + "p" will resolve to that all by itself! In addition, the + IsString instance recognizes something called Clark notation, which allows you + to prefix the namespace surrounded in curly brackets. In other words:

+
"{namespace}element" == Name "element" (Just "namespace") Nothing
+"element" == Name "element" Nothing Nothing
+
+

The Four Types of Nodes

+

+

XML documents are a tree of nested nodes. There are in fact four different types of nodes + allowed: elements, content (i.e., text), comments, and processing instructions.

+ +

Since processing instructions have two pieces of text associated with them (the target and the + data), we have a simple data type:

+
data Instruction = Instruction
+    { instructionTarget :: Text
+    , instructionData :: Text
+    }
+

Comments have no special datatype, since they are just text. But content is an + interesting one: it could contain either plain text or unresolved entities (e.g., + &copyright-statement;). xml-types keeps those + unresolved entities in all the data types in order to completely match the spec. However, in + practice, it can be very tedious to program against those data types. And in most use cases, an + unresolved entity is going to end up as an error anyway.

+

So the Text.XML module defines its own set + of datatypes for nodes, elements and documents that removes all unresolved entities. If you need + to deal with unresolved entities instead, you should use the Text.XML.Unresolved module. From now on, we'll be focusing only on the + Text.XML data types, though they are almost identical to the + xml-types versions.

+

Anyway, after that detour: content is just a piece of text, and therefore it too does + not have a special datatype. The last node type is an element, which contains three pieces of + information: a name, a list of attributes and a list of children nodes. An attribute has two + pieces of information: a name and a value. (In xml-types, this value could + contain unresolved entities as well.) So our Element is defined as:

+
data Element = Element
+    { elementName :: Name
+    , elementAttributes :: [(Name, Text)]
+    , elementNodes :: [Node]
+    }
+

Which of course begs the question: what does a Node look like? This + is where Haskell really shines: its sum types model the XML data model perfectly.

+
data Node
+    = NodeElement Element
+    | NodeInstruction Instruction
+    | NodeContent Text
+    | NodeComment Text
+
+
+

Documents

+

+

So now we have elements and nodes, but what about an entire document? Let's just lay out the + datatypes:

+
data Document = Document
+    { documentPrologue :: Prologue
+    , documentRoot :: Element
+    , documentEpilogue :: [Miscellaneous]
+    }
+
+data Prologue = Prologue
+    { prologueBefore :: [Miscellaneous]
+    , prologueDoctype :: Maybe Doctype
+    , prologueAfter :: [Miscellaneous]
+    }
+
+data Miscellaneous
+    = MiscInstruction Instruction
+    | MiscComment Text
+
+data Doctype = Doctype
+    { doctypeName :: Text
+    , doctypeID :: Maybe ExternalID
+    }
+
+data ExternalID
+    = SystemID Text
+    | PublicID Text Text
+

The XML spec says that a document has a single root element + (documentRoot). It also has an optional doctype statement. Before and after + both the doctype and the root element, you are allowed to have comments and processing + instructions. (You can also have whitespace, but that is ignored in the parsing.)

+

So what's up with the doctype? Well, it specifies the root element of the document, and then + optional public and system identifiers. These are used to refer to DTD files, which give more + information about the file (e.g., validation rules, default attributes, entity resolution). Let's + see some examples:

+
<!DOCTYPE root> <!-- no external identifier -->
+<!DOCTYPE root SYSTEM "root.dtd"> <!-- a system identifier -->
+<!DOCTYPE root PUBLIC "My Root Public Identifier" "root.dtd"> <!-- public identifiers have a system ID as well -->
+

And that, my friends, is the entire XML data model. For many parsing purposes, you'll + be able to simply ignore the entire Document datatype and go immediately to the + documentRoot.

+
+
+

Events

+

+

In addition to the document API, xml-types defines an Event datatype. This can be used for constructing + streaming tools, which can be much more memory efficient for certain kinds of processing (eg, + adding an extra attribute to all elements). We will not be covering the streaming API currently, + though it should look very familiar after analyzing the document API.

+ +
+
+
+

Text.XML

+

+

The recommended entry point to xml-conduit is the Text.XML module. This module exports all of the datatypes you'll need to + manipulate XML in a DOM fashion, as well as a number of different approaches for parsing and + rendering XML content. Let's start with the simple + ones:

readFile  :: ParseSettings  -> FilePath -> IO Document
+writeFile :: RenderSettings -> FilePath -> Document -> IO ()
This + introduces the ParseSettings and RenderSettings datatypes. You can use these to modify the behavior of the parser and + renderer, such as adding character entities and turning on pretty (i.e., indented) output. Both + these types are instances of the Default + typeclass, so you can simply use def when these need to be supplied. + That is how we will supply these values through the rest of the chapter; please see the API docs + for more information.

+

It's worth pointing out that in addition to the file-based API, there is also a text- and + bytestring-based API. The bytestring-powered functions all perform intelligent encoding + detections, and support UTF-8, UTF-16 and UTF-32, in either big or little endian, with and + without a Byte-Order Marker (BOM). All output is generated in UTF-8.

+

For complex data lookups, we recommend using the higher-level cursors API. The + standard Text.XML API not only forms the basis for that higher level, but is + also a great API for simple XML transformations and for XML generation. See the synopsis for an + example.

+
+

A note about file paths

+

+

In the type signature above, we have a type FilePath. However, this isn't + Prelude.FilePath + . The standard Prelude defines a type + synonym type FilePath = [Char]. Unfortunately, there are many limitations to + using such an approach, including confusion of filename character encodings and differences in + path separators.

+

Instead, xml-conduit uses the system-filepath package, + which defines an abstract FilePath type. I've personally found this to be a much + nicer approach to work with. The package is fairly easy to follow, so I won't go into details + here. But I do want to give a few quick explanations of how to use it:

+
    +
  • +

    Since a FilePath is an instance of IsString, you can type + in regular strings and they will be treated properly, as long as the + OverloadedStrings extension is enabled. (I highly recommend enabling it + anyway, as it makes dealing with Text values much more pleasant.)

    +
  • +
  • +

    If you need to explicitly convert to or from Prelude's + FilePath, you should use the + encodeString and + decodeString, respectively. This + takes into account file path encodings.

    +
  • +
  • +

    Instead of manually splicing together directory names and file names with extensions, use the + operators in the Filesystem.Path.CurrentOS module, e.g. myfolder </> + filename <.> extension.

    +
  • +
+
+
+
+

Cursor

+

+

Suppose you want to pull the title out of an XHTML document. You could do so with the + Text.XML interface we just described, using standard pattern matching on the + children of elements. But that would get very tedious, very quickly. Probably the gold standard + for these kinds of lookups is XPath, where you would be able to write /html/head/title. And that's exactly what inspired the design of the Text.XML.Cursor combinators.

+

A cursor is an XML node that knows its location in the tree; it's able to traverse + upwards, sideways, and downwards. (Under the surface, this is achieved by tying + the knot.) There are two functions available for creating cursors from + Text.XML types: fromDocument and + fromNode.

+

We also have the concept of an Axis, defined as type Axis = Cursor -> [Cursor]. It's easiest to get started by looking at + example axes: child returns zero or more cursors that are the child of the current one, parent + returns the single parent cursor of the input, or an empty list if the input is the root element, + and so on.

+

In addition, there are some axes that take predicates. element is a commonly + used function that filters down to only elements which match the given name. For example, + element "title" will return the input element if its name is "title", or an + empty list otherwise.

+

Another common function which isn't quite an axis is content :: Cursor -> + [Text]. For all content nodes, it returns the contained text; otherwise, it returns an + empty list.

+

And thanks to the monad instance for lists, it's easy to string all of these + together. For example, to do our title lookup, we would write the following program:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+            child cursor >>= element "head" >>= child
+                         >>= element "title" >>= descendant >>= content
+

What this says is:

+
    +
  1. +

    Get me all the child nodes of the root element

    +
  2. +
  3. +

    Filter down to only the elements named "head"

    +
  4. +
  5. +

    Get all the children of all those head elements

    +
  6. +
  7. +

    Filter down to only the elements named "title"

    +
  8. +
  9. +

    Get all the descendants of all those title elements. (A descendant is a child, or a + descendant of a child. Yes, that was a recursive definition.)

    +
  10. +
  11. +

    Get only the text nodes.

    +
  12. +
+

So for the input document:

+
<html>
+    <head>
+        <title>My <b>Title</b></title>
+    </head>
+    <body>
+        <p>Foo bar baz</p>
+    </body>
+</html>
+

We end up with the output My Title. This is all well and good, but it's much + more verbose than the XPath solution. To combat this verbosity, Aristid Breitkreuz added a set of + operators to the Cursor module to handle many common cases. So we can rewrite our example as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $/ element "head" &/ element "title" &// content
+

+ $/ says to apply the axis on the right to the children of + the cursor on the left. &/ is almost identical, but is instead + used to combine two axes together. This is a general rule in Text.XML.Cursor: operators beginning with $ directly apply an axis, while & will + combine two together. &// is used for applying an axis to all + descendants.

+

Let's go for a more complex, if more contrived, example. We have a document that looks + like:

+
<html>
+    <head>
+        <title>Headings</title>
+    </head>
+    <body>
+        <hgroup>
+            <h1>Heading 1 foo</h1>
+            <h2 class="foo">Heading 2 foo</h2>
+        </hgroup>
+        <hgroup>
+            <h1>Heading 1 bar</h1>
+            <h2 class="bar">Heading 2 bar</h2>
+        </hgroup>
+    </body>
+</html>
+

We want to get the content of all the h1 tags which precede an + h2 tag with a class attribute of "bar". To perform this + convoluted lookup, we can write:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test2.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $// element "h2"
+               >=> attributeIs "class" "bar"
+               >=> precedingSibling
+               >=> element "h1"
+               &// content
+

Let's step through that. First we get all h2 elements in the document. + ($// gets all descendants of the root element.) Then we filter out only those + with class=bar. That >=> operator is actually + the standard operator from Control.Monad; yet another advantage + of the monad instance of lists. precedingSibling finds all nodes that come + before our node and share the same parent. (There is also a preceding axis which takes all elements earlier in the tree.) We then take just the + h1 elements, and then grab their content.

+ +

While the cursor API isn't quite as succinct as XPath, it has the advantages of being standard + Haskell code, and of type safety.

+
+
+

xml-hamlet

+

+

Thanks to the simplicity of Haskell's data type system, creating + XML content with the Text.XML API is easy, if a bit verbose. The + following code:

+
{-# LANGUAGE OverloadedStrings #-}
+import Text.XML
+import Prelude hiding (writeFile)
+import Data.Map (empty)
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty
+        [ NodeElement $ Element "head" empty
+            [ NodeElement $ Element "title" empty
+                [ NodeContent "My "
+                , NodeElement $ Element "b" empty
+                    [ NodeContent "Title"
+                    ]
+                ]
+            ]
+        , NodeElement $ Element "body" empty
+            [ NodeElement $ Element "p" empty
+                [ NodeContent "foo bar baz"
+                ]
+            ]
+        ]
+

produces

+
<?xml version="1.0" encoding="UTF-8"?>
+<html><head><title>My <b>Title</b></title></head><body><p>foo bar baz</p></body></html>
+

This is leaps and bounds easier than having to deal with an imperative, mutable-value-based API + (cough, Java, cough), but it's far from pleasant, and obscures what we're really trying to + achieve. To simplify things, we have the xml-hamlet package, which using + Quasi-Quotation to allow you to type in your XML in a natural syntax. For example, the above + could be rewritten as:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+import Data.Map (empty)
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>
+        My #
+        <b>Title
+<body>
+    <p>foo bar baz
+|]
+

Let's make a few points:

+
    +
  • +

    The syntax is almost identical to normal Hamlet, except URL-interpolation (@{...}) has been + removed. As such:

      +
    • +

      No close tags.

      +
    • +
    • +

      Whitespace-sensitive.

      +
    • +
    • +

      If you want to have whitespace at the end of a line, use a # at the end. At the beginning, + use a backslash.

      +
    • +
    +

    +
  • +
  • +

    An xml interpolation will return a list of Nodes. So you still need to wrap up the output in all the normal + Document and root Element constructs.

    +
  • +
  • +

    There is no support for the special .class and + #id attribute forms.

    +
  • +
+

And like normal Hamlet, you can use variable interpolation and control structures. So a + slightly more complex example would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+import Data.Text (Text, pack)
+import Data.Map (empty)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Michael" 26
+    , Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Gavriella" 1
+    ]
+
+main :: IO ()
+main =
+    writeFile def "people.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>Some People
+<body>
+    <h1>Some People
+    $if null people
+        <p>There are no people.
+    $else
+        <dl>
+            $forall person <- people
+                ^{personNodes person}
+|]
+
+personNodes :: Person -> [Node]
+personNodes person = [xml|
+<dt>#{personName person}
+<dd>#{pack $ show $ personAge person}
+|]
+

A few more notes:

+
    +
  • +

    The caret-interpolation (^{...}) takes a list of nodes, and so can easily embed + other xml-quotations.

    +
  • +
  • +

    Unlike Hamlet, hash-interpolations (#{...}) are not polymorphic, and can only accept Text values.

    +
  • +
+
+
+

xml2html

+

+

So far in this chapter, our examples have revolved around XHTML. I've done that so far + simply because it is likely to be the most familiar form of XML for most of our readers. But + there's an ugly side to all this that we must acknowledge: not all XHTML will be correct HTML. + The following discrepancies exist:

+
    +
  • +

    There are some void tags (e.g., img, br) in HTML + which do not need to have close tags, and in fact are not allowed to.

    +
  • +
  • +

    HTML does not understand self-closing tags, so <script></script> and + <script/> mean very different things.

    +
  • +
  • +

    Combining the previous two points: you are free to self-close void tags, though to a browser + it won't mean anything.

    +
  • +
  • +

    In order to avoid quirks mode, you should start your HTML documents with a + DOCTYPE statement.

    +
  • +
  • +

    We do not want the XML declaration <?xml ...?> at the top of an HTML + page

    +
  • +
  • +

    We do not want any namespaces used in HTML, while XHTML is fully namespaced.

    +
  • +
  • +

    The contents of <style> and <script> + tags should not be escaped.

    +
  • +
+

That's where the xml2html package comes into play. It provides a + ToHtml instance for Nodes, + Documents and Elements. In order to use it, just import the + Text.XML.Xml2Html module.

+
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+import Text.Blaze (toHtml)
+import Text.Blaze.Renderer.String (renderHtml)
+import Text.XML
+import Text.Hamlet.XML
+import Text.XML.Xml2Html ()
+
+main :: IO ()
+main = putStr $ renderHtml $ toHtml $ Document (Prologue [] Nothing []) root []
+
+root :: Element
+root = Element "html" [] [xml|
+<head>
+    <title>Test
+    <script>if (5 < 6 || 8 > 9) alert("Hello World!");
+    <style>body > h1 { color: red }
+<body>
+    <h1>Hello World!
+|]
+

Outputs: (whitespace added)

+
<!DOCTYPE HTML>
+<html>
+    <head>
+        <title>Test</title>
+        <script>if (5 < 6 || 8 > 9) alert("Hello World!");</script>
+        <style>body > h1 { color: red }</style>
+    </head>
+    <body>
+        <h1>Hello World!</h1>
+    </body>
+</html>
+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/yesod-typeclass.html b/public/book-1.1/yesod-typeclass.html new file mode 100644 index 00000000..91a4fb23 --- /dev/null +++ b/public/book-1.1/yesod-typeclass.html @@ -0,0 +1,547 @@ + Yesod Typeclass :: Yesod Web Framework Book- Version 1.1 +
+

Yesod Typeclass

+ + +

+

+

Every one of our Yesod applications requires an instance of the Yesod typeclass. So far, we've only seen defaultLayout. In this chapter, we'll explore the meaning of many of the + methods of the Yesod typeclass.

+

The Yesod typeclass gives us a central place for defining + settings for our application. Eeverything else has a default definition which is usually + the right thing. But in order to build a powerful, customized application, you'll + usually end up wanting to override at least a few of these methods.

+
+

Rendering and Parsing URLs

+

+

We've already mentioned how Yesod is able to automatically render type-safe URLs into a textual URL that can be inserted into an HTML page. Let's say we have a route definition that looks like:

+
mkYesod "MyApp" [parseRoutes|
+/some/path SomePathR GET
+]
+

If we place SomePathR into a hamlet template, how does Yesod render + it? Yesod always tries to construct absolute URLs. This is especially + useful once we start creating XML sitemaps and Atom feeds, or sending emails. But in + order to construct an absolute URL, we need to know the domain name of the + application.

+

You might think we could get that information from the user's request, but we still + need to deal with ports. And even if we get the port number from the request, are we + using HTTP or HTTPS? And even if you know that, such an approach would + mean that, depending on how the user submitted a request would generate different URLs. + For example, we would generate different URLs depending if the user connected to + "example.com" or "www.example.com". For Search Engine Optimization, we want to be able + to consolidate on a single canonical URL.

+

And finally, Yesod doesn't make any assumption about where you host your application. For example, I may have a mostly static site (http://static.example.com/), but I'd like to stick a Yesod-powered Wiki at /wiki/. There is no reliable way for an application to determine what subpath it is being hosted from. So instead of doing all of this guesswork, Yesod needs you to tell it the application root.

+

Using the wiki example, you would write your Yesod instance as:

+
instance Yesod MyWiki where
+    approot _ = "http://static.example.com/wiki" -- FIXME this is out-of-date
+

Notice that there is no trailing slash there. Next, when Yesod wants to construct a URL + for SomePathR, it determines that the relative path for + SomePathR is /some/path, appends that to your + approot and creates http://static.example.com/wiki/some/path.

+

This also explains our cryptic approot _ = "" FIXME: for our + examples in the book, we're always serving from the root of the domain (in our case, + localhost). By using an empty string, SomePathR + renders to /some/path, which works just fine. In real life + applications, however, you should use a real application root.

+ +

And by the way, the scaffolded site can load different settings for developing, + testing, staging, and production builds, so you can easily test on one domain- like + localhost- and serve from a different domain.

+ +
+

joinPath

+

+

In order to convert a type-safe URL into a text value, Yesod uses two helper functions. + The first is the renderRoute method of the RenderRoute typeclass. Every type-safe URL is an instance of this + typeclass. renderRoute converts a value into a list of path + pieces. For example, our SomePathR from above would be + converted into ["some", "path"].

+ +

The other function is the joinPath method of the Yesod + typeclass. This function takes four arguments: the foundation value, the application + root, a list of path segments and a list of query string parameters, and returns a + textual URL. The default implementation does the "right thing": it separates the path + pieces by forward slashes, prepends the application root and appends the query + string.

+

If you are happy with default URL rendering, you should not need to modify it. However, if you want to modify URL rendering to do things like append a trailing slash, this would be the place to do it.

+
+
+

cleanPath

+

+

The flip side to joinPath is cleanPath. Let's look at how it gets used in the dispatch process:

+
    +
  1. +

    The path info requested by the user is split into a series of path pieces.

    +
  2. +
  3. +

    We pass the path pieces to the cleanPath function.

    +
  4. +
  5. +

    If cleanPath indicates a redirect (a Left response), + then a 301 response is sent to the client. This is used to force canonical URLs (eg, + remove extra slashes).

    +
  6. +
  7. +

    Otherwise, we try to dispatch using the response from cleanPath (a + Right). If this works, we return a response. Otherwise, we + return a 404.

    +
  8. +
+

This combination allows subsites to retain full control of how their URLs appear, yet allows master sites to have modified URLs. As a simple example, let's see how we could modify Yesod to always produce trailing slashes on URLs:

+
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-}
+import Yesod
+import Network.HTTP.Types (encodePath)
+import Blaze.ByteString.Builder.Char.Utf8 (fromText)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
+import Control.Arrow ((***))
+import Data.Monoid (mappend)
+
+data Slash = Slash
+
+mkYesod "Slash" [parseRoutes|
+/ RootR GET
+/foo FooR GET
+|]
+
+instance Yesod Slash where
+    joinPath _ ar pieces' qs' =
+        fromText ar `mappend` encodePath pieces qs
+      where
+        qs = map (TE.encodeUtf8 *** go) qs'
+        go "" = Nothing
+        go x = Just $ TE.encodeUtf8 x
+        pieces = pieces' ++ [""]
+
+    -- We want to keep canonical URLs. Therefore, if the URL is missing a
+    -- trailing slash, redirect. But the empty set of pieces always stays the
+    -- same.
+    cleanPath _ [] = Right []
+    cleanPath _ s
+        | dropWhile (not . T.null) s == [""] = -- the only empty string is the last one
+            Right $ init s
+        -- Since joinPath will append the missing trailing slash, we simply
+        -- remove empty pieces.
+        | otherwise = Left $ filter (not . T.null) s
+
+getRootR = defaultLayout [whamlet|
+<p>
+    <a href=@{RootR}>RootR
+<p>
+    <a href=@{FooR}>FooR
+|]
+
+getFooR = getRootR
+
+main = warpDebug 3000 Slash
+
+

First, let's look at our joinPath implementation. This is copied almost + verbatim from the default Yesod implementation, with one difference: we append an extra + empty string to the end. When dealing with path pieces, an empty string will append + another slash. So adding an extra empty string will force a trailing slash.

+

+ cleanPath is a little bit trickier. First, we check for the empty path + like before, and if so pass it through as-is. We use Right to indicate that a redirect + is not necessary. The next clause is actually checking for two different possible URL + issues:

+
    +
  • +

    There is a double slash, which would show up as an empty string in the middle of our paths.

    +
  • +
  • +

    There is a missing trailing slash, which would show up as the last piece not being an empty string.

    +
  • +
+

Assuming neither of those conditions hold, then only the last piece is empty, and we + should dispatch based on all but the last piece. However, if this is not the case, we + want to redirect to a canonical URL. In this case, we strip out all empty pieces and do + not bother appending a trailing slash, since joinPath will do that for + us.

+
+
+
+

defaultLayout

+

+

Most websites like to apply some general template to all of their pages. + defaultLayout is the recommended approach for this. While you could + just as easily define your own function and call that instead, when you override + defaultLayout all of the Yesod-generated pages (error pages, + authentication pages) automatically get this style.

+

Overriding is very straight-forward: we use widgetToPageContent + to convert a Widget to a title, head tags and body tags, and then use + hamletToRepHtml to convert a Hamlet template into a + RepHtml. We can even add extra widget components, like a Lucius + template. from within defaultLayout. An example should make this all + clear:

+
    defaultLayout contents = do
+        PageContent title headTags bodyTags <- widgetToPageContent $ do
+            toWidget [cassius|
+#body
+    font-family: sans-serif
+#wrapper
+    width: 760px
+    margin: 0 auto
+|]
+            addWidget contents
+        hamletToRepHtml [hamlet|
+$doctype 5
+
+<html>
+    <head>
+        <title>#{title}
+        ^{headTags}
+    <body>
+        <div id="wrapper">
+            ^{bodyTags}
+|]
+
+
+

getMessage

+

+

Even though we haven't covered sessions yet, I'd like to mention + getMessage here. A common pattern in web development is setting a + message in one handler and displaying it in another. For example, if a user + POSTs a form, you may want to redirect him/her to another page + along with a "Form submission complete" message.

+ +

To facilitate this, Yesod comes built in with a pair of functions: + setMessage sets a message in the user session, and + getMessage retrieves the message (and clears it, so it doesn't + appear a second time). It's recommended that you put the result of + getMessage into your defaultLayout. For + example:

+
    defaultLayout contents = do
+        PageContent title headTags bodyTags <- widgetToPageContent contents
+        mmsg <- getMessage
+        hamletToRepHtml [hamlet|
+$doctype 5
+
+<html>
+    <head>
+        <title>#{title}
+        ^{headTags}
+    <body>
+        $maybe msg <- mmsg
+            <div #message>#{msg}
+        ^{bodyTags}
+|]
+
+

We'll cover getMessage/setMessage in more detail when + we discuss sessions.

+
+
+
+

Custom error pages

+

+

One of the marks of a professional web site is a properly designed error page. Yesod + gets you a long way there by automatically using your defaultLayout for + displaying error pages. But sometimes, you'll want to go even further. For this, you'll + want to override the errorHandler method:

+
    errorHandler NotFound = fmap chooseRep $ defaultLayout $ do
+        setTitle "Request page not located"
+        toWidget [hamlet|
+<h1>Not Found
+<p>We apologize for the inconvenience, but the requested page could not be located.
+|]
+    errorHandler other = defaultErrorHandler other
+
+

Here we specify a custom 404 error page. We can also use the + defaultErrorHandler when we don't want to write a custom handler + for each error type. Due to type constraints, we need to start off our methods with + fmap chooseRep, but otherwise you can write a typical handler + function.

+

In fact, you could even use special responses like redirects:

+
    errorHandler NotFound = redirect RootR
+    errorHandler other = defaultErrorHandler other
+
+ +
+
+

External CSS and Javascript

+

+ +

One of the most powerful, and most intimidating, methods in the Yesod typeclass is + addStaticContent. Remember that a Widget consists of multiple + components, including CSS and Javascript. How exactly does that CSS/JS arrive in the user's + browser? By default, they are served in the <head> of the page, inside + <style> and <script> tags, respectively.

+

That might be simple, but it's far from efficient. Every page load will now require loading up + the CSS/JS from scratch, even if nothing changed! What we 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 (text/css or text/javascript) and the + content itself. It will then return one of three possible results:

+
+
Nothing
+

No static file saving occurred; embed this content directly in the HTML. This is the default + behavior.

+
+
Just (Left Text)
+

This content was saved in an external file, and use the given textual link to refer to + it.

+
+
Just (Right (Route a, Query))
+

Same, but now use a type-safe URL along with some query string parameters.

+
+
+

The Left result is useful if you want to store your static files on + an external server, such as a CDN or memory-backed server. The Right result is + more commonly used, and ties in very well with the static subsite. This is the recommended + approach for most applications, and is provided by the scaffolded site by default.

+ +

The scaffolded addStaticContent does a number of intelligent things + to help you out:

+
    +
  • +

    It automatically minifies your Javascript using the hjsmin package.

    +
  • +
  • +

    It names the output files based on a hash of the file contents. This means you can set your + cache headers to far in the future without fears of stale content.

    +
  • +
  • +

    Also, since filenames are based on hashes, you can be guaranteed that a file doesn't need to + be written if a file with the same name already exists. The scaffold code automatically checks + for the existence of that file, and avoids the costly disk I/O of a write if it's not + necessary.

    +
  • +
+
+
+

Smarter Static Files

+

+

Google recommends an important optimization: serve static files from a separate domain. The advantage + to this approach is that cookies set on your main domain are not sent when retrieving static + files, thus saving on a bit of bandwidth.

+

To facilitate this, we have the urlRenderOverride method. This method + intercepts the normal URL rendering and sets a special value for some routes. For example, the + scaffolding defines this method as:

+
    urlRenderOverride y (StaticR s) =
+        Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s
+    urlRenderOverride _ _ = Nothing
+

This means that static routes are served from a special static root, which you can configure to + be a different domain. This is a great example of the power and flexibility of type-safe URLs: + with a single line of code you're able to change the rendering of static routes throughout all of + your handlers.

+
+
+

Authentication/Authorization

+

+

For simple applications, checking permissions inside each handler function can be a + simple, convenient approach. However, it doesn't scale well. Eventually, you're going to want to + have a more declarative approach. Many systems out there define ACLs, special config files, and a + lot of other hocus-pocus. In Yesod, it's just plain old Haskell. There are three methods + involved:

+
+
isWriteRequest
+

Determine if the current request is a "read" or "write" operations. By default, + Yesod follows RESTful principles, and assumes GET, HEAD, + OPTIONS, and TRACE requests are read-only, while all others + are can write.

+
+
isAuthorized
+

Takes a route (i.e., type-safe URL) and a boolean indicating whether or not the + request is a write request. It returns an AuthResult, which can have one of + three values:

    +
  • +

    + Authorized +

    +
  • +
  • +

    + AuthenticationRequired +

    +
  • +
  • +

    + Unauthorized +

    +
  • +
By default, it returns Authorized for all requests.

+
+
authRoute
+

If isAuthorized returns AuthenticationRequired, + then redirect to the given route. If no route is provided (the default), return a 403 + "Permission Denied" message.

+
+
+

These methods tie in nicely with the yesod-auth package, + which is used by the scaffolded site to provide a number of authentication options, such as + OpenID, BrowserID, email, username and Twitter. We'll cover more concrete examples in the auth chapter.

+
+
+

Some Simple Settings

+

+

Not everything in the Yesod typeclass is complicated. Some methods are simple + functions. Let's just go through the list:

+
+
encryptKey
+

Yesod uses client-side sessions, which are stored in encrypted, + cryptographically-hashed cookies. Well, as long as you provide an encryption key. If this + function returns Nothing, then sessions are disabled. This can be a useful optimization on + sites that don't need session facilities, as it avoids an encrypt/decrypt pair on each + request.

+

+
+
clientSessionDuration
+

How long a session should last for. By default, this is two hours.

+
+
sessionIpAddress
+

By default, sessions are tied to an individual IP address. If your users are sitting behind + a proxy server, this can cause trouble when their IP suddenly changes. This setting lets you + disable this security feature.

+
+
cookiePath
+

What paths within your current domain to set cookies for. The default is "/", and will + almost always be correct. One exception might be when you're serving from a subpath within a + domain (like our wiki example above).

+
+
maximumContentLength
+

To prevent Denial of Server (DoS) attacks, Yesod will limit the size of request bodies. Some + of the time, you'll want to bump that limit for some routes (e.g., a file upload page). This is + where you'd do that.

+
+
yepnopeJs
+

You can specify the location of the yepnope Javascript library. If this is given, then yepnope will be + used to asynchronously load all of the Javascript on your page.

+
+
+
+
+

Summary

+

+

The Yesod typeclass has a number of overrideable methods that allow you to configure your + application. They are all optional, and provide + sensible defaults. By using built-in Yesod constructs like + defaultLayout and getMessage, you'll get a + consistent look-and-feel throughout your site, including pages automatically generated + by Yesod such as error pages and authentication.

+

We haven't covered all the methods in the Yesod typeclass in this chapter. For a full + listing of methods available, you should consult the Haddock documentation.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.1/yesods-monads.html b/public/book-1.1/yesods-monads.html new file mode 100644 index 00000000..3644c392 --- /dev/null +++ b/public/book-1.1/yesods-monads.html @@ -0,0 +1,384 @@ + Yesod's Monads :: Yesod Web Framework Book- Version 1.1 +
+

Yesod's Monads

+ + +

+

+

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 let's 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.

+

So far, we've presented some very straight-forward ways to use these monads: your main + handler will run in Handler, using runDB to execute a + YesodDB query, and defaultLayout to return a + Widget, which in turn was created by calls to toWidget.

+

However, if we have a deeper understanding of these types, we can achieve some fancier + results.

+
+

Monad Transformers

+

+
+

Monads are like onions. Monads are not like cakes.Shrek, more or less +

+
+

Before we get into the heart of Yesod's monads, we need to understand a bit about + monad transformers. (If you already know all about monad transformers, you can likely skip this + section.) Different monads provide different functionality: Reader allows + read-only access to some piece of data throughout a computation, Error allows + you to short-circuit computations, and so on.

+

Often times, however, you would like to be able to combine a few of these features + together. After all, why not have a computation with read-only access to some settings variable, + that could error out at any time? One approach to this would be to write a new monad like + ReaderError, but this has the obvious downside of exponential complexity: + you'll need to write a new monad for every single possible combination.

+

Instead, we have monad transformers. In addition to Reader, we have + ReaderT, which adds reader functionality to any other monad. So we could + represent our ReaderError as (conceptually):

+
type ReaderError = ReaderT Error
+

In order to access our settings variable, we can use the ask function. But what about short-circuiting a computation? We'd like to use + throwError, but that won't exactly work. Instead, we need to lift our call into the next monad up. In other words:

+
throwError :: errValue -> Error
+lift . throwError :: errValue -> ReaderT Error
+

There are a few things you should pick up here:

+
    +
  • +

    A transformer can be used to add functionality to an existing monad.

    +
  • +
  • +

    A transformer must always wrap around an existing monad.

    +
  • +
  • +

    The functionality available in a wrapped monad will be dependent not only on the + monad transformer, but also on the inner monad that is being wrapped.

    +
  • +
+

A great example of that last point is the IO monad. No matter how + many layers 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

+

+

We've already discussed two of our transformers previously: Handler and + Widget. Just to recap, there are two special things about these + transformers:

+
    +
  1. +

    In order to simplify error messages, they are not actual transformers. Instead, they are + newtypes that hard-code their inner monads.

    +

    +
  2. +
  3. +

    In reality they have extra type parameters for the sub and master site. As a result, the + Yesod libraries provide GHandler sub master a and GWidget sub master + a, and each site gets a pair of type synonyms type Handler = GHandler MyApp + MyApp and type Widget = GWidget MyApp My App ().

    +
  4. +
+

In persistent, we have a typeclass called + PersistStore. This typeclass defines all of the primitive operations you can + perform on a database, like get. This typeclass essentially looks like + class (Monad (b m)) => PersistStore b m. b is the backend itself, and is in fact a monad transformer, while m is the inner monad that b wraps around. Both SQL and + MongoDB have their own instances; in the case of SQL, it looks like:

+
instance MonadBaseControl IO m => PersistBackend SqlPersist m
+

This means that you can run a SQL database with any underlying monad, so long as that + underlying monad supports MonadBaseControl IO, which allows you to + properly deal with exceptions in a monad stack. That basically means any transformer stack built + around IO (besides exceptional cases like ContT). + Fortunately for us, that includes both Handler and Widget. The + takeaway here is that we can layer our Persistent transformer on top of Handler + or Widget.

+ +

In order to make it simpler to refer to the relevant Persistent transformer, the + yesod-persistent package defines the YesodPersistBackend + associated type. For example, if I have a site called MyApp and it uses SQL, I + would define something like type instance YesodPersistBackend MyApp = + SqlPersist.

+

When we want to run our database actions, we'll have a SqlPersist + wrapped around a Handler or Widget. We can then use the + standard Persistent unwrap functions (like runSqlPool) to run the action and get + back a normal Handler/Widget. To automate this, we provide the + runDB function. Putting it all together, we can now run database actions inside + our handlers and widgets.

+

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 + if you look at that last paragraph again, you'll realize that's not the way things have to be. + Since a widget is a transformer on top of a handler, anything you do in a handler can be done in + a widget, including database actions. All you have to do is lift.

+
+
+

Example: Database-driven navbar

+

+

Let's put some of this new knowledge into action. We want to create a + Widget that generates its output based on the contents of the + database. Previously, our approach would have been to load up the data in a + Handler, and then pass that data into a Widget. + Now, we'll do the loading of data in the Widget itself. This is a boon + for modularity, as this Widget can be used in any + Handler we want, without any need to pass in the database + contents.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell, FlexibleContexts,
+             QuasiQuotes, TypeFamilies, MultiParamTypeClasses, GADTs #-}
+import Yesod
+import Database.Persist.Sqlite
+import Data.Text (Text)
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
+Link
+    title Text
+    url Text
+    added UTCTime
+|]
+
+data LinksExample = LinksExample ConnectionPool
+
+mkYesod "LinksExample" [parseRoutes|
+/ RootR GET
+/add-link AddLinkR POST
+|]
+
+instance Yesod LinksExample
+
+instance RenderMessage LinksExample FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+instance YesodPersist LinksExample where
+    type YesodPersistBackend LinksExample = SqlPersist
+    runDB db = do
+        LinksExample pool <- getYesod
+        runSqlPool db pool
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<form method=post action=@{AddLinkR}>
+    <p>
+        Add a new link to #
+        <input type=url name=url value=http://>
+        \ titled #
+        <input type=text name=title>
+        \ #
+        <input type=submit value="Add link">
+<h2>Existing links
+^{existingLinks}
+|]
+
+existingLinks :: Widget
+existingLinks = do
+    links <- lift $ runDB $ selectList [] [LimitTo 5, Desc LinkAdded]
+    [whamlet|
+<ul>
+    $forall Entity _ link <- links
+        <li>
+            <a href=#{linkUrl link}>#{linkTitle link}
+|]
+
+postAddLinkR :: Handler ()
+postAddLinkR = do
+    url <- runInputPost $ ireq urlField "url"
+    title <- runInputPost $ ireq textField "title"
+    now <- liftIO getCurrentTime
+    runDB $ insert $ Link title url now
+    setMessage "Link added"
+    redirect RootR
+
+main :: IO ()
+main = withSqlitePool "links.db3" 10 $ \pool -> do
+    runSqlPool (runMigration migrateAll) pool
+    warpDebug 3000 $ LinksExample pool
+

Pay attention in particular to the existingLinks function. + Notice how all we needed to do was apply lift to a normal + database action. And from within getRootR, we treated existingLinks like any ordinary Widget, + no special parameters at all. See the figure for the output of this app.

+ +
+
+

Example: Request information

+

+

Likewise, you can get request information inside a Widget. Here we + can determine the sort order of a list based on a GET parameter.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
+             QuasiQuotes, TypeFamilies, MultiParamTypeClasses, GADTs #-}
+import Yesod
+import Data.Text (Text)
+import Data.List (sortBy)
+import Data.Ord (comparing)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Michael" 26
+    , Person "Gavriella" 1
+    ]
+
+data People = People
+
+mkYesod "People" [parseRoutes|
+/ RootR GET
+|]
+
+instance Yesod People
+
+instance RenderMessage People FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+
+getRootR :: Handler RepHtml
+getRootR = defaultLayout [whamlet|
+<p>
+    <a href="?sort=name">Sort by name
+    \ | #
+    <a href="?sort=age">Sort by age
+    \ | #
+    <a href="?">No sort
+^{showPeople}
+|]
+
+showPeople :: Widget
+showPeople = do
+    msort <- lift $ runInputGet $ iopt textField "sort"
+    let people' =
+            case msort of
+                Just "name" -> sortBy (comparing personName) people
+                Just "age"  -> sortBy (comparing personAge)  people
+                _           -> people
+    [whamlet|
+<dl>
+    $forall person <- people'
+        <dt>#{personName person}
+        <dd>#{show $ personAge person}
+|]
+
+main :: IO ()
+main = warpDebug 3000 People
+

Once again, all we need to do is lift our normal + Handler code (in this case, runInputGet) to have + it run in our Widget.

+
+
+

Summary

+

+

If you completely ignore this chapter, you'll still be able to use Yesod to great benefit. The + advantage of understanding how Yesod's monads interact is to be able to produce cleaner, more + modular code. Being able to perform arbitrary actions in a Widget can be a + powerful tool, and understanding how Persistent and your Handler code interact + can help you make more informed design decisions in your app.

+
+
+
+

Note: You are looking at version 1.1 of the book, which is three versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2.html b/public/book-1.2.html new file mode 100644 index 00000000..8c9b3d54 --- /dev/null +++ b/public/book-1.2.html @@ -0,0 +1,188 @@ + Yesod Web Framework Book- Version 1.2 +

Available from O'Reilly:

+
Developing Web Applications with Haskell and Yesod + +
+
+

The current version of the book +covers Yesod 1.6. We also maintain older copies for +version 1.4, version 1.2 +, and +version 1.1.

+ +
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/authentication-and-authorization.html b/public/book-1.2/authentication-and-authorization.html new file mode 100644 index 00000000..eed38615 --- /dev/null +++ b/public/book-1.2/authentication-and-authorization.html @@ -0,0 +1,673 @@ + Authentication and Authorization :: Yesod Web Framework Book- Version 1.2 +
+

Authentication and Authorization

+ + +

Authentication and authorization are two very related, and yet separate, +concepts. While the former deals with identifying a user, the latter determines +what a user is allowed to do. Unfortunately, since both terms are often +abbreviated as "auth," the concepts are often conflated.

+

Yesod provides built-in support for a number of third-party authentication +systems, such as OpenID, BrowserID and OAuth. These are systems where your +application trusts some external system for validating a user’s credentials. +Additionally, there is support for more commonly used username/password and +email/password systems. The former route ensures simplicity for users (no new +passwords to remember) and implementors (no need to deal with an entire +security architecture), while the latter gives the developer more control.

+

On the authorization side, we are able to take advantage of REST and type-safe +URLs to create simple, declarative systems. Additionally, since all +authorization code is written in Haskell, you have the full flexibility of the +language at your disposal.

+

This chapter will cover how to set up an "auth" solution in Yesod and discuss +some trade-offs in the different authentication options.

+
+

Overview

+

The yesod-auth package provides a unified interface for a number of different +authentication plugins. The only real requirement for these backends is that +they identify a user based on some unique string. In OpenID, for instance, this +would be the actual OpenID value. In BrowserID, it’s the email address. For +HashDB (which uses a database of hashed passwords), it’s the username.

+

Each authentication plugin provides its own system for logging in, whether it +be via passing tokens with an external site or a email/password form. After a +successful login, the plugin sets a value in the user’s session to indicate +his/her AuthId. This AuthId is usually a Persistent ID from a table used +for keeping track of users.

+

There are a few functions available for querying a user’s AuthId, most +commonly maybeAuthId, requireAuthId, maybeAuth and requireAuth. The +require versions will redirect to a login page if the user is not logged in, +while the second set of functions (the ones not ending in Id) give both the +table ID and entity value.

+

Since all of the storage of AuthId is built on top of sessions, all of the +rules from there apply. In particular, the data is stored in an encrypted, +HMACed client cookie, which automatically times out after a certain +configurable period of inactivity. Additionally, since there is no server-side +component to sessions, logging out simply deletes the data from the session +cookie; if a user reuses an older cookie value, the session will still be +valid.

+ +

On the flip side, authorization is handled by a few methods inside the Yesod +typeclass. For every request, these methods are run to determine if access +should be allowed, denied, or if the user needs to be authenticated. By +default, these methods allow access for every request. Alternatively, you can +implement authorization in a more ad-hoc way by adding calls to requireAuth +and the like within individual handler functions, though this undermines many +of the benefits of a declarative authorization system.

+
+
+

Authenticate Me

+

Let’s jump right in with an example of authentication.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default           (def)
+import           Data.Text              (Text)
+import           Network.HTTP.Conduit   (Manager, conduitManagerSettings, newManager)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.BrowserId
+import           Yesod.Auth.GoogleEmail
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Note: In order to log in with BrowserID, you must correctly
+    -- set your hostname here.
+    approot = ApprootStatic "http://localhost:3000"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    getAuthId = return . Just . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ =
+        [ authBrowserId def
+        , authGoogleEmail
+        ]
+
+    authHttpManager = httpManager
+
+    -- The default maybeAuthId assumes a Persistent database. We're going for a
+    -- simpler AuthId, so we'll just do a direct lookup in the session.
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = do
+    man <- newManager conduitManagerSettings
+    warp 3000 $ App man
+

We’ll start with the route declarations. First we declare our standard HomeR +route, and then we set up the authentication subsite. Remember that a subsite +needs four parameters: the path to the subsite, the route name, the subsite +name, and a function to get the subsite value. In other words, based on the +line:

+
/auth AuthR Auth getAuth
+

We need to have getAuth :: MyAuthSite → Auth. While we haven’t written +that function ourselves, yesod-auth provides it automatically. With other +subsites (like static files), we provide configuration settings in the subsite +value, and therefore need to specify the get function. In the auth subsite, we +specify these settings in a separate typeclass, YesodAuth.

+ +

So what exactly goes in this YesodAuth instance? There are six required declarations:

+
    +
  • +

    +AuthId is an associated type. This is the value yesod-auth will give you + when you ask if a user is logged in (via maybeAuthId or requireAuthId). + In our case, we’re simply using Text, to store the raw identifier- email + address in our case, as we’ll soon see. +

    +
  • +
  • +

    +getAuthId gets the actual AuthId from the Creds (credentials) data + type. This type has three pieces of information: the authentication backend + used (browserid or googleemail in our case), the actual identifier, and an + associated list of arbitrary extra information. Each backend provides + different extra information; see their docs for more information. +

    +
  • +
  • +

    +loginDest gives the route to redirect to after a successful login. +

    +
  • +
  • +

    +Likewise, logoutDest gives the route to redirect to after a logout. +

    +
  • +
  • +

    +authPlugins is a list of individual authentication backends to use. In our example, we’re using BrowserID, which logs in via Mozilla’s BrowserID system, and Google Email, which authenticates a user’s email address using their Google account. The nice thing about these two backends is: +

    +
      +
    • +

      +They require no set up, as opposed to Facebook or OAuth, which require setting up credentials. +

      +
    • +
    • +

      +They use email addresses as identifiers, which people are comfortable with, as opposed to OpenID, which uses a URL. +

      +
    • +
    +
  • +
  • +

    +authHttpManager gets an HTTP connection manager from the foundation type. + This allow authentication backends which use HTTP connections (i.e., almost + all third-party login systems) to share connections, avoiding the cost of + restarting a TCP connection for each request. +

    +
  • +
+

In addition to these six methods, there are other methods available to control +other behavior of the authentication system, such as what the login page looks +like. For more information, please +see +the API documentation.

+

In our HomeR handler, we have some simple links to the login and logout +pages, depending on whether or not the user is logged in. Notice how we +construct these subsite links: first we give the subsite route name (AuthR), +followed by the route within the subsite (LoginR and LogoutR).

+

The figures below show what the login process looks like from a user perspective.

+

Initial page load

+ + + + + + +
+

BrowserID login screen

+ + + + + + +
+

Homepage after logging in

+ + + + + + +
+
+
+

Email

+

For many use cases, third-party authentication of email will be sufficient. +Occasionally, you’ll want users to actual create passwords on your site. The +scaffolded site does not include this setup, because:

+
    +
  • +

    +In order to securely accept passwords, you need to be running over SSL. Many + users are not serving their sites over SSL. +

    +
  • +
  • +

    +While the email backend properly salts and hashes passwords, a compromised + database could still be problematic. Again, we make no assumptions that Yesod + users are following secure deployment practices. +

    +
  • +
  • +

    +You need to have a working system for sending email. Many web servers these + days are not equipped to deal with all of the spam protection measures used + by mail servers. +

    +
  • +
+ +

But assuming you are able to meet these demands, and you want to have a +separate password login specifically for your site, Yesod offers a built-in +backend. It requires quite a bit of code to set up, since it needs to store +passwords securely in the database and send a number of different emails to +users (verify account, password retrieval, etc.).

+

Let’s have a look at a site that provides email authentication, storing +passwords in a Persistent SQLite database.

+
{-# LANGUAGE DeriveDataTypeable    #-}
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE GADTs                 #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Monad            (join)
+import           Control.Monad.Logger (runNoLoggingT)
+import           Data.Maybe               (isJust)
+import           Data.Text                (Text)
+import qualified Data.Text.Lazy.Encoding
+import           Data.Typeable            (Typeable)
+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
+import           Yesod.Auth.Email
+
+share [mkPersist sqlSettings { mpsGeneric = False }, mkMigrate "migrateAll"] [persistLowerCase|
+User
+    email Text
+    password Text Maybe -- Password may not be set yet
+    verkey Text Maybe -- Used for resetting passwords
+    verified Bool
+    UniqueUser email
+    deriving Typeable
+|]
+
+data App = App Connection
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Emails will include links, so be sure to include an approot so that
+    -- the links are valid!
+    approot = ApprootStatic "http://localhost:3000"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Set up Persistent
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlPersistT
+    runDB f = do
+        App conn <- getYesod
+        runSqlConn f conn
+
+instance YesodAuth App where
+    type AuthId App = UserId
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    authPlugins _ = [authEmail]
+
+    -- Need to find the UserId for the given email address.
+    getAuthId creds = runDB $ do
+        x <- insertBy $ User (credsIdent creds) Nothing Nothing False
+        return $ Just $
+            case x of
+                Left (Entity userid _) -> userid -- newly added user
+                Right userid -> userid -- existing user
+
+    authHttpManager = error "Email doesn't need an HTTP manager"
+
+-- Here's all of the email-specific code
+instance YesodAuthEmail App where
+    type AuthEmailId App = UserId
+
+    afterPasswordRoute _ = HomeR
+
+    addUnverified email verkey =
+        runDB $ insert $ User email Nothing (Just verkey) False
+
+    sendVerifyEmail email _ verurl =
+        liftIO $ renderSendMail (emptyMail $ Address Nothing "noreply")
+            { mailTo = [Address Nothing email]
+            , mailHeaders =
+                [ ("Subject", "Verify your email address")
+                ]
+            , mailParts = [[textPart, htmlPart]]
+            }
+      where
+        textPart = Part
+            { partType = "text/plain; charset=utf-8"
+            , partEncoding = None
+            , partFilename = Nothing
+            , partContent = Data.Text.Lazy.Encoding.encodeUtf8
+                [stext|
+                    Please confirm your email address by clicking on the link below.
+
+                    #{verurl}
+
+                    Thank you
+                |]
+            , partHeaders = []
+            }
+        htmlPart = Part
+            { partType = "text/html; charset=utf-8"
+            , partEncoding = None
+            , partFilename = Nothing
+            , partContent = renderHtml
+                [shamlet|
+                    <p>Please confirm your email address by clicking on the link below.
+                    <p>
+                        <a href=#{verurl}>#{verurl}
+                    <p>Thank you
+                |]
+            , partHeaders = []
+            }
+    getVerifyKey = runDB . fmap (join . fmap userVerkey) . get
+    setVerifyKey uid key = runDB $ update uid [UserVerkey =. Just key]
+    verifyAccount uid = runDB $ do
+        mu <- get uid
+        case mu of
+            Nothing -> return Nothing
+            Just u -> do
+                update uid [UserVerified =. True]
+                return $ Just uid
+    getPassword = runDB . fmap (join . fmap userPassword) . get
+    setPassword uid pass = runDB $ update uid [UserPassword =. Just pass]
+    getEmailCreds email = runDB $ do
+        mu <- getBy $ UniqueUser email
+        case mu of
+            Nothing -> return Nothing
+            Just (Entity uid u) -> return $ Just EmailCreds
+                { emailCredsId = uid
+                , emailCredsAuthId = Just uid
+                , emailCredsStatus = isJust $ userPassword u
+                , emailCredsVerkey = userVerkey u
+                , emailCredsEmail = email
+                }
+    getEmail = runDB . fmap (fmap userEmail) . get
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = withSqliteConn "email.db3" $ \conn -> do
+    runNoLoggingT $ runSqlConn (runMigration migrateAll) conn
+    warp 3000 $ App conn
+
+
+

Authorization

+

Once you can authenticate your users, you can use their credentials to +authorize requests. Authorization in Yesod is simple and declarative: most of +the time, you just need to add the authRoute and isAuthorized methods to +your Yesod typeclass instance. Let’s see an example.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default         (def)
+import           Data.Text            (Text)
+import           Network.HTTP.Conduit (Manager, conduitManagerSettings, newManager)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy -- just for testing, don't use in real life!!!
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET POST
+/admin AdminR GET
+/auth  AuthR  Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+
+    -- route name, then a boolean indicating if it's a write request
+    isAuthorized HomeR True = isAdmin
+    isAuthorized AdminR _ = isAdmin
+
+    -- anyone can access other pages
+    isAuthorized _ _ = return Authorized
+
+isAdmin = do
+    mu <- maybeAuthId
+    return $ case mu of
+        Nothing -> AuthenticationRequired
+        Just "admin" -> Authorized
+        Just _ -> Unauthorized "You must be an admin"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    getAuthId = return . Just . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ = [authDummy]
+
+    authHttpManager = httpManager
+
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Note: Log in as "admin" to be an administrator.
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            <p>
+                <a href=@{AdminR}>Go to admin page
+            <form method=post>
+                Make a change (admins only)
+                \ #
+                <input type=submit>
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    setMessage "You made some change to the page"
+    redirect HomeR
+
+getAdminR :: Handler Html
+getAdminR = defaultLayout
+    [whamlet|
+        <p>I guess you're an admin!
+        <p>
+            <a href=@{HomeR}>Return to homepage
+    |]
+
+main :: IO ()
+main = do
+    manager <- newManager conduitManagerSettings
+    warp 3000 $ App manager
+

authRoute should be your login page, almost always AuthR LoginR. +isAuthorized is a function that takes two parameters: the requested route, +and whether or not the request was a "write" request. You can actually change +the meaning of what a write request is using the isWriteRequest method, but +the out-of-the-box version follows RESTful principles: anything but a GET, +HEAD, OPTIONS or TRACE request is a write request.

+

What’s convenient about the body of isAuthorized is that you can run any +Handler code you want. This means you can:

+
    +
  • +

    +Access the filesystem (normal IO) +

    +
  • +
  • +

    +Lookup values in the database +

    +
  • +
  • +

    +Pull any session or request values you want +

    +
  • +
+

Using these techniques, you can develop as sophisticated an authorization +system as you like, or even tie into existing systems used by your +organization.

+
+
+

Conclusion

+

This chapter covered the basics of setting up user authentication, as well as +how the built-in authorization functions provide a simple, declarative approach +for users. While these are complicated concepts, with many approaches, Yesod +should provide you with the building blocks you need to create your own +customized auth solution.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/basics.html b/public/book-1.2/basics.html new file mode 100644 index 00000000..3b1e9eb9 --- /dev/null +++ b/public/book-1.2/basics.html @@ -0,0 +1,432 @@ + Basics :: Yesod Web Framework Book- Version 1.2 +
+

Basics

+ + +

The first step with any new technology is getting it running. The goal of +this chapter is to get you started with a simple Yesod application, and cover +some of the basic concepts and terminology.

+
+

Hello World

+

Let’s get this book started properly: a simple web page that says Hello +World:

+
{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data HelloWorld = HelloWorld
+
+mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod HelloWorld
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|Hello World!|]
+
+main :: IO ()
+main = warp 3000 HelloWorld
+

If you save that code in helloworld.hs and run it with runhaskell +helloworld.hs, you’ll get a web server running on port 3000. If you point your +browser to http://localhost:3000, you’ll get the following HTML:

+
<!DOCTYPE html>
+<html><head><title></title></head><body>Hello World!</body></html>
+

We’ll refer back to this example through the rest of the chapter.

+
+
+

Routing

+

Like most modern web frameworks, Yesod follows a +front controller +pattern. This means that every request to a Yesod application enters at the +same point and is routed from there. As a contrast, in systems like PHP and ASP +you usually create a number of different files, and the web server +automatically directs requests to the relevant file.

+

In addition, Yesod uses a declarative style for specifying routes. In our +example above, this looked like:

+
mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+ +

In English, all this means is: In the HelloWorld application, create one route. +I’d like to call it HomeR, it should listen for requests to / (the root of +the application), and should answer GET requests. We call HomeR a resource, +which is where the "R" suffix comes from.

+ +

The mkYesod TH function generates quite a bit of code here: a route data +type, parser/render functions, a dispatch function, and some helper types. +We’ll look at this in more detail in the routing chapter. But by using the +-ddump-splices GHC option, we can get an immediate look at the generated +code. A much cleaned up version of it is:

+
instance RenderRoute HelloWorld where
+    data Route HelloWorld = HomeR
+        deriving (Show, Eq, Read)
+    renderRoute HomeR = ([], [])
+
+instance ParseRoute HelloWorld where
+    parseRoute ([], _) = Just HomeR
+    parseRoute _       = Nothing
+
+instance YesodDispatch HelloWorld where
+    yesodDispatch env req =
+        yesodRunner handler env mroute req
+      where
+        mroute = parseRoute (pathInfo req, textQueryString req)
+        handler =
+            case mroute of
+                Nothing -> notFound
+                Just HomeR ->
+                    case requestMethod req of
+                        "GET" -> getHomeR
+                        _     -> badMethod
+
+type Handler = HandlerT HelloWorld IO
+

We can see that the RenderRoute class defines an associated data type +providing the routes for our application. In this simple example, we have just +one route: HomeR. In real life applications, we’ll have many more, and they +will be more complicated than our HomeR.

+

renderRoute takes a route and turns it into path segments and query string +parameters. Again, our example is simple, so the code is likewise simple: both +values are empty lists.

+

ParseRoute provides the inverse function, parseRoute. Here we see the first +strong motivation for our reliance on Template Haskell: it ensures that the +parsing and rendering of routes correspond correctly with each other. This kind +of code can easily become difficult to keep in sync when written by hand. By +relying on code generation, we’re letting the compiler (and Yesod) handle those +details for us.

+

YesodDispatch provides a means of taking an input request and passing it to +the appropriate handler function. The process is essentially:

+
    +
  1. +

    +Parse the request. +

    +
  2. +
  3. +

    +Choose a handler function. +

    +
  4. +
  5. +

    +Run the handler function. +

    +
  6. +
+

The code generation follows a simple format for matching routes to handler +function names, which we’ll describe in the next section.

+

Finally, we have a simple type synonym defining Handler to make our code a +little easier to write.

+

There’s a lot more going on here than we’ve described. The generated dispatch +code actually uses a far more efficient data structure, more type class +instances are created, and there are other cases to handle such as subsites. +We’ll get into the details as we go through the book, especially in the +“Understanding a Request” chapter.

+
+
+

Handler function

+

So we have a route named HomeR, and it responds to GET requests. How do you +define your response? You write a handler function. Yesod follows a standard +naming scheme for these functions: it’s the lower case method name (e.g., GET +becomes get) followed by the route name. In this case, the function name +would be getHomeR.

+

Most of the code you write in Yesod lives in handler functions. This is where +you process user input, perform database queries and create responses. In our +simple example, we create a response using the defaultLayout function. This +function wraps up the content it’s given in your site’s template. By default, +it produces an HTML file with a doctype and html, head and body tags. As +we’ll see in the Yesod typeclass chapter, this function can be overridden to do +much more.

+

In our example, we pass [whamlet|Hello World!|] to defaultLayout. +whamlet is another quasi-quoter. In this case, it converts Hamlet syntax into +a Widget. Hamlet is the default HTML templating engine in Yesod. Together with +its siblings Cassius, Lucius and Julius, you can create HTML, CSS and +Javascript in a fully type-safe and compile-time-checked manner. We’ll see much +more about this in the Shakespeare chapter.

+

Widgets are another cornerstone of Yesod. They allow you to create modular +components of a site consisting of HTML, CSS and Javascript and reuse them +throughout your site. We’ll get into more detail on them in the widgets +chapter.

+
+
+

The Foundation

+

The word ‘HelloWorld’ shows up a number of times in our example. Every Yesod +application has a foundation datatype. This datatype must be an instance of the +Yesod typeclass, which provides a central place for declaring a number of +different settings controlling the execution of our application.

+

In our case, this datatype is pretty boring: it doesn’t contain any +information. Nonetheless, the foundation is central to how our example runs: it +ties together the routes with the instance declaration and lets it all be run. +We’ll see throughout this book that the foundation pops up in a whole bunch of +places.

+

But foundations don’t have to be boring: they can be used to store lots of +useful information, usually stuff that needs to be initialized at program +launch and used throughout. Some very common examples are:

+
    +
  • +

    +A database connection pool. +

    +
  • +
  • +

    +Settings loaded from a config file. +

    +
  • +
  • +

    +An HTTP connection manager. +

    +
  • +
  • +

    +A random number generator. +

    +
  • +
+ +
+
+

Running

+

Once again we mention HelloWorld in our main function. Our foundation +contains all the information we need to route and respond to requests in our +application; now we just need to convert it into something that can run. A +useful function for this in Yesod is warp, which runs the Warp webserver with +a number of default settings enabled on the specified port (here, it’s 3000).

+

One of the features of Yesod is that you aren’t tied down to a single +deployment strategy. Yesod is built on top of the Web Application Interface +(WAI), allowing it to run on FastCGI, SCGI, Warp, or even as a desktop +application using the Webkit library. We’ll discuss some of these options in +the deployment chapter. And at the end of this chapter, we will explain the +development server.

+

Warp is the premiere deployment option for Yesod. It is a lightweight, highly +efficient web server developed specifically for hosting Yesod. It is also used +outside of Yesod for other Haskell development (both framework and +non-framework applications), as well as a standard file server in a number of +production environments.

+
+
+

Resources and type-safe URLs

+

In our hello world, we defined just a single resource (HomeR). A web +application is usually much more exciting with more than one page on it. Let’s +take a look:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data Links = Links
+
+mkYesod "Links" [parseRoutes|
+/ HomeR GET
+/page1 Page1R GET
+/page2 Page2R GET
+|]
+
+instance Yesod Links
+
+getHomeR  = defaultLayout [whamlet|<a href=@{Page1R}>Go to page 1!|]
+getPage1R = defaultLayout [whamlet|<a href=@{Page2R}>Go to page 2!|]
+getPage2R = defaultLayout [whamlet|<a href=@{HomeR}>Go home!|]
+
+main = warp 3000 Links
+

Overall, this is very similar to Hello World. Our foundation is now Links +instead of HelloWorld, and in addition to the HomeR resource, we’ve added +Page1R and Page2R. As such, we’ve also added two more handler functions: +getPage1R and getPage2R.

+

The only truly new feature is inside the whamlet quasi-quotation. We’ll delve +into syntax in the “Shakespeare” chapter, but we can see that:

+
<a href=@{Page1R}>Go to page 1!
+

creates a link to the Page1R resource. The important thing to note here is +that Page1R is a data constructor. By making each resource a data +constructor, we have a feature called type-safe URLs. Instead of splicing +together strings to create URLs, we simply create a plain old Haskell value. By +using at-sign interpolation (@{…}), Yesod automatically renders those +values to textual URLs before sending things off to the user. We can see how +this is implemented by looking again at the -ddump-splices output:

+
instance RenderRoute Links where
+    data Route Links = HomeR | Page1R | Page2R
+      deriving (Show, Eq, Read)
+
+    renderRoute HomeR  = ([], [])
+    renderRoute Page1R = (["page1"], [])
+    renderRoute Page2R = (["page2"], [])
+

In the Route associated type for Links, we have additional constructors for +Page1R and Page2R. We also now have a better glimpse of the return values +for renderRoute. The first part of the tuple gives the path pieces for the +given route. The second part gives the query string parameters; for almost all +use cases, this will be an empty list.

+

It’s hard to over-estimate the value of type-safe URLs. They give you a huge +amount of flexibility and robustness when developing your application. You can +move URLs around at will without ever breaking links. In the routing chapter, +we’ll see that routes can take parameters, such as a blog entry URL taking the +blog post ID.

+

Let’s say you want to switch from routing on the numerical post ID to a +year/month/slug setup. In a traditional web framework, you would need to go +through every single reference to your blog post route and update +appropriately. If you miss one, you’ll have 404s at runtime. In Yesod, all you +do is update your route and compile: GHC will pinpoint every single line of +code that needs to be corrected.

+
+
+

The scaffolded site

+

Installing Yesod will give you both the Yesod library, as well as a yesod +executable. This executable accepts a few commands, but the first one you’ll +want to be acquainted with is yesod init. It will ask you some questions, and +then generate a folder containing the default scaffolded site. Inside that +folder, you can run cabal install --only-dependencies to build any extra +dependencies (such as your database backends), and then yesod devel to run +your site.

+

The scaffolded site gives you a lot of best practices out of the box, setting +up files and dependencies in a time-tested approach used by most production +Yesod sites. However, all this convenience can get in the way of actually +learning Yesod. Therefore, most of this book will avoid the scaffolding tool, +and instead deal directly with Yesod as a library. But if you’re going to build +a real site, I strongly recommend using the scaffolding.

+

We will cover the structure of the scaffolded site in the scaffolding chapter.

+
+
+

Development server

+

One of the advantages interpreted languages have over compiled languages is +fast prototyping: you save changes to a file and hit refresh. If we want to +make any changes to our Yesod apps above, we’ll need to call runhaskell from +scratch, which can be a bit tedious.

+

Fortunately, there’s a solution to this: yesod devel automatically rebuilds +and reloads your code for you. This can be a great way to develop your Yesod +projects, and when you’re ready to move to production, you still get to compile +down to incredibly efficient code. The Yesod scaffolding automatically sets +things up for you. This gives you the best of both worlds: rapid prototyping +and fast production code.

+

It’s a little bit more involved to set up your code to be used by yesod +devel, so our examples will just use warp. Fortunately, the scaffolded site +is fully configured to use the development server, so when you’re ready to move +over to the real world, it will be waiting for you.

+
+
+

Summary

+

Every Yesod application is built around a foundation datatype. We associate +some resources with that datatype and define some handler functions, and Yesod +handles all of the routing. These resources are also data constructors, which +lets us have type-safe URLs.

+

By being built on top of WAI, Yesod applications can run with a number of +different backends. For simple apps, the warp function provides a convenient +way to use the Warp web server. For rapid development, using yesod devel is a +good choice. And when you’re ready to move to production, you have the full +power and flexibility to configure Warp (or any other WAI handler) to suit your +needs.

+

When developing in Yesod, we get a number of choices for coding style: +quasi-quotation or external files, warp or yesod devel, and so on. The +examples in this book will tend towards using the choices that are easiest to +copy-and-paste, but the more powerful options will be available when you start +building real Yesod applications.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/blog-example-advanced.html b/public/book-1.2/blog-example-advanced.html new file mode 100644 index 00000000..6bda7b98 --- /dev/null +++ b/public/book-1.2/blog-example-advanced.html @@ -0,0 +1,499 @@ + Blog: i18n, authentication, authorization, and database :: Yesod Web Framework Book- Version 1.2 +
+

Blog: i18n, authentication, authorization, and database

+ + +

This is a simple blog app. It allows an admin to add blog posts via a rich text +editor (nicedit), allows logged-in users to comment, and has full i18n support. +It is also a good example of using a Persistent database, leveraging Yesod’s +authorization system, and templates.

+

While in general we recommend placing templates, Persist entity definitions, +and routing in separate files, we’ll keep it all in one file here for +convenience. The one exception you’ll see below will be i18n messages.

+

We’ll start off with our language extensions. In scaffolded code, the language +extensions are specified in the cabal file, so you won’t need to put this in +your individual Haskell files.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, GADTs, FlexibleContexts,
+             MultiParamTypeClasses, DeriveDataTypeable #-}
+

Now our imports.

+
import Yesod
+import Yesod.Auth
+import Yesod.Form.Nic (YesodNic, nicHtmlField)
+import Yesod.Auth.BrowserId (authBrowserId, def)
+import Data.Text (Text)
+import Network.HTTP.Client (defaultManagerSettings)
+import Network.HTTP.Conduit (Manager, newManager)
+import Database.Persist.Sqlite
+    ( ConnectionPool, SqlPersistT, runSqlPool, runMigration
+    , createSqlitePool, runSqlPersistMPool
+    )
+import Data.Time (UTCTime, getCurrentTime)
+import Control.Applicative ((<$>), (<*>), pure)
+import Data.Typeable (Typeable)
+

First we’ll set up our Persistent entities. We’re going to both create our data +types (via mkPersist) and create a migration function, which will automatically +create and update our SQL schema. If you were using the MongoDB backend, +migration would not be needed.

+
share [mkPersist sqlOnlySettings, mkMigrate "migrateAll"] [persistLowerCase|
+

Keeps track of users. In a more robust application, we would also keep account +creation date, display name, etc.

+
User
+   email Text
+   UniqueUser email
+

In order to work with yesod-auth’s caching, our User type must be an instance +of Typeable.

+
   deriving Typeable
+

An individual blog entry (I’ve avoided using the word "post" due to the +confusion with the request method POST).

+
Entry
+   title Text
+   posted UTCTime
+   content Html
+

And a comment on the blog post.

+
Comment
+   entry EntryId
+   posted UTCTime
+   user UserId
+   name Text
+   text Textarea
+|]
+

Every site has a foundation datatype. This value is initialized before +launching your application, and is available throughout. We’ll store a database +connection pool and HTTP connection manager in ours. See the very end of this +file for how those are initialized.

+
data Blog = Blog
+   { connPool    :: ConnectionPool
+   , httpManager :: Manager
+   }
+

To make i18n easy and translator friendly, we have a special file format for +translated messages. There is a single file for each language, and each file is +named based on the language code (e.g., en, es, de-DE) and placed in that +folder. We also specify the main language file (here, "en") as a default +language.

+
mkMessage "Blog" "blog-messages" "en"
+

Our blog-messages/en.msg file contains the following content:

+
-- @blog-messages/en.msg
+NotAnAdmin: You must be an administrator to access this page.
+
+WelcomeHomepage: Welcome to the homepage
+SeeArchive: See the archive
+
+NoEntries: There are no entries in the blog
+LoginToPost: Admins can login to post
+NewEntry: Post to blog
+NewEntryTitle: Title
+NewEntryContent: Content
+
+PleaseCorrectEntry: Your submitted entry had some errors, please correct and try again.
+EntryCreated title@Text: Your new blog post, #{title}, has been created
+
+EntryTitle title@Text: Blog post: #{title}
+CommentsHeading: Comments
+NoComments: There are no comments
+AddCommentHeading: Add a Comment
+LoginToComment: You must be logged in to comment
+AddCommentButton: Add comment
+
+CommentName: Your display name
+CommentText: Comment
+CommentAdded: Your comment has been added
+PleaseCorrectComment: Your submitted comment had some errors, please correct and try again.
+
+HomepageTitle: Yesod Blog Demo
+BlogArchiveTitle: Blog Archive
+

Now we’re going to set up our routing table. We have four entries: a homepage, +an entry list page (BlogR), an individual entry page (EntryR) and our +authentication subsite. Note that BlogR and EntryR both accept GET and POST +methods. The POST methods are for adding a new blog post and adding a new +comment, respectively.

+
mkYesod "Blog" [parseRoutes|
+/              HomeR  GET
+/blog          BlogR  GET POST
+/blog/#EntryId EntryR GET POST
+/auth          AuthR  Auth getAuth
+|]
+

Every foundation needs to be an instance of the Yesod typeclass. This is where +we configure various settings.

+
instance Yesod Blog where
+

The base of our application. Note that in order to make BrowserID work +properly, this must be a valid URL.

+
    approot = ApprootStatic "http://localhost:3000"
+

Our authorization scheme. We want to have the following rules:

+
    +
  • +

    +Only admins can add a new entry. +

    +
  • +
  • +

    +Only logged in users can add a new comment. +

    +
  • +
  • +

    +All other pages can be accessed by anyone. +

    +
  • +
+

We set up our routes in a RESTful way, where the actions that could make +changes are always using a POST method. As a result, we can simply check for +whether or not a request is a write request, given by the True in the second +field.

+

First, we’ll authorize requests to add a new entry.

+
    isAuthorized BlogR True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just (Entity _ user)
+                | isAdmin user -> return Authorized
+                | otherwise    -> unauthorizedI MsgNotAnAdmin
+

Now we’ll authorize requests to add a new comment.

+
    isAuthorized (EntryR _) True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just _  -> return Authorized
+

And for all other requests, the result is always authorized.

+
    isAuthorized _ _ = return Authorized
+

Where a user should be redirected to if they get an AuthenticationRequired.

+
    authRoute _ = Just (AuthR LoginR)
+

This is where we define our site look-and-feel. The function is given the +content for the individual page, and wraps it up with a standard template.

+
    defaultLayout inside = do
+

Yesod encourages the get-following-post pattern, where after a POST, the user +is redirected to another page. In order to allow the POST page to give the user +some kind of feedback, we have the getMessage and setMessage functions. It’s a +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 +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 +CSS for all pages. We’ll use the Lucius template language to create the latter.

+
        pc <- widgetToPageContent $ do
+            toWidget [lucius|
+body {
+    width: 760px;
+    margin: 1em auto;
+    font-family: sans-serif;
+}
+textarea {
+    width: 400px;
+    height: 200px;
+}
+#message {
+  color: #900;
+}
+|]
+            inside
+

And finally we’ll use a new Hamlet template to wrap up the individual +components (title, head data and body data) into the final output.

+
        giveUrlRenderer [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <div #message>#{msg}
+        ^{pageBody pc}
+|]
+

This is a simple function to check if a user is the admin. In a real +application, we would likely store the admin bit in the database itself, or +check with some external system. For now, I’ve just hard-coded my own email +address.

+
isAdmin :: User -> Bool
+isAdmin user = userEmail user == "michael@snoyman.com"
+

In order to access the database, we need to create a YesodPersist instance, +which says which backend we’re using and how to run an action.

+
instance YesodPersist Blog where
+   type YesodPersistBackend Blog = SqlPersistT
+   runDB f = do
+       master <- getYesod
+       let pool = connPool master
+       runSqlPool f pool
+

This is a convenience synonym. It is defined automatically for you in the +scaffolding.

+
type Form x = Html -> MForm Handler (FormResult x, Widget)
+

In order to use yesod-form and yesod-auth, we need an instance of RenderMessage +for FormMessage. This allows us to control the i18n of individual form +messages.

+
instance RenderMessage Blog FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

In order to use the built-in nic HTML editor, we need this instance. We just +take the default values, which use a CDN-hosted version of Nic.

+
instance YesodNic Blog
+

In order to use yesod-auth, we need a YesodAuth instance.

+
instance YesodAuth Blog where
+    type AuthId Blog = UserId
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    authHttpManager = httpManager
+

We’ll use BrowserID (a.k.a. Mozilla Persona), +which is a third-party system using email addresses as your identifier. This +makes it easy to switch to other systems in the future, locally authenticated +email addresses (also included with yesod-auth).

+
    authPlugins _ = [authBrowserId def]
+

This function takes someone’s login credentials (i.e., his/her email address) +and gives back a UserId.

+
    getAuthId creds = do
+        let email = credsIdent creds
+            user = User email
+        res <- runDB $ insertBy user
+        return $ Just $ either entityKey id res
+

Homepage handler. The one important detail here is our usage of setTitleI, +which allows us to use i18n messages for the title. We also use this message +with a _{Msg…} interpolation in Hamlet.

+
getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitleI MsgHomepageTitle
+    [whamlet|
+<p>_{MsgWelcomeHomepage}
+<p>
+   <a href=@{BlogR}>_{MsgSeeArchive}
+|]
+

Define a form for adding new entries. We want the user to provide the title and +content, and then fill in the post date automatically via getCurrentTime.

+

Note that slightly strange lift (liftIO getCurrentTime) manner of running an +IO action. The reason is that applicative forms are not monads, and therefore +cannot be instances of MonadIO. Instead, we use lift to run the action in +the underlying Handler monad, and liftIO to convert the IO action into a +Handler action.

+
entryForm :: Form Entry
+entryForm = renderDivs $ Entry
+    <$> areq textField (fieldSettingsLabel MsgNewEntryTitle) Nothing
+    <*> lift (liftIO getCurrentTime)
+    <*> areq nicHtmlField (fieldSettingsLabel MsgNewEntryContent) Nothing
+

Get the list of all blog entries, and present an admin with a form to create a +new entry.

+
getBlogR :: Handler Html
+getBlogR = do
+    muser <- maybeAuth
+    entries <- runDB $ selectList [] [Desc EntryPosted]
+    (entryWidget, enctype) <- generateFormPost entryForm
+    defaultLayout $ do
+        setTitleI MsgBlogArchiveTitle
+        [whamlet|
+$if null entries
+    <p>_{MsgNoEntries}
+$else
+    <ul>
+        $forall Entity entryId entry <- entries
+            <li>
+                <a href=@{EntryR entryId}>#{entryTitle entry}
+

We have three possibilities: the user is logged in as an admin, the user is +logged in and is not an admin, and the user is not logged in. In the first +case, we should display the entry form. In the second, we’ll do nothing. In the +third, we’ll provide a login link.

+
$maybe Entity _ user <- muser
+    $if isAdmin user
+        <form method=post enctype=#{enctype}>
+            ^{entryWidget}
+            <div>
+                <input type=submit value=_{MsgNewEntry}>
+$nothing
+    <p>
+        <a href=@{AuthR LoginR}>_{MsgLoginToPost}
+|]
+

Process an incoming entry addition. We don’t do any permissions checking, since +isAuthorized handles it for us. If the form submission was valid, we add the +entry to the database and redirect to the new entry. Otherwise, we ask the user +to try again.

+
postBlogR :: Handler Html
+postBlogR = do
+    ((res, entryWidget), enctype) <- runFormPost entryForm
+    case res of
+        FormSuccess entry -> do
+            entryId <- runDB $ insert entry
+            setMessageI $ MsgEntryCreated $ entryTitle entry
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectEntry
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{entryWidget}
+    <div>
+        <input type=submit value=_{MsgNewEntry}>
+|]
+

A form for comments, very similar to our entryForm above. It takes the +EntryId of the entry the comment is attached to. By using pure, we embed +this value in the resulting Comment output, without having it appear in the +generated HTML.

+
commentForm :: EntryId -> Form Comment
+commentForm entryId = renderDivs $ Comment
+    <$> pure entryId
+    <*> lift (liftIO getCurrentTime)
+    <*> lift requireAuthId
+    <*> areq textField (fieldSettingsLabel MsgCommentName) Nothing
+    <*> areq textareaField (fieldSettingsLabel MsgCommentText) Nothing
+

Show an individual entry, comments, and an add comment form if the user is +logged in.

+
getEntryR :: EntryId -> Handler Html
+getEntryR entryId = do
+    (entry, comments) <- runDB $ do
+        entry <- get404 entryId
+        comments <- selectList [CommentEntry ==. entryId] [Asc CommentPosted]
+        return (entry, map entityVal comments)
+    muser <- maybeAuth
+    (commentWidget, enctype) <-
+        generateFormPost (commentForm entryId)
+    defaultLayout $ do
+        setTitleI $ MsgEntryTitle $ entryTitle entry
+        [whamlet|
+<h1>#{entryTitle entry}
+<article>#{entryContent entry}
+    <section .comments>
+        <h1>_{MsgCommentsHeading}
+        $if null comments
+            <p>_{MsgNoComments}
+        $else
+            $forall Comment _entry posted _user name text <- comments
+                <div .comment>
+                    <span .by>#{name}
+                    <span .at>#{show posted}
+                    <div .content>#{text}
+        <section>
+            <h1>_{MsgAddCommentHeading}
+            $maybe _ <- muser
+                <form method=post enctype=#{enctype}>
+                    ^{commentWidget}
+                    <div>
+                        <input type=submit value=_{MsgAddCommentButton}>
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>_{MsgLoginToComment}
+|]
+

Receive an incoming comment submission.

+
postEntryR :: EntryId -> Handler Html
+postEntryR entryId = do
+    ((res, commentWidget), enctype) <-
+        runFormPost (commentForm entryId)
+    case res of
+        FormSuccess comment -> do
+            _ <- runDB $ insert comment
+            setMessageI MsgCommentAdded
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectComment
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{commentWidget}
+    <div>
+        <input type=submit value=_{MsgAddCommentButton}>
+|]
+

Finally our main function.

+
main :: IO ()
+main = do
+    pool <- createSqlitePool "blog.db3" 10 -- create a new pool
+    -- perform any necessary migration
+    runSqlPersistMPool (runMigration migrateAll) pool
+    manager <- newManager defaultManagerSettings -- create a new HTTP manager
+    warp 3000 $ Blog pool manager -- start our server
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/case-study-sphinx.html b/public/book-1.2/case-study-sphinx.html new file mode 100644 index 00000000..3922e4e0 --- /dev/null +++ b/public/book-1.2/case-study-sphinx.html @@ -0,0 +1,786 @@ + Case Study: Sphinx-based Search :: Yesod Web Framework Book- Version 1.2 +
+

Case Study: Sphinx-based Search

+ + +

Sphinx is a search server, and powers the +search feature on many sites. While the actual code necessary to integrate +Yesod with Sphinx is relatively short, it touches on a number of complicated +topics, and is therefore a great case study in how to play with some of the +under-the-surface details of Yesod.

+

There are essentially three different pieces at play here:

+
    +
  • +

    +Storing the content we wish to search. This is fairly straight-forward + Persistent code, and we won’t dwell on it much in this chapter. +

    +
  • +
  • +

    +Accessing Sphinx search results from inside Yesod. Thanks to the sphinx + package, this is actually very easy. +

    +
  • +
  • +

    +Providing the document content to Sphinx. This is where the interesting stuff + happens, and will show how to deal with streaming content from a database + directly to XML, which gets sent directly over the wire to the client. +

    +
  • +
+

The full code for this example can be +found +on FP Haskell Center.

+
+

Sphinx Setup

+

Unlike many of our other examples, to start with here we’ll need to actually +configure and run our external Sphinx server. I’m not going to go into all the +details of Sphinx, partly because it’s not relevant to our point here, and +mostly because I’m not an expert on Sphinx.

+

Sphinx provides three main command line utilities: searchd is the actual +search daemon that receives requests from the client (in this case, our web +app) and returns the search results. indexer parses the set of documents and +creates the search index. search is a debugging utility that will run simple +queries against Sphinx.

+

There are two important settings: the source and the index. The source tells +Sphinx where to read document information from. It has direct support for MySQL +and PostgreSQL, as well as a more general XML format known as xmlpipe2. We’re +going to use the last one. This not only will give us more flexibility with +choosing Persistent backends, but will also demonstrate some more powerful +Yesod concepts.

+

The second setting is the index. Sphinx can handle multiple indices +simultaneously, which allows it to provide search for multiple services at +once. Each index will have a source it pulls from.

+

In our case, we’re going to provide a URL from our application +(/search/xmlpipe) that provides the XML file required by Sphinx, and then pipe +that through to the indexer. So we’ll add the following to our Sphinx config +file:

+
source searcher_src
+{
+        type = xmlpipe2
+        xmlpipe_command = curl http://localhost:3000/search/xmlpipe
+}
+
+index searcher
+{
+        source = searcher_src
+        path = /var/data/searcher
+        docinfo = extern
+        charset_type = utf-8
+}
+
+searchd
+{
+        listen                  = 9312
+        pid_file                = /var/run/sphinxsearch/searchd.pid
+}
+

In order to build your search index, you would run indexer searcher. +Obviously this won’t work until you have your web app running. For a production +site, it would make sense to run this command via a crontab script so the index +is regularly updated.

+
+
+

Basic Yesod Setup

+

Let’s get our basic Yesod setup going. We’re going to have a single table in +the database for holding documents, which consist of a title and content. We’ll +store this in a SQLite database, and provide routes for searching, adding +documents, viewing documents and providing the xmlpipe file to Sphinx.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlPersistT
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where -- see below
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

Hopefully all of this looks pretty familiar by now. The one new thing we’ve +defined here is an instance of YesodPersistRunner. This is a typeclass +necessary for creating streaming database responses. The default implementation +(defaultGetDBRunner) is almost always appropriate.

+

Next we’ll define some forms: one for creating documents, and one for searching:

+
addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+

The True parameter to searchField makes the field auto-focus on page load. +Finally, we have some standard handlers for the homepage (shows the add +document form and the search form), the document display, and adding a +document.

+
getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+
+

Searching

+

Now that we’ve got the boring stuff out of the way, let’s jump into the actual +searching. We’re going to need three pieces of information for displaying a +result: the document ID it comes from, the title of that document, and the +excerpts. Excerpts are the highlighted portions of the document which contain +the search term.

+

Search Result

+ + + + + + +
+

So let’s start off by defining a Result datatype:

+
data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+

Next we’ll look at the search handler:

+
getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+

Nothing magical here, we’re just relying on the searchForm defined above, and +the getResults function which hasn’t been defined yet. This function just +takes a search string, and returns a list of results. This is where we first +interact with the Sphinx API. We’ll be using two functions: query will return +a list of matches, and buildExcerpts will return the highlighted excerpts. +Let’s first look at getResults:

+
getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" $ T.unpack qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (Key . PersistInt64 . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+

query takes three parameters: the configuration options, the index to search +against (searcher in this case) and the search string. It returns a list of +document IDs that contain the search string. The tricky bit here is that those +documents are returned as Int64 values, whereas we need DocIds. We’re +taking advantage of the fact that the SQL Persistent backends use a +PersistInt64 constructor for their IDs, and simply wrap up the values +appropriately.

+ +

We then loop over the resulting IDs to get a [Maybe Result] value, and use +catMaybes to turn it into a [Result]. In the where clause, we define our +local settings, which override the default port and set up the search to work +when any term matches the document.

+

Let’s finally look at the getResult function:

+
getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [T.unpack $ escape $ docContent doc]
+        "searcher"
+        (T.unpack qstring)
+    let excerpt =
+            case excerpt' of
+                ST.Ok bss -> preEscapedToHtml $ decodeUtf8 $ mconcat bss
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+

buildExcerpts takes four parameters: the configuration options, the textual +contents of the document, the search index and the search term. The interesting +bit is that we entity escape the text content. Sphinx won’t automatically +escape these for us, so we must do it explicitly.

+

Similarly, the result from Sphinx is a list of Texts. But of course, we’d +rather have Html. So we concat that list into a single Text and use +preEscapedToHtml to make sure that the tags inserted for matches are not +escaped. A sample of this HTML is:

+
&#8230; Departments.  The President shall have <span class='match'>Power</span> to fill up all Vacancies
+&#8230;  people. Amendment 11 The Judicial <span class='match'>power</span> of the United States shall
+&#8230; jurisdiction. 2. Congress shall have <span class='match'>power</span> to enforce this article by
+&#8230; 5. The Congress shall have <span class='match'>power</span> to enforce, by appropriate legislation
+&#8230;
+
+
+

Streaming xmlpipe output

+

We’ve saved the best for last. For the majority of Yesod handlers, the +recommended approach is to load up the database results into memory and then +produce the output document based on that. It’s simpler to work with, but more +importantly it’s more resilient to exceptions. If there’s a problem loading the +data from the database, the user will get a proper 500 response code.

+ +

However, generating the xmlpipe output is a perfect example of the alternative. +There are potentially a huge number of documents, and documents could easily be +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: +a content type, and a conduit Source providing a stream of blaze-builder +Builders. Yesod that handles all of the issues of grabbing a database +connection from the connection pool, starting a transaction, and streaming the +response to the user.

+

Now we know we want to create a stream of Builders from some XML content. +Fortunately, the xml-conduit package provides this interface directly. +xml-conduit provides some high-level interfaces for dealing with documents as +a whole, but in our case, we’re going to need to use the low-level Event +interface to ensure minimal memory impact. So the function we’re interested in +is:

+
renderBuilder :: Monad m => RenderSettings -> Conduit Event m Builder
+

In plain English, that means renderBuilder takes some settings (we’ll just use +the defaults), and will then convert a stream of Events to a stream of +Builders. This is looking pretty good, all we need now is a stream of +Events.

+

Speaking of which, what should our XML document actually look like? It’s pretty +simple, we have a sphinx:docset root element, a sphinx:schema element +containing a single sphinx:field (which defines the content field), and then +a sphinx:document for each document in our database. That last element will +have an id attribute and a child content element. Below is an example of +such a document:

+
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">
+    <sphinx:schema>
+        <sphinx:field name="content"/>
+    </sphinx:schema>
+    <sphinx:document id="1">
+        <content>bar</content>
+    </sphinx:document>
+    <sphinx:document id="2">
+        <content>foo bar baz</content>
+    </sphinx:document>
+</sphinx:docset>
+

Every document is going to start off with the same events (start the docset, +start the schema, etc) and end with the same event (end the docset). We’ll +start off by defining those:

+
toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+

Now that we have the shell of our document, we need to get the Events for +each individual document. This is actually a fairly simple function:

+
entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+

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.

+

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 +also provides the (more powerful) selectSource function. So we end up with +the function:

+
docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+

The $= operator joins together a source and a conduit into a new source. 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 +piece of cake:

+
fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+

Now we need to tie it together in getXmlpipeR. To do so, we’ll use the respondSourceDB function mentioned earlier. The last trick we need to do is convert our stream of Events into a stream of Chunk Builders. Converting to a stream of Builders is achieved with renderBuilder, and finally we’ll just wrap each Builder in its own Chunk:

+
getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+
+

Full code

+
{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE GADTs                 #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+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)
+import qualified Text.Search.Sphinx                      as S
+import qualified Text.Search.Sphinx.ExcerptConfiguration as E
+import qualified Text.Search.Sphinx.Types                as ST
+import           Text.XML.Stream.Render                  (def, renderBuilder)
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlPersistT
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+
+getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [T.unpack $ escape $ docContent doc]
+        "searcher"
+        (T.unpack qstring)
+    let excerpt =
+            case excerpt' of
+                ST.Ok bss -> preEscapedToHtml $ decodeUtf8 $ mconcat bss
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+
+getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" $ T.unpack qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (Key . PersistInt64 . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+
+getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+
+fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+
+docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+
+toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+
+main :: IO ()
+main = withSqlitePool "searcher.db3" 10 $ \pool -> do
+    runStdoutLoggingT $ runSqlPool (runMigration migrateAll) pool
+    warp 3000 $ Searcher pool
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/conduits.html b/public/book-1.2/conduits.html new file mode 100644 index 00000000..a366f67c --- /dev/null +++ b/public/book-1.2/conduits.html @@ -0,0 +1,111 @@ + Conduit :: Yesod Web Framework Book- Version 1.2 +
+ +
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/creating-a-subsite.html b/public/book-1.2/creating-a-subsite.html new file mode 100644 index 00000000..8b1f4573 --- /dev/null +++ b/public/book-1.2/creating-a-subsite.html @@ -0,0 +1,223 @@ + Creating a Subsite :: Yesod Web Framework Book- Version 1.2 +
+

Creating a Subsite

+ + +

How many sites provide authentication systems? Or need to provide CRUD (CRUD) +management of some objects? Or a blog? Or a wiki?

+

The theme here is that many websites include common components that can be +reused throughout multiple sites. However, it is often quite difficult to get +code to be modular enough to be truly plug-and-play: a component will require +hooks into the routing system, usually for multiple routes, and will need some +way of sharing styling information with the master site.

+

In Yesod, the solution is subsites. A subsite is a collection of routes and +their handlers that can be easily inserted into a master site. By using type +classes, it is easy to ensure that the master site provides certain +capabilities, and to access the default site layout. And with type-safe URLs, +it’s easy to link from the master site to subsites.

+
+

Hello World

+

Perhaps the trickiest part of writing subsites is getting started. Let’s dive +in with a simple Hello World subsite. We need to create one module to contain +our subsite’s data types, another for the subsite’s dispatch code, and then a +final module for an application that uses the subsite.

+ +
-- @HelloSub/Data.hs
+{-# LANGUAGE QuasiQuotes     #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies    #-}
+module HelloSub.Data where
+
+import           Yesod
+
+-- Subsites have foundations just like master sites.
+data HelloSub = HelloSub
+
+-- We have a familiar analogue from mkYesod, with just one extra parameter.
+-- We'll discuss that later.
+mkYesodSubData "HelloSub" [parseRoutes|
+/ SubHomeR GET
+|]
+
-- @HelloSub.hs
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+module HelloSub
+    ( module HelloSub.Data
+    , module HelloSub
+    ) where
+
+import           HelloSub.Data
+import           Yesod
+
+-- And we'll spell out the handler type signature.
+getSubHomeR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
+getSubHomeR = lift $ defaultLayout [whamlet|Welcome to the subsite!|]
+
+instance Yesod master => YesodSubDispatch HelloSub (HandlerT master IO) where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesHelloSub)
+
{-# LANGUAGE QuasiQuotes     #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies    #-}
+import           HelloSub
+import           Yesod
+
+-- And let's create a master site that calls it.
+data Master = Master
+    { getHelloSub :: HelloSub
+    }
+
+mkYesod "Master" [parseRoutes|
+/ HomeR GET
+/subsite SubsiteR HelloSub getHelloSub
+|]
+
+instance Yesod Master
+
+-- Spelling out type signature again.
+getHomeR :: HandlerT Master IO Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>Welcome to the homepage
+        <p>
+            Feel free to visit the #
+            <a href=@{SubsiteR SubHomeR}>subsite
+            \ as well.
+    |]
+
+main = warp 3000 $ Master HelloSub
+

This simple example actually shows most of the complications involved in +creating a subsite. Like a normal Yesod application, everything in a subsite is +centered around a foundation datatype, HelloSub in our case. We then use +mkYesodSubData to generate our subsite route data type and associated parse +and render functions.

+

On the dispatch side, we start off by defining our handler function for the SubHomeR route. You should pay special attention to the type signature on this function:

+
getSubHomeR :: Yesod master
+            => HandlerT HelloSub (HandlerT master IO) Html
+

This is the heart and soul of what a subsite is all about. All of our actions +live in this layered monad, where we have our subsite wrapping around our main +site. Given this monadic layering, it should come as no surprise that we end up +calling lift. In this case, our subsite is using the master site’s +defaultLayout function to render a widget.

+

The defaultLayout function is part of the Yesod typeclass. Therefore, in +order to call it, the master type argument must be an instance of Yesod. +The advantage of this approach is that any modifications to the master site’s +defaultLayout method will automatically be reflected in subsites.

+

When we embed a subsite in our master site route definition, we need to specify +four pieces of information: the route to use as the base of the subsite (in +this case, /subsite), the constructor for the subsite routes (SubsiteR), +the subsite foundation data type (HelloSub) and a function that takes a +master foundation value and returns a subsite foundation value (getHelloSub).

+

In the definition of getHomeR, we can see how the route constructor gets used. +In a sense, SubsiteR promotes any subsite route to a master site route, +making it possible to safely link to it from any master site template.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/deploying-your-webapp.html b/public/book-1.2/deploying-your-webapp.html new file mode 100644 index 00000000..f2aba1e5 --- /dev/null +++ b/public/book-1.2/deploying-your-webapp.html @@ -0,0 +1,489 @@ + Deploying your Webapp :: Yesod Web Framework Book- Version 1.2 +
+

Deploying your Webapp

+ + +

I can’t speak for others, but I personally prefer programming to system +administration. But the fact is that, eventually, you need to serve your app +somehow, and odds are that you’ll need to be the one to set it up.

+

There are some promising initiatives in the Haskell web community towards +making deployment easier. In the future, we may even have a service that allows +you to deploy your app with a single command.

+

But we’re not there yet. And even if we were, such a solution will never work +for everyone. This chapter covers the different options you have for +deployment, and gives some general recommendations on what you should choose in +different situations.

+ +
+

Compiling

+

First things first: how do you build your production application? If you’re +using the scaffolded site, it’s as simple as cabal build. I also recommend +cleaning beforehand to make sure there is no cached information, so a simple +combination to build your executable is:

+
cabal clean && cabal configure && cabal build
+
+
+

Files to deploy

+

With a Yesod scaffolded application, there are essentially three sets of files that need +to be deployed:

+
    +
  1. +

    +Your executable. +

    +
  2. +
  3. +

    +The config folder. +

    +
  4. +
  5. +

    +The static folder. +

    +
  6. +
+

Everything else, such as Shakespearean templates, gets compiled into the +executable itself.

+

There is one caveat, however: the config/client_session_key.aes file. This +file controls the server side encryption used for securing client side session +cookies. Yesod will automatically generate a new one of these keys if none is +present. In practice, this means that, if you do not include this file in +deployment, all of your users will have to log in again when you redeploy. If +you follow the advice above and include the config folder, this issue will be +partially resolved.

+

The other half of the resolution is to ensure that once you generate a +config/client_session_key.aes file, you keep the same one for all future +deployments. The simplest way to ensure this is to keep that file in your +version control. However, if your version control is open source, this will be +dangerous: anyone with access to your repository will be able to spoof login +credentials!

+

The problem described here is essentially one of system administration, not +programming. Yesod does not provide any built-in approach for securely storing +client session keys. If you have an open source repository, or do not trust +everyone who has access to your source code repository, it’s vital to figure +out a safe storage solution for the client session key.

+
+
+

Warp

+

As we have mentioned before, Yesod is built on the Web Application Interface +(WAI), allowing it to run on any WAI backend. At the time of writing, the +following backends are available:

+
    +
  • +

    +Warp +

    +
  • +
  • +

    +FastCGI +

    +
  • +
  • +

    +SCGI +

    +
  • +
  • +

    +CGI +

    +
  • +
  • +

    +Webkit +

    +
  • +
  • +

    +Development server +

    +
  • +
+

The last two are not intended for production deployments. Of the remaining +four, all can be used for production deployment in theory. In practice, a CGI +backend will likely be horribly inefficient, since a new process must be +spawned for each connection. And SCGI is not nearly as well supported by +frontend web servers as Warp (via reverse proxying) or FastCGI.

+

So between the two remaining choices, Warp gets a very strong recommendation +because:

+
    +
  • +

    +It is significantly faster. +

    +
  • +
  • +

    +Like FastCGI, it can run behind a frontend server like Nginx, using reverse + HTTP proxy. +

    +
  • +
  • +

    +In addition, it is a fully capable server of its own accord, and can + therefore be used without any frontend server. +

    +
  • +
+

So that leaves one last question: should Warp run on its own, or via reverse +proxy behind a frontend server? For most use cases, I recommend the latter, +because:

+
    +
  • +

    +As fast as Warp is, it is still optimized as an application server, not a + static file server. +

    +
  • +
  • +

    +Using Nginx, you can set up virtual hosting to serve your static contents + from a separate domain. (It’s possible to do this with Warp, but a bit more + involved). +

    +
  • +
  • +

    +You can use Nginx as either a load balancer or a SSL proxy. (Though with + warp-tls it’s entirely possible to run an https site on Warp alone.) +

    +
  • +
+

So my final recommendation is: set up Nginx to reverse proxy to Warp.

+ +
+

Configuration

+

In general, Nginx will listen on port 80 and your Yesod/Warp app will listen on +some unprivileged port (lets say 4321). You will then need to provide a +nginx.conf file, such as:

+
daemon off; # Don't run nginx in the background, good for monitoring apps
+events {
+    worker_connections 4096;
+}
+
+http {
+    server {
+        listen 80; # Incoming port for Nginx
+        server_name www.myserver.com;
+        location / {
+            proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+        }
+    }
+}
+

You can add as many server blocks as you like. A common addition is to ensure +users always access your pages with the www prefix on the domain name, ensuring +the RESTful principle of canonical URLs. (You could just as easily do the +opposite and always strip the www, just make sure that your choice is reflected +in both the nginx config and the approot of your site.) In this case, we would +add the block:

+
server {
+    listen 80;
+    server_name myserver.com;
+    rewrite ^/(.*) http://www.myserver.com/$1 permanent;
+}
+

A highly recommended optimization is to serve static files from a separate +domain name, therefore bypassing the cookie transfer overhead. Assuming that +our static files are stored in the static folder within our site folder, and +the site folder is located at /home/michael/sites/mysite, this would look +like:

+
server {
+    listen 80;
+    server_name static.myserver.com;
+    root /home/michael/sites/mysite/static;
+    # Since yesod-static appends a content hash in the query string,
+    # we are free to set expiration dates far in the future without
+    # concerns of stale content.
+    expires max;
+}
+

In order for this to work, your site must properly rewrite static URLs to this +alternate domain name. The scaffolded site is set up to make this fairly simple +via the Settings.staticRoot function and the definition of +urlRenderOverride. However, if you just want to get the benefit of nginx’s +faster static file serving without dealing with separate domain names, you can +instead modify your original server block like so:

+
server {
+    listen 80; # Incoming port for Nginx
+    server_name www.myserver.com;
+    location / {
+        proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+    }
+    location /static {
+        root /home/michael/sites/mysite; # Notice that we do *not* include /static
+        expires max;
+    }
+}
+
+
+

Server Process

+

Many people are familiar with an Apache/mod_php or Lighttpd/FastCGI kind of +setup, where the web server automatically spawns the web application. With +nginx, either for reverse proxying or FastCGI, this is not the case: you are +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.

+
+
+
+

FastCGI

+

Some people may prefer using FastCGI for deployment. In this case, you’ll need +to add an extra tool to the mix. FastCGI works by receiving new connection from +a file descriptor. The C library assumes that this file descriptor will be 0 +(standard input), so you need to use the spawn-fcgi program to bind your +application’s standard input to the correct socket.

+

It can be very convenient to use Unix named sockets for this instead of binding +to a port, especially when hosting multiple applications on a single host. A +possible script to load up your app could be:

+
spawn-fcgi \
+    -d /home/michael/sites/mysite \
+    -s /tmp/mysite.socket \
+    -n \
+    -M 511 \
+    -u michael \
+    -- /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi
+

You will also need to configure your frontend server to speak to your app over +FastCGI. This is relatively painless in Nginx:

+
server {
+    listen 80;
+    server_name www.myserver.com;
+    location / {
+        fastcgi_pass unix:/tmp/mysite.socket;
+    }
+}
+

That should look pretty familiar from above. The only last trick is that, with +Nginx, you need to manually specify all of the FastCGI variables. It is +recommended to store these in a separate file (say, fastcgi.conf) and then add +include fastcgi.conf; to the end of your http block. The contents of the +file, to work with WAI, should be:

+
fastcgi_param  QUERY_STRING       $query_string;
+fastcgi_param  REQUEST_METHOD     $request_method;
+fastcgi_param  CONTENT_TYPE       $content_type;
+fastcgi_param  CONTENT_LENGTH     $content_length;
+fastcgi_param  PATH_INFO          $fastcgi_script_name;
+fastcgi_param  SERVER_PROTOCOL    $server_protocol;
+fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
+fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
+fastcgi_param  REMOTE_ADDR        $remote_addr;
+fastcgi_param  SERVER_ADDR        $server_addr;
+fastcgi_param  SERVER_PORT        $server_port;
+fastcgi_param  SERVER_NAME        $server_name;
+
+
+

Desktop

+

Another nifty backend is wai-handler-webkit. This backend combines Warp and +QtWebkit to create an executable that a user simply double-clicks. This can be +a convenient way to provide an offline version of your application.

+

One of the very nice conveniences of Yesod for this is that your templates are +all compiled into the executable, and thus do not need to be distributed with +your application. Static files do, however.

+ +

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 +doesn’t receive a ping for two minutes, it shuts down.

+
+
+

CGI on Apache

+

CGI and FastCGI work almost identically on Apache, so it should be fairly +straight-forward to port this configuration. You essentially need to accomplish +two goals:

+
    +
  1. +

    +Get the server to serve your file as (Fast)CGI. +

    +
  2. +
  3. +

    +Rewrite all requests to your site to go through the (Fast)CGI executable. +

    +
  4. +
+

Here is a configuration file for serving a blog application, with an executable +named "bloggy.cgi", living in a subfolder named "blog" of the document root. +This example was taken from an application living in the path +/f5/snoyman/public/blog.

+
Options +ExecCGI
+AddHandler cgi-script .cgi
+Options +FollowSymlinks
+
+RewriteEngine On
+RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1]
+RewriteCond $1 !^bloggy.cgi
+RewriteCond $1 !^static/
+RewriteRule ^(.*) bloggy.cgi/$1 [L]
+

The first RewriteRule is to deal with subfolders. In particular, it redirects a +request for /blog to /blog/. The first RewriteCond prevents directly +requesting the executable, the second allows Apache to serve the static files, +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." +when absent and serves static files from a separate domain. However, this +should serve to show the basics.

+

Here, "/home/michael/fastcgi" is the fastcgi application. The idea is to +rewrite all requests to start with "/app", and then serve everything beginning +with "/app" via the FastCGI executable.

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_fastcgi", "mod_rewrite")
+
+url.rewrite-once = (
+  "(.*)" => "/app/$1"
+)
+
+fastcgi.server = (
+    "/app" => ((
+        "socket" => "/tmp/test.fastcgi.socket",
+        "check-local" => "disable",
+        "bin-path" => "/home/michael/fastcgi", # full path to executable
+        "min-procs" => 1,
+        "max-procs" => 30,
+        "idle-timeout" => 30
+    ))
+)
+
+
+

CGI on lighttpd

+

This is basically the same as the FastCGI version, but tells lighttpd to run a +file ending in ".cgi" as a CGI executable. In this case, the file lives at +"/home/michael/myapp.cgi".

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_cgi", "mod_rewrite")
+
+url.rewrite-once = (
+    "(.*)" => "/myapp.cgi/$1"
+)
+
+cgi.assign = (".cgi" => "")
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/environment-variables.html b/public/book-1.2/environment-variables.html new file mode 100644 index 00000000..bbb783bb --- /dev/null +++ b/public/book-1.2/environment-variables.html @@ -0,0 +1,172 @@ + Environment variables for configuration :: Yesod Web Framework Book- Version 1.2 +
+

Environment variables for configuration

+ + +

There’s a recent move, perhaps most prominently advocated by +the twelve-factor app, to store all app +configuration in environment variables, instead of using config files or +hard-coding them into the application source code (you don’t do that, right?).

+

Yesod’s scaffolding comes built in with some support for this, most +specifically for respecting the APPROOT environment variable to indicate how +URLs should be generated, the PORT environment variable for which port to +listen for requests on, and database connection settings. (Incidentally, this +ties in nicely with the Keter deployment +manager.)

+

The technique for doing this is quite easy: just do the environment variable +lookup in your main funciton. This example demonstrates this technique, along +with the slightly special handling necessary for setting the application root.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text          (Text, pack)
+import           System.Environment
+import           Yesod
+
+data App = App
+    { myApproot      :: Text
+    , welcomeMessage :: Text
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    approot = ApprootMaster myApproot
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    App {..} <- getYesod
+    setTitle "Environment variables"
+    [whamlet|
+        <p>Here's the welcome message: #{welcomeMessage}
+        <p>
+            <a href=@{HomeR}>And a link to: @{HomeR}
+    |]
+
+main :: IO ()
+main = do
+    myApproot <- fmap pack $ getEnv "APPROOT"
+    welcomeMessage <- fmap pack $ getEnv "WELCOME_MESSAGE"
+    warp 3000 App {..}
+

The only tricky things here are:

+
    +
  1. +

    +You need to convert the String value returned by getEnv into a Text by using pack. +

    +
  2. +
  3. +

    +We use the ApprootMaster constructor for approot, which says "apply this function to the foundation datatype to get the actual applicaiton root." +

    +
  4. +
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/forms.html b/public/book-1.2/forms.html new file mode 100644 index 00000000..0f0e74b4 --- /dev/null +++ b/public/book-1.2/forms.html @@ -0,0 +1,1113 @@ + Forms :: Yesod Web Framework Book- Version 1.2 +
+

Forms

+ + +

I’ve mentioned the boundary issue already: whenever data enters or leaves an +application, we need to validate it. Probably the most difficult place this +occurs is forms. Coding forms is complex; in an ideal world, we’d like a +solution that addresses the following problems:

+
    +
  • +

    +Ensure data is valid. +

    +
  • +
  • +

    +Marshal string data in the form submission to Haskell datatypes. +

    +
  • +
  • +

    +Generate HTML code for displaying the form. +

    +
  • +
  • +

    +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. +

    +
  • +
  • +

    +Automatically assign names to our fields that are guaranteed to be unique. +

    +
  • +
+

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 +Haskell’s type system to make sure everything is working correctly.

+
+

Synopsis

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative ((<$>), (<*>))
+import           Data.Text           (Text)
+import           Data.Time           (Day)
+import           Yesod
+import           Yesod.Form.Jquery
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/person PersonR POST
+|]
+
+instance Yesod App
+
+-- Tells our application to use the standard English messages.
+-- If you want i18n, then you can supply a translating function instead.
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- And tell us where to find the jQuery libraries. We'll just use the defaults,
+-- which point to the Google CDN.
+instance YesodJquery App
+
+-- The datatype we wish to receive from the form
+data Person = Person
+    { personName          :: Text
+    , personBirthday      :: Day
+    , personFavoriteColor :: Maybe Text
+    , personEmail         :: Text
+    , personWebsite       :: Maybe Text
+    }
+  deriving Show
+
+-- Declare the form. The type signature is a bit intimidating, but here's the
+-- overview:
+--
+-- * The Html parameter is used for encoding some extra information. See the
+-- discussion regarding runFormGet and runFormPost below for further
+-- explanation.
+--
+-- * We have our Handler as the inner monad, which indicates which site this is
+-- running in.
+--
+-- * FormResult can be in three states: FormMissing (no data available),
+-- FormFailure (invalid data) and FormSuccess
+--
+-- * The Widget is the viewable form to place into the web page.
+--
+-- Note that the scaffolded site provides a convenient Form type synonym,
+-- so that our signature could be written as:
+--
+-- > personForm :: Form Person
+--
+-- For our purposes, it's good to see the long version.
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm = renderDivs $ Person
+    <$> areq textField "Name" Nothing
+    <*> areq (jqueryDayField def
+        { jdsChangeYear = True -- give a year dropdown
+        , jdsYearRange = "1900:-5" -- 1900 till five years ago
+        }) "Birthday" Nothing
+    <*> aopt textField "Favorite color" Nothing
+    <*> areq emailField "Email address" Nothing
+    <*> aopt urlField "Website" Nothing
+
+-- The GET handler displays the form
+getHomeR :: Handler Html
+getHomeR = do
+    -- Generate the form to be displayed
+    (widget, enctype) <- generateFormPost personForm
+    defaultLayout
+        [whamlet|
+            <p>
+                The widget generated contains only the contents
+                of the form, not the form tag itself. So...
+            <form method=post action=@{PersonR} enctype=#{enctype}>
+                ^{widget}
+                <p>It also doesn't include the submit button.
+                <button>Submit
+        |]
+
+-- The POST handler processes the form. If it is successful, it displays the
+-- parsed person. Otherwise, it displays the form again with error messages.
+postPersonR :: Handler Html
+postPersonR = do
+    ((result, widget), enctype) <- runFormPost personForm
+    case result of
+        FormSuccess person -> defaultLayout [whamlet|<p>#{show person}|]
+        _ -> defaultLayout
+            [whamlet|
+                <p>Invalid input, let's try again.
+                <form method=post action=@{PersonR} enctype=#{enctype}>
+                    ^{widget}
+                    <button>Submit
+            |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Kinds of Forms

+

Before jumping into the types themselves, we should begin with an overview of +the different kinds of forms. There are three categories:

+
+
+Applicative +
+

+These are the most commonly used (it’s what appeared in the +synopsis). Applicative gives us some nice properties of letting error messages +coallesce together and keep a very high-level, declarative approach. (For more +information on applicative code, see +the Haskell +wiki.) +

+
+
+Monadic +
+

+A more powerful alternative to applicative. While this allows you +more flexibility, it does so at the cost of being more verbose. Useful if you +want to create forms that don’t fit into the standard two-column look. +

+
+
+Input +
+

+Used only for receiving input. Does not generate any HTML for receiving +the user input. Useful for interacting with existing forms. +

+
+
+

In addition, there are a number of different variables that come into play for +each form and field you will want to set up:

+
    +
  • +

    +Is the field required or optional? +

    +
  • +
  • +

    +Should it be submitted with GET or POST? +

    +
  • +
  • +

    +Does it have a default value, or not? +

    +
  • +
+

An overriding goal is to minimize the number of field definitions and let them +work in as many contexts as possible. One result of this is that we end up with +a few extra words for each field. In the synopsis, you may have noticed things +like areq and that extra Nothing parameter. We’ll cover why all of those +exist in the course of this chapter, but for now realize that by making these +parameters explicit, we are able to reuse the individuals fields (like +intField) in many different ways.

+

A quick note on naming conventions. Each form type has a one-letter prefix (A, +M and I) which is used in a few places, such as saying MForm. We also use req +and opt to mean required and optional. Combining these, we create a required +applicative field with areq, or an optional input field with iopt.

+
+
+

Types

+

The Yesod.Form.Types module declares a few types. We won’t cover all the types +available, but will instead focus on the most crucial. Let’s start with some of +the simple ones:

+
+
+Enctype +
+

+The encoding type, either UrlEncoded or Multipart. This datatype +declares an instance of ToHtml, so you can use the enctype directly in +Hamlet. +

+
+
+FormResult +
+

+Has one of three possible states: FormMissing if no data was +submitted, FormFailure if there was an error parsing the form (e.g., missing +a required field, invalid content), or FormSuccess if everything went +smoothly. +

+
+
+FormMessage +
+

+Represents all of the different messages that can be generated as +a data type. For example, MsgInvalidInteger is used by the library to +indicate that the textual value provided is not an integer. By keeping this +data highly structured, you are able to provide any kind of rendering function +you want, which allows for internationalization (i18n) of your application. +

+
+
+

Next we have some datatypes used for defining individual fields. We define a +field as a single piece of information, such as a number, a string, or an email +address. Fields are combined together to build forms.

+
+
+Field +
+

+Defines two pieces of functionality: how to parse the text input from a +user into a Haskell value, and how to create the widget to be displayed to the +user. yesod-form defines a number of individual Fields in Yesod.Form.Fields. +

+
+
+FieldSettings +
+

+Basic information on how a field should be displayed, such as +the display name, an optional tooltip, and possibly hardcoded id and name +attributes. (If none are provided, they are automatically generated.) Note that +FieldSettings provides an IsString instance, so when you need to provide a +FieldSettings value, you can actually type in a literal string. That’s how we +interacted with it in the synopsis. +

+
+
+

And finally, we get to the important stuff: the forms themselves. There are +three types for this: MForm is for monadic forms, AForm for applicative and +FormInput for input. MForm is actually a type synonym for a +monad stack that provides the following features:

+
    +
  • +

    +A Reader monad giving us the parameters submitted by the user, the + foundation datatype and the list of languages the user supports. The last two + are used for rendering of the FormMessages to support i18n (more on this + later). +

    +
  • +
  • +

    +A Writer monad keeping track of the Enctype. A form will always be + UrlEncoded, unless there is a file input field, which will force us to use + multipart instead. +

    +
  • +
  • +

    +A State monad keeping track of generated names and identifiers for fields. +

    +
  • +
+

An AForm is pretty similar. However, there are a few major differences:

+
    +
  • +

    +It produces a list of FieldViews, which are used for tracking what we + will display to the user. This allows us to keep an abstract idea of the form + display, and then at the end of the day choose an appropriate function for + laying it out on the page. In the synopsis, we used renderDivs, which + creates a bunch of div tags. Two other options are renderBootstrap and + renderTable. +

    +
  • +
  • +

    +It does not provide a Monad instance. The goal of Applicative is to allow + the entire form to run, grab as much information on each field as possible, + and then create the final result. This cannot work in the context of Monad. +

    +
  • +
+

A FormInput is even simpler: it returns either a list of error messages or a +result.

+
+
+

Converting

+

“But wait a minute,” you say. “You said the synopsis uses applicative forms, +but I’m sure the type signature said MForm. Shouldn’t it be Monadic?” That’s +true, the final form we produced was monadic. But what really happened is that +we converted an applicative form to a monadic one.

+

Again, our goal is to reuse code as much as possible, and minimize the number +of functions in the API. And Monadic forms are more powerful than Applicative, +if a bit clumsy, so anything that can be expressed in an Applicative form could +also be expressed in a Monadic form. There are two core functions that help out +with this: aformToForm converts any applicative form to a monadic one, and +formToAForm converts certain kinds of monadic forms to applicative forms.

+

“But wait another minute,” you insist. “I didn’t see any aformToForm!” +Also true. The renderDivs function takes care of that for us.

+
+
+

Create AForms

+

Now that I’ve (hopefully) convinced you that in our synopsis we were really +dealing with applicative forms, let’s have a look and try to understand how +these things get created. Let’s take a simple example:

+
data Car = Car
+    { carModel :: Text
+    , carYear  :: Int
+    }
+  deriving Show
+
+carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable carAForm
+

Here, we’ve explicitly split up applicative and monadic forms. In carAForm, +we use the <$> and <*> operators. This should not be surprising; these are +almost always used in applicative-style code. And we have one line for each +record in our Car datatype. Perhaps also unsurprisingly, we have a +textField for the Text record, and an intField for the Int record.

+

Let’s look a bit more closely at the areq function. Its (simplified) type +signature is Field a → FieldSettings → Maybe a → AForm a. That +first argument specifies the datatype of this field, how to parse +it, and how to render it. The next argument, FieldSettings, tells us the +label, tooltip, name and ID of the field. In this case, we’re using the +previously-mentioned IsString instance of FieldSettings.

+

And what’s up with that Maybe a? It provides the optional default value. For +example, if we want our form to fill in "2007" as the default car year, we +would use areq intField "Year" (Just 2007). We can even take this to the next +level, and have a form that takes an optional parameter giving the default +values.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+
+

Optional fields

+

Suppose we wanted to have an optional field (like the car color). All we do +instead is use the aopt function.

+
carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+    <*> aopt textField "Color" Nothing
+

And like required fields, the last argument is the optional default value. +However, this has two layers of Maybe wrapping. This is actually a bit +redundant, but it makes it much easier to write code that takes an optional +default form parameter, such as in the next example.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+    <*> aopt textField "Color" (carColor <$> mcar)
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable $ carAForm $ Just $ Car "Forte" 2010 $ Just "gray"
+
+
+
+

Validation

+

How would we make our form only accept cars created after 1990? If you +remember, we said above that the Field itself contained the information on +what is a valid entry. So all we need to do is write a new Field, right? +Well, that would be a bit tedious. Instead, let’s just modify an existing one:

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField    "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year"  (carYear  <$> mcar)
+    <*> aopt textField    "Color" (carColor <$> mcar)
+  where
+    errorMessage :: Text
+    errorMessage = "Your car is too old, get a new one!"
+
+    carYearField = check validateYear intField
+
+    validateYear y
+        | y < 1990 = Left errorMessage
+        | otherwise = Right y
+

The trick here is the check function. It takes a function (validateYear) +that returns either an error message or a modified field value. In this +example, we haven’t modified the value at all. That is usually going to be the +case. This kind of checking is very common, so we have a shortcut:

+
carYearField = checkBool (>= 1990) errorMessage intField
+

checkBool takes two parameters: a condition that must be fulfilled, and an +error message to be displayed if it was not.

+ +

It’s great to make sure the car isn’t too old. But what if we want to make sure +that the year specified is not from the future? In order to look up the current +year, we’ll need to run some IO. For such circumstances, we’ll need checkM, +which allows our validation code to perform arbitrary actions:

+
    carYearField = checkM inPast $ checkBool (>= 1990) errorMessage intField
+
+    inPast y = do
+        thisYear <- liftIO getCurrentYear
+        return $ if y <= thisYear
+            then Right y
+            else Left ("You have a time machine!" :: Text)
+
+getCurrentYear :: IO Int
+getCurrentYear = do
+    now <- getCurrentTime
+    let today = utctDay now
+    let (year, _, _) = toGregorian today
+    return $ fromInteger year
+

inPast is a function that will return an Either result in the Handler +monad. We use liftIO getCurrentYear to get the current year and then compare +it against the user-supplied year. Also, notice how we can chain together +multiple validators.

+ +
+
+

More sophisticated fields

+

Our color entry field is nice, but it’s not exactly user-friendly. What we +really want is a drop-down list.

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectFieldList colors) "Color" (carColor <$> mcar)
+  where
+    colors :: [(Text, Color)]
+    colors = [("Red", Red), ("Blue", Blue), ("Gray", Gray), ("Black", Black)]
+

selectFieldList takes a list of pairs. The first item in the pair is the text displayed to the user in the drop-down list, and the second item is the actual Haskell value. Of course, the code above looks really repetitive; we can get the same result using the Enum and Bounded instance GHC automatically derives for us.

+
colors = map (pack . show &&& id) [minBound..maxBound]
+

[minBound..maxBound] gives us a list of all the different Color values. We +then apply a map and &&& (a.k.a, the fan-out operator) to turn that into a +list of pairs.

+

Some people prefer radio buttons to drop-down lists. Fortunately, this is just a one-word change.

+
carAForm = Car
+    <$> areq textField               "Model" Nothing
+    <*> areq intField                "Year"  Nothing
+    <*> aopt (radioFieldList colors) "Color" Nothing
+
+
+

Running forms

+

At some point, we’re going to need to take our beautiful forms and produce some +results. There are a number of different functions available for this, each +with its own purpose. I’ll go through them, starting with the most common.

+
+
+runFormPost +
+

+This will run your form against any submitted POST parameters. +If this is not a POST submission, it will return a FormMissing. This +automatically inserts a security token as a hidden form field to avoid +cross-site request +forgery (CSRF) attacks. +

+
+
+runFormGet +
+

+The equivalent of runFormPost for GET parameters. In order to +distinguish a normal GET page load from a GET submission, it includes an +extra _hasdata hidden field in the form. Unlike runFormPost, it does +not include CSRF protection. +

+
+
+runFormPostNoToken +
+

+Same as runFormPost, but does not include (or require) +the CSRF security token. +

+
+
+generateFormPost +
+

+Instead of binding to existing POST parameters, acts as if +there are none. This can be useful when you want to generate a new form after a +previous form was submitted, such as in a wizard. +

+
+
+generateFormGet +
+

+Same as generateFormPost, but for GET. +

+
+
+

The return type from the first three is ((FormResult a, Widget), Enctype). +The Widget will already have any validation errors and previously submitted +values.

+
+
+

i18n

+

There have been a few references to i18n in this chapter. The topic will get +more thorough coverage in its own chapter, but since it has such a profound +effect on yesod-form, I wanted to give a brief overview. The idea behind i18n +in Yesod is to have data types represent messages. Each site can have an +instance of RenderMessage for a given datatype which will translate that +message based on a list of languages the user accepts. As a result of all this, +there are a few things you should be aware of:

+
    +
  • +

    +There is an automatic instance of RenderMessage for Text in every site, + so you can just use plain strings if you don’t care about i18n support. + However, you may need to use explicit type signatures occasionally. +

    +
  • +
  • +

    +yesod-form expresses all of its messages in terms of the FormMessage datatype. Therefore, to use yesod-form, you’ll need to have an appropriate RenderMessage instance. A simple one that uses the default English translations would be: +

    +
  • +
+
instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

This is provided automatically by the scaffolded site.

+
+
+

Monadic Forms

+

Often times, a simple form layout is adequate, and applicative forms excel at +this approach. Sometimes, however, you’ll want to have a more customized look +to your form.

+

A non-standard form layout

+ + + + + + +
+

For these use cases, monadic forms fit the bill. They are a bit more verbose +than their applicative cousins, but this verbosity allows you to have complete +control over what the form will look like. In order to generate the form above, +we could code something like this.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm extra = do
+    (nameRes, nameView) <- mreq textField "this is not used" Nothing
+    (ageRes, ageView) <- mreq intField "neither is this" Nothing
+    let personRes = Person <$> nameRes <*> ageRes
+    let widget = do
+            toWidget
+                [lucius|
+                    ##{fvId ageView} {
+                        width: 3em;
+                    }
+                |]
+            [whamlet|
+                #{extra}
+                <p>
+                    Hello, my name is #
+                    ^{fvInput nameView}
+                    \ and I am #
+                    ^{fvInput ageView}
+                    \ years old. #
+                    <input type=submit value="Introduce myself">
+            |]
+    return (personRes, widget)
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet personForm
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+        |]
+
+main :: IO ()
+main = warp 3000 App
+

Similar to the applicative areq, we use mreq for monadic forms. (And yes, +there’s also mopt for optional fields.) But there’s a big difference: mreq +gives us back a pair of values. Instead of hiding away the FieldView value and +automatically inserting it into a widget, we have the ability to insert it as +we see fit.

+

FieldView has a number of pieces of information. The most important is +fvInput, which is the actual form field. In this example, we also use fvId, +which gives us back the HTML id attribute of the input tag. In our example, +we use that to specify the width of the field.

+

You might be wondering what the story is with the “this is not used” and +“neither is this” values. mreq takes a FieldSettings as its second +argument. Since FieldSettings provides an IsString instance, the strings +are essentially expanded by the compiler to:

+
fromString "this is not used" == FieldSettings
+    { fsLabel = "this is not used"
+    , fsTooltip = Nothing
+    , fsId = Nothing
+    , fsName = Nothing
+    , fsAttrs = []
+    }
+

In the case of applicative forms, the fsLabel and fsTooltip values are used +when constructing your HTML. In the case of monadic forms, Yesod does not +generate any of the “wrapper” HTML for you, and therefore these values are +ignored. However, we still keep the FieldSettings parameter to allow you to +override the id and name attributes of your fields if desired.

+

The other interesting bit is the extra value. GET forms include an extra +field to indicate that they have been submitted, and POST forms include a +security token to prevent CSRF attacks. If you don’t include this extra hidden +field in your form, the form submission will fail.

+

Other than that, things are pretty straight-forward. We create our personRes +value by combining together the nameRes and ageRes values, and then return +a tuple of the person and the widget. And in the getHomeR function, +everything looks just like an applicative form. In fact, you could swap out our +monadic form with an applicative one and the code would still work.

+
+
+

Input forms

+

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 +forms.

+

These work mostly the same as applicative and monadic forms, with some differences:

+
    +
  • +

    +You use runInputPost and runInputGet. +

    +
  • +
  • +

    +You use ireq and iopt. These functions now only take two arguments: the + field type and the name (i.e., HTML name attribute) of the field in + question. +

    +
  • +
  • +

    +After running a form, it returns the value. It doesn’t return a widget or an + encoding type. +

    +
  • +
  • +

    +If there are any validation errors, the page returns an "invalid arguments" + error page. +

    +
  • +
+

You can use input forms to recreate the previous example. Note, however, that +the input version is less user friendly. If you make a mistake in an +applicative or monadic form, you will be brought back to the same page, with +your previously entered values in the form, and an error message explaning what +you need to correct. With input forms, the user simply gets an error message.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/input InputR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form action=@{InputR}>
+            <p>
+                My name is
+                <input type=text name=name>
+                and I am
+                <input type=text name=age>
+                years old.
+                <input type=submit value="Introduce myself">
+    |]
+
+getInputR :: Handler Html
+getInputR = do
+    person <- runInputGet $ Person
+                <$> ireq textField "name"
+                <*> ireq intField "age"
+    defaultLayout [whamlet|<p>#{show person}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Custom fields

+

The fields that come built-in with Yesod will likely cover the vast majority of +your form needs. But occasionally, you’ll need something more specialized. +Fortunately, you can create new forms in Yesod yourself. The Field constructor +has three values: fieldParse takes a list of values submitted by the user and +returns one of three results:

+
    +
  • +

    +An error message saying validation failed. +

    +
  • +
  • +

    +The parsed value. +

    +
  • +
  • +

    +Nothing, indicating that no data was supplied. +

    +
  • +
+

That last case might sound surprising. It would seem that Yesod can +automatically know that no information is supplied when the input list is +empty. But in reality, for some field types, the lack of any input is actually +valid input. Checkboxes, for instance, indicate an unchecked state by sending +in an empty list.

+

Also, what’s up with the list? Shouldn’t it be a Maybe? That’s also not the +case. With grouped checkboxes and multi-select lists, you’ll have multiple +widgets with the same name. We also use this trick in our example below.

+

The second value in the constructor is fieldView, and it renders a widget to display to the +user. This function has the following arguments:

+
    +
  1. +

    +The id attribute. +

    +
  2. +
  3. +

    +The name attribute. +

    +
  4. +
  5. +

    +Any other arbitrary attributes. +

    +
  6. +
  7. +

    +The result, given as an Either value. This will provide either the unparsed +input (when parsing failed) or the successfully parsed value. intField is a +great example of how this works. If you type in 42, the value of result +will be Right 42. But if you type in turtle, the result will be Left +"turtle". This lets you put in a value attribute on your input tag that will +give the user a consistent experience. +

    +
  8. +
  9. +

    +A Bool indicating if the field is required. +

    +
  10. +
+

The final value in the constructor is fieldEnctype. If you’re dealing with +file uploads, this should be Multipart; otherwise, it should be UrlEncoded.

+

As a small example, let’s create a new field type that is a password confirm +field. This field has two text inputs- both with the same name attribute- and +returns an error message if the values don’t match. Note that, unlike most +fields, it does not provide a value attribute on the input tags, as you don’t +want to send back a user-entered password in your HTML ever.

+
passwordConfirmField :: Field Handler Text
+passwordConfirmField = Field
+    { fieldParse = \rawVals _fileVals ->
+        case rawVals of
+            [a, b]
+                | a == b -> return $ Right $ Just a
+                | otherwise -> return $ Left "Passwords don't match"
+            [] -> return $ Right Nothing
+            _ -> return $ Left "You must enter two values"
+    , fieldView = \idAttr nameAttr otherAttrs eResult isReq ->
+        [whamlet|
+            <input id=#{idAttr} name=#{nameAttr} *{otherAttrs} type=password>
+            <div>Confirm:
+            <input id=#{idAttr}-confirm name=#{nameAttr} *{otherAttrs} type=password>
+        |]
+    , fieldEnctype = UrlEncoded
+    }
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet $ renderDivs
+        $ areq passwordConfirmField "Password" Nothing
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+                <input type=submit value="Change password">
+        |]
+
+
+

Values that don’t come from the user

+

Imagine you’re writing a blog hosting web app, and you want to have a form for +users to enter a blog post. A blog post will consist of four pieces of +information:

+
    +
  • +

    +Title +

    +
  • +
  • +

    +HTML contents +

    +
  • +
  • +

    +User ID of the author +

    +
  • +
  • +

    +Publication date +

    +
  • +
+

We want the user to enter the first two values, but not the second two. User ID +should be determined automatically by authenticating the user (a topic we +haven’t covered yet), and the publication date should just be the current time. +The question is, how do we keep our simple applicative form syntax, and yet +pull in values that don’t come from the user?

+

The answer is two separate helper functions:

+
    +
  • +

    +pure allows us to wrap up a plain value as an applicative form value. +

    +
  • +
  • +

    +lift allows us to run arbitrary Handler actions inside an applicative form. +

    +
  • +
+

Let’s see an example of using these two functions:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Data.Time
+import           Yesod
+
+-- In the authentication chapter, we'll address this properly
+newtype UserId = UserId Int
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+type Form a = Html -> MForm Handler (FormResult a, Widget)
+
+data Blog = Blog
+    { blogTitle    :: Text
+    , blogContents :: Textarea
+    , blogUser     :: UserId
+    , blogPosted   :: UTCTime
+    }
+    deriving Show
+
+form :: UserId -> Form Blog
+form userId = renderDivs $ Blog
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+    <*> pure userId
+    <*> lift (liftIO getCurrentTime)
+
+getHomeR :: Handler Html
+getHomeR = do
+    let userId = UserId 5 -- again, see the authentication chapter
+    ((res, widget), enctype) <- runFormPost $ form userId
+    defaultLayout
+        [whamlet|
+            <p>Previous result: #{show res}
+            <form method=post action=@{HomeR} enctype=#{enctype}>
+                ^{widget}
+                <input type=submit>
+        |]
+
+postHomeR :: Handler Html
+postHomeR = getHomeR
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Summary

+

Forms in Yesod are broken up into three groups. Applicative is the most common, +as it provides a nice user interface with an easy-to-use API. Monadic forms +give you more power, but are harder to use. Input forms are intended when you +just want to read data from the user, not generate the input widgets.

+

There are a number of different Fields provided by Yesod out-of-the-box. In +order to use these in your forms, you need to indicate the kind of form and +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 +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.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/haskell.html b/public/book-1.2/haskell.html new file mode 100644 index 00000000..46caef61 --- /dev/null +++ b/public/book-1.2/haskell.html @@ -0,0 +1,462 @@ + Haskell :: Yesod Web Framework Book- Version 1.2 +
+

Haskell

+ + +

In order to use Yesod, you’re going to have to know at least the basics of +Haskell. Additionally, Yesod uses some features of Haskell that aren’t covered +in most introductory texts. While this book assumes the reader has a basic +familiarity with Haskell, this chapter is intended to fill in the gaps.

+

If you are already fluent in Haskell, feel free to completely skip this +chapter. Also, if you would prefer to start off by getting your feet wet with +Yesod, you can always come back to this chapter later as a reference.

+

If you are looking for a more thorough introduction to Haskell, I would +recommend either Real World Haskell or Learn You a Haskell.

+
+

Terminology

+

Even for those familiar with Haskell as a language, there can sometimes be some +confusion about terminology. Let’s establish some base terms that we can use +throughout this book.

+
+
+Data type +
+

+This is one of the core building blocks for a strongly typed +language like Haskell. Some data types, like Int, can be treated as primitive +values, while other data types will build on top of these to create more +complicated values. For example, you might represent a person with: +

+
data Person = Person Text Int
+

Here, the Text would give the person’s name, and the Int would give the +person’s age. Due to its simplicity, this specific example type will recur +throughout the book. There are essentially three ways you can create a new data +type:

+
    +
  • +

    +A type declaration such as type GearCount = Int merely creates a + synonym for an existing type. The type system will do nothing to prevent + you from using an Int where you asked for a GearCount. Using this can + make your code more self-documenting. +

    +
  • +
  • +

    +A newtype declaration such as newtype Make = Make Text. In this case, + you cannot accidentally use a Text in place of a Make; the compiler + will stop you. The newtype wrapper always disappears during compilation, + and will introduce no overhead. +

    +
  • +
  • +

    +A data declaration, such as Person above. You can also create + Algebraic Data Types (ADTs), such as data Vehicle = Bicycle GearCount | + Car Make Model. +

    +
  • +
+
+
+Data constructor +
+

+In our examples above, Person, Make, Bicycle, and +Car are all data constructors. +

+
+
+Type constructor +
+

+In our examples above, Person, Make, and Vehicle are +all type constructors. +

+
+
+Type variables +
+

+Consider the data type data Maybe a = Just a | Nothing. In +this case, a is a type variable. +

+
+
+
+
+

Tools

+

There are two main tools you’ll need to Haskell development. The Glasgow +Haskell Compiler (GHC) is the standard Haskell compiler, and the only one +officially supported by Yesod. You’ll also need Cabal, which is the standard +Haskell build tool. Not only do we use Cabal for building our local code, but +it can automatically download and install dependencies from Hackage, the +Haskell package repository.

+

The Yesod website keeps an up-to-date +quick start guide which includes +information on how to install and configure the various tools. It’s highly +recommended that you follow those instructions.

+

If you decide to install your tools yourself, please make sure to avoid these +common pitfalls:

+
    +
  1. +

    +Some Javascript tools that ship with Yesod require the build tools alex and happy to be installed. These can be added with cabal install alex happy. +

    +
  2. +
  3. +

    +Cabal installs executable to a user-specific directory, which needs to be added to your PATH. The exact location is OS-specific; please make sure to get the correct directory added. +

    +
  4. +
  5. +

    +On Windows, it’s difficult to install the network package from source, as it requires a POSIX shell. Installing the Haskell Platform avoids this issue. +

    +
  6. +
  7. +

    +On Mac OS X, there are multiple C preprocessors available: one from Clang, and one from GCC. Many Haskell libraries depend on the GCC preprocessor. Again, the Haskell Platform sets things up correctly. +

    +
  8. +
  9. +

    +Some Linux distributions- Ubuntu in particular- typically have outdated packages for GHC and Haskell Platform. These may no longer be supported by the current version of Yesod. Please check the quick start guide for minimum version requirements. +

    +
  10. +
  11. +

    +Make sure you have all necessary system libraries installed. This is usually handled automatically by Haskell Platform, but may require extra work on Linux distros. If you get error messages about missing libraries, you usually just need to apt-get install or yum install the relevant libraries. +

    +
  12. +
+

Once you have your toolchain set up correctly, you’ll need to install a number +of Haskell libraries. For the vast majority of the book, the following command +will install all the libraries you need:

+
cabal update && cabal install yesod yesod-bin persistent-sqlite yesod-static
+

Again, please refer to the +quick start guide for the +most up to date and accurate information.

+
+
+

Language Pragmas

+

GHC will run by default in something very close to Haskell98 mode. It also +ships with a large number of language extensions, allowing more powerful type +classes, syntax changes, and more. There are multiple ways to tell GHC to turn +on these extensions. For most of the code snippets in this book, you’ll see +language pragmas, which look like this:

+
{-# LANGUAGE MyLanguageExtension #-}
+

These should always appear at the top of your source file. Additionally, there +are two other common approaches:

+
    +
  • +

    +On the GHC command line, pass an extra argument -XMyLanguageExtension. +

    +
  • +
  • +

    +In your cabal file, add an extensions block. +

    +
  • +
+

I personally never use the GHC command line argument approach. It’s a personal +preference, but I like to have my settings clearly stated in a file. In general +it’s recommended to avoid putting extensions in your cabal file; however, in +the Yesod scaffolded site we specifically use this approach to avoid the +boilerplate of specifying the same language pragmas in every source file.

+

We’ll end up using quite a few language extensions in this book (the +scaffolding uses 11). We will not cover the meaning of all of them. Instead, +please see the +GHC +documentation.

+
+
+

Overloaded Strings

+

What’s the type of "hello"? Traditionally, it’s String, which is defined as +type String = [Char]. Unfortunately, there are a number of limitations with +this:

+
    +
  • +

    +It’s a very inefficient implementation of textual data. We need to allocate + extra memory for each cons cell, plus the characters themselves each take up + a full machine word. +

    +
  • +
  • +

    +Sometimes we have string-like data that’s not actually text, such as + ByteStrings and HTML. +

    +
  • +
+

To work around these limitations, GHC has a language extension called +OverloadedStrings. When enabled, literal strings no longer have the +monomorphic type String; instead, they have the type IsString a ⇒ a, +where IsString is defined as:

+
class IsString a where
+    fromString :: String -> a
+

There are IsString instances available for a number of types in Haskell, such +as Text (a much more efficient packed String type), ByteString, and +Html. Virtually every example in this book will assume that this language +extension is turned on.

+

Unfortunately, there is one drawback to this extension: it can sometimes +confuse GHC’s type checker. Imagine we have:

+
{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}
+import Data.Text (Text)
+
+class DoSomething a where
+    something :: a -> IO ()
+
+instance DoSomething String where
+    something _ = putStrLn "String"
+
+instance DoSomething Text where
+    something _ = putStrLn "Text"
+
+myFunc :: IO ()
+myFunc = something "hello"
+

Will the program print out String or Text? It’s not clear. So instead, +you’ll need to give an explicit type annotation to specify whether "hello" +should be treated as a String or Text.

+
+
+

Type Families

+

The basic idea of a type family is to state some association between two +different types. Suppose we want to write a function that will safely take the +first element of a list. But we don’t want it to work just on lists; we’d like +it to treat a ByteString like a list of Word8s. To do so, we need to +introduce some associated type to specify what the contents of a certain type +are.

+
{-# LANGUAGE TypeFamilies, OverloadedStrings #-}
+import Data.Word (Word8)
+import qualified Data.ByteString as S
+import Data.ByteString.Char8 () -- get an orphan IsString instance
+
+class SafeHead a where
+    type Content a
+    safeHead :: a -> Maybe (Content a)
+
+instance SafeHead [a] where
+    type Content [a] = a
+    safeHead [] = Nothing
+    safeHead (x:_) = Just x
+
+instance SafeHead S.ByteString where
+    type Content S.ByteString = Word8
+    safeHead bs
+        | S.null bs = Nothing
+        | otherwise = Just $ S.head bs
+
+main :: IO ()
+main = do
+    print $ safeHead ("" :: String)
+    print $ safeHead ("hello" :: String)
+
+    print $ safeHead ("" :: S.ByteString)
+    print $ safeHead ("hello" :: S.ByteString)
+

The new syntax is the ability to place a type inside of a class and +instance. We can also use data instead, which will create a new datatype +instead of reference an existing one.

+ +
+
+

Template Haskell

+

Template Haskell (TH) is an approach to code generation. We use it in Yesod +in a number of places to reduce boilerplate, and to ensure that the generated +code is correct. Template Haskell is essentially Haskell which generates a +Haskell Abstract Syntax Tree (AST).

+ +

Writing TH code can be tricky, and unfortunately there isn’t very much type +safety involved. You can easily write TH that will generate code that won’t +compile. This is only an issue for the developers of Yesod, not for its users. +During development, we use a large collection of unit tests to ensure that the +generated code is correct. As a user, all you need to do is call these already +existing functions. For example, to include an externally defined Hamlet +template, you can write:

+
$(hamletFile "myfile.hamlet")
+

(Hamlet is discussed in the Shakespeare chapter.) The dollar sign immediately +followed by parentheses tell GHC that what follows is a Template Haskell +function. The code inside is then run by the compiler and generates a Haskell +AST, which is then compiled. And yes, it’s even possible to +go meta +with this.

+

A nice trick is that TH code is allowed to perform arbitrary IO actions, and +therefore we can place some input in external files and have it parsed at +compile time. One example usage is to have compile-time checked HTML, CSS, and +Javascript templates.

+

If your Template Haskell code is being used to generate declarations, and is +being placed at the top level of our file, we can leave off the dollar sign and +parentheses. In other words:

+
{-# LANGUAGE TemplateHaskell #-}
+
+-- Normal function declaration, nothing special
+myFunction = ...
+
+-- Include some TH code
+$(myThCode)
+
+-- Or equivalently
+myThCode
+

It can be useful to see what code is being generated by Template Haskell for +you. To do so, you should use the -ddump-splices GHC option.

+ +

One final note: Template Haskell introduces something called the stage +restriction, which essentially means that code before a Template Haskell splice +cannot refer to code in the Template Haskell, or what follows. This will +sometimes require you to rearrange your code a bit. The same restriction +applies to QuasiQuotes.

+
+
+

QuasiQuotes

+

QuasiQuotes (QQ) are a minor extension of Template Haskell that let us embed +arbitrary content within our Haskell source files. For example, we mentioned +previously the hamletFile TH function, which reads the template contents from +an external file. We also have a quasi-quoter named hamlet that takes the +content inline:

+
{-# LANGUAGE QuasiQuotes #-}
+
+[hamlet|<p>This is quasi-quoted Hamlet.|]
+

The syntax is set off using square brackets and pipes. The name of the +quasi-quoter is given between the opening bracket and the first pipe, and the +content is given between the pipes.

+

Throughout the book, we will often times use the QQ-approach over a TH-powered +external file since the former is simpler to copy-and-paste. However, in +production, external files are recommended for all but the shortest of inputs +as it gives a nice separation of the non-Haskell syntax from your Haskell code.

+
+
+

API Documentation

+

The standard API documentation program in Haskell is called Haddock. The +standard Haddock search tool is called Hoogle. My recommendation is to use +FP Complete’s Hoogle search and its +accompanying Haddocks for searching and browsing documentation. The reason for +this is that the FP Complete Hoogle database covers a very large number of open +source Haskell packages, and the documentation provided is always fully +generated and known to link to other working Haddocks.

+

The more commonly used sources for these are +Hackage itself, and +haskell.org’s Hoogle instance. The +downsides to these are that- based on build issues on the server- documentation +is sometimes not generated, and the Hoogle search defaults to searching only a +subset of available packages. Most importantly for us, Yesod is indexed by FP +Complete’s Hoogle, but not by haskell.org’s.

+

If when reading this book you run into types or functions that you do not +understand, try doing a Hoogle search with FP Complete’s Hoogle to get more +information.

+
+
+

Summary

+

You don’t need to be an expert in Haskell to use Yesod, a basic familiarity +will suffice. This chapter hopefully gave you just enough extra information to +feel more comfortable following the rest of the book.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/http-conduit.html b/public/book-1.2/http-conduit.html new file mode 100644 index 00000000..1677c933 --- /dev/null +++ b/public/book-1.2/http-conduit.html @@ -0,0 +1,339 @@ + http-conduit :: Yesod Web Framework Book- Version 1.2 +
+

http-conduit

+ + +

Most of Yesod is about serving content over HTTP. But that’s only half the +story: someone has to receive it. And even when you’re writing a web app, +sometimes that someone will be you. If you want to consume content from other +services or interact with RESTful APIs, you’ll need to write client code. And +the recommended approach for that is http-conduit.

+

This chapter is not directly connected to Yesod, and will be generally useful +for anyone wanting to make HTTP requests.

+
+

Synopsis

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Conduit -- the main module
+
+-- The streaming interface uses conduits
+import Data.Conduit
+import Data.Conduit.Binary (sinkFile)
+
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+import Control.Monad.Trans.Resource (runResourceT)
+
+main :: IO ()
+main = do
+    -- Simplest query: just download the information from the given URL as a
+    -- lazy ByteString.
+    simpleHttp "http://www.example.com/foo.txt" >>= L.writeFile "foo.txt"
+
+    -- Use the streaming interface instead. We need to run all of this inside a
+    -- ResourceT, to ensure that all our connections get properly cleaned up in
+    -- the case of an exception.
+    runResourceT $ do
+        -- We need a Manager, which keeps track of open connections. simpleHttp
+        -- creates a new manager on each run (i.e., it never reuses
+        -- connections).
+        manager <- liftIO $ newManager conduitManagerSettings
+
+        -- A more efficient version of the simpleHttp query above. First we
+        -- parse the URL to a request.
+        req <- liftIO $ parseUrl "http://www.example.com/foo.txt"
+
+        -- Now get the response
+        res <- http req manager
+
+        -- And finally stream the value to a file
+        responseBody res $$+- sinkFile "foo.txt"
+
+        -- Make it a POST request, don't follow redirects, and accept any
+        -- status code.
+        let req2 = req
+                { method = "POST"
+                , redirectCount = 0
+                , checkStatus = \_ _ _ -> Nothing
+                }
+        res2 <- http req2 manager
+        responseBody res2 $$+- sinkFile "post-foo.txt"
+
+
+

Concepts

+

The simplest way to make a request in http-conduit is with the simpleHttp +function. This function takes a String giving a URL and returns a +ByteString with the contents of that URL. But under the surface, there are a +few more steps:

+
    +
  • +

    +A new connection Manager is allocated. +

    +
  • +
  • +

    +The URL is parsed to a Request. If the URL is invalid, then an exception is thrown. +

    +
  • +
  • +

    +The HTTP request is made, following any redirects from the server. +

    +
  • +
  • +

    +If the response has a status code outside the 200-range, an exception is thrown. +

    +
  • +
  • +

    +The response body is read into memory and returned. +

    +
  • +
  • +

    +runResourceT is called, which will free up any resources (e.g., the open socket to the server). +

    +
  • +
+

If you want more control of what’s going on, then you can configure any of the +steps above (plus a few more) by explicitly creating a Request value, +allocating your Manager manually, and using the http and httpLbs +functions.

+
+
+

Request

+

The easiest way to creating a Request is with the parseUrl function. This +function will return a value in any Failure monad, such as Maybe or IO. +The last of those is the most commonly used, and results in a runtime exception +whenever an invalid URL is provided. However, you can use a different monad if, +for example, you want to validate user input.

+
import Network.HTTP.Conduit
+import System.Environment (getArgs)
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    args <- getArgs
+    case args of
+        [urlString] ->
+            case parseUrl urlString of
+                Nothing -> putStrLn "Sorry, invalid URL"
+                Just req -> withManager $ \manager -> do
+                    res <- httpLbs req manager
+                    liftIO $ L.putStr $ responseBody res
+        _ -> putStrLn "Sorry, please provide exactly one URL"
+

The Request type is abstract so that http-conduit can add new settings in +the future without breaking the API (see the Settings Type chapter for more +information). In order to make changes to individual records, you use record +notation. For example, a modification to our program that issues HEAD +requests and prints the response headers would be:

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Conduit
+import System.Environment (getArgs)
+import qualified Data.ByteString.Lazy as L
+import Control.Monad.IO.Class (liftIO)
+
+main :: IO ()
+main = do
+    args <- getArgs
+    case args of
+        [urlString] ->
+            case parseUrl urlString of
+                Nothing -> putStrLn "Sorry, invalid URL"
+                Just req -> withManager $ \manager -> do
+                    let reqHead = req { method = "HEAD" }
+                    res <- http reqHead manager
+                    liftIO $ do
+                        print $ responseStatus res
+                        mapM_ print $ responseHeaders res
+        _ -> putStrLn "Sorry, please provide example one URL"
+

There are a number of different configuration settings in the API, some noteworthy ones are:

+
+
+proxy +
+

+Allows you to pass the request through the given proxy server. +

+
+
+redirectCount +
+

+Indicate how many redirects to follow. Default is 10. +

+
+
+checkStatus +
+

+Check the status code of the return value. By default, gives an exception for any non-2XX response. +

+
+
+requestBody +
+

+The request body to be sent. Be sure to also update the method. For the common case of url-encoded data, you can use the urlEncodedBody function. +

+
+
+
+
+

Manager

+

The connection manager allows you to reuse connections. When making multiple +queries to a single server (e.g., accessing Amazon S3), this can be critical +for creating efficient code. A manager will keep track of multiple connections +to a given server (taking into account port and SSL as well), automatically +reaping unused connections as needed. When you make a request, http-conduit +first tries to check out an existing connection. When you’re finished with the +connection (if the server allows keep-alive), the connection is returned to the +manager. If anything goes wrong, the connection is closed.

+

To keep our code exception-safe, we use the ResourceT monad transformer. All +this means for you is that your code needs to be wrapped inside a call to +runResourceT, either implicitly or explicitly, and that code inside that +block will need to liftIO to perform normal IO actions.

+

There are two ways you can get ahold of a manager. newManager will return a +manager that will not be automatically closed (you can use closeManager to do +so manually), while withManager will start a new ResourceT block, allow you +to use the manager, and then automatically close the ResourceT when you’re +done. If you want to use a ResourceT for an entire application, and have no +need to close it, you should probably use newManager.

+

One other thing to point out: you obviously don’t want to create a new manager +for each and every request; that would defeat the whole purpose. You should +create your Manager early and then share it.

+
+
+

Response

+

The Response datatype has three pieces of information: the status code, the +response headers, and the response body. The first two are straight-forward; +let’s discuss the body.

+

The Response type has a type variable to allow the response body to be of +multiple types. If you want to use http-conduit's streaming interface, you +want this to be a Source. For the simple interface, it will be a lazy +ByteString. One thing to note is that, even though we use a lazy +ByteString, the entire response is held in memory. In other words, we +perform no lazy I/O in this package.

+ +
+
+

http and httpLbs

+

So let’s tie it together. The http function gives you access to the streaming +interface (i.e., it returns a Response using a ResumableSource) while +httpLbs returns a lazy ByteString. Both of these return values in the +ResourceT transformer so that they can access the Manager and have +connections handled properly in the case of exceptions.

+ +
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/initializing-foundation-data.html b/public/book-1.2/initializing-foundation-data.html new file mode 100644 index 00000000..b3078b2c --- /dev/null +++ b/public/book-1.2/initializing-foundation-data.html @@ -0,0 +1,271 @@ + Initializing data in the foundation datatype :: Yesod Web Framework Book- Version 1.2 +
+

Initializing data in the foundation datatype

+ + +

This example is meant to demonstrate a relatively simple concept: performing +some initialization of data to be kept in the foundation datatype. There are +various reasons to do this, though the two most important are:

+
    +
  • +

    +Efficiency: by initializing data once, at process startup, you can avoid + having to recompute the same value in each request. +

    +
  • +
  • +

    +Persistence: we want to store some information in a mutable location which + will be persisted between individual requests. Often times, this is done via + an external database, but it can also be done via an in-memory mutable + variable. +

    +
  • +
+ +

To demonstrate, we’ll implement a very simple website. It will contain a single +route, and will serve content stored in a Markdown file. In addition to serving +that content, we’ll also display an old-school visitor counter indicating how +many visitors have been to the site.

+
+

Step 1: define your foundation

+

We’ve identified two pieces of information to be initialized: the Markdown +content to be display on the homepage, and a mutable variable holding the +visitor count. Remember that our goal is to perform as much of the work in the +initialization phase as possible and thereby avoid performing the same work in +the handlers themselves. Therefore, we want to preprocess the Markdown content +into HTML. As for the visitor count, a simple IORef should be sufficient. So +our foundation data type is:

+
data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+
+

Step 2: use the foundation

+

For this trivial example, we only have one route: the homepage. All we need to do is:

+
    +
  1. +

    +Increment the visitor count. +

    +
  2. +
  3. +

    +Get the new visitor count. +

    +
  4. +
  5. +

    +Display the Markdown content together with the visitor count. +

    +
  6. +
+

One trick we’ll use to make the code a bit shorter is to utilize record +wildcard syntax: App {..}. This is convenient when we want to deal with a +number of different fields in a datatype.

+
getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+
+

Step 3: create the foundation value

+

When we initialize our application, we’ll now need to provide values for the +two fields we described above. This is normal IO code, and can perform any +arbitrary actions needed. In our case, we need to:

+
    +
  1. +

    +Read the Markdown from the file. +

    +
  2. +
  3. +

    +Convert that Markdown to HTML. +

    +
  4. +
  5. +

    +Create the visitor counter variable. +

    +
  6. +
+

The code ends up being just as simple as those steps imply:

+
go :: IO ()
+go = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+

Conclusion

+

There’s no rocket science involved in this example, just very straightforward +programming. The purpose of this chapter is to demonstrate the commonly used +best practice for achieving these often needed objectives. In your own +applications, the initialization steps will likely be much more complicated: +setting up database connection pools, starting background jobs to batch process +large data, or anything else. After reading this chapter, you should now have a +good idea of where to place your application-specific initialization code.

+

Below is the full source code for the example described above:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import qualified Data.Text.Lazy.IO as TLIO
+import           Text.Markdown
+import           Yesod
+
+data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+main :: IO ()
+main = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/internationalization.html b/public/book-1.2/internationalization.html new file mode 100644 index 00000000..a8c5621b --- /dev/null +++ b/public/book-1.2/internationalization.html @@ -0,0 +1,412 @@ + Internationalization :: Yesod Web Framework Book- Version 1.2 +
+

Internationalization

+ + +

Users expect our software to speak their language. Unfortunately for us, there +will likely be more than one language involved. While doing simple string +replacement isn’t too involved, correctly dealing with all the grammar issues +can be tricky. After all, who wants to see "List 1 file(s)" from a program +output?

+

But a real i18n solution needs to do more than just provide a means of +achieving the correct output. It needs to make this process easy for both the +programmer and the translator and relatively error-proof. Yesod’s answer to the +problem gives you:

+
    +
  • +

    +Intelligent guessing of the user’s desired language based on request headers, + with the ability to override. +

    +
  • +
  • +

    +A simple syntax for giving translations which requires no Haskell knowledge. + (After all, most translators aren’t programmers.) +

    +
  • +
  • +

    +The ability to bring in the full power of Haskell for tricky grammar issues + as necessary, along with a default selection of helper functions to cover + most needs. +

    +
  • +
  • +

    +Absolutely no issues at all with word order. +

    +
  • +
+
+

Synopsis

+
-- @messages/en.msg
+Hello: Hello
+EnterItemCount: I would like to buy:
+Purchase: Purchase
+ItemCount count@Int: You have purchased #{showInt count} #{plural count "item" "items"}.
+SwitchLanguage: Switch language to:
+Switch: Switch
+
-- @messages/he.msg
+Hello: שלום
+EnterItemCount: אני רוצה לקנות:
+Purchase: קנה
+ItemCount count: קנית #{showInt count} #{plural count "דבר" "דברים"}.
+SwitchLanguage: החלף שפה ל:
+Switch: החלף
+
-- @i18n-synopsis.hs
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkMessage "App" "messages" "en"
+
+plural :: Int -> String -> String -> String
+plural 1 x _ = x
+plural _ _ y = y
+
+showInt :: Int -> String
+showInt = show
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+mkYesod "App" [parseRoutes|
+/     HomeR GET
+/buy  BuyR  GET
+/lang LangR POST
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>_{MsgHello}
+        <form action=@{BuyR}>
+            _{MsgEnterItemCount}
+            <input type=text name=count>
+            <input type=submit value=_{MsgPurchase}>
+        <form action=@{LangR} method=post>
+            _{MsgSwitchLanguage}
+            <select name=lang>
+                <option value=en>English
+                <option value=he>Hebrew
+            <input type=submit value=_{MsgSwitch}>
+    |]
+
+getBuyR :: Handler Html
+getBuyR = do
+    count <- runInputGet $ ireq intField "count"
+    defaultLayout [whamlet|<p>_{MsgItemCount count}|]
+
+postLangR :: Handler ()
+postLangR = do
+    lang <- runInputPost $ ireq textField "lang"
+    setLanguage lang
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Overview

+

Most existing i18n solutions out there, like gettext or Java message bundles, +work on the principle of string lookups. Usually some form of +printf-interpolation is used to interpolate variables into the strings. In +Yesod, as you might guess, we instead rely on types. This gives us all of our +normal advantages, such as the compiler automatically catching mistakes.

+

Let’s take a concrete example. Suppose our application has two things it wants +to say to a user: say hello, and state how many users are logged into the +system. This can be modeled with a sum type:

+
data MyMessage = MsgHello | MsgUsersLoggedIn Int
+

I can also write a function to turn this datatype into an English representation:

+
toEnglish :: MyMessage -> String
+toEnglish MsgHello = "Hello there!"
+toEnglish (MsgUsersLoggedIn 1) = "There is 1 user logged in."
+toEnglish (MsgUsersLoggedIn i) = "There are " ++ show i ++ " users logged in."
+

We can also write similar functions for other languages. The advantage to this +inside-Haskell approach is that we have the full power of Haskell for +addressing tricky grammar issues, especially pluralization.

+ +

The downside, however, is that you have to write all of this inside of Haskell, +which won’t be very translator-friendly. To solve this, Yesod introduces the +concept of message files. We’ll cover that in a little bit.

+

Assuming we have this full set of translation functions, how do we go about +using them? What we need is a new function to wrap them all up together, and +then choose the appropriate translation function based on the user’s selected +language. Once we have that, Yesod can automatically choose the most relevant +render function and call it on the values you provide.

+

In order to simplify things a bit, Hamlet has a special interpolation syntax, +_{…}, which handles all the calls to the render functions. And in order to +associate a render function with your application, you use the YesodMessage +typeclass.

+
+
+

Message files

+

The simplest approach to creating translations is via message files. The setup +is simple: there is a single folder containing all of your translation files, +with a single file for each language. Each file is named based on its language +code, e.g. en.msg. And each line in a file handles one phrase, which +correlates to a single constructor in your message data type.

+ +

So firstly, a word about language codes. There are really two choices +available: using a two-letter language code, or a language-LOCALE code. For +example, when I load up a page in my web browser, it sends two language codes: +en-US and en. What my browser is saying is "if you have American English, I +like that the most. If you have English, I’ll take that instead."

+

So which format should you use in your application? Most likely two-letter +codes, unless you are actually creating separate translations by locale. This +ensures that someone asking for Canadian English will still see your English. +Behind the scenes, Yesod will add the two-letter codes where relevant. For +example, suppose a user has the following language list:

+
pt-BR, es, he
+

What this means is "I like Brazilian Porteguese, then Spanish, and then +Hebrew." Suppose your application provides the languages pt (general +Porteguese) and English, with English as the default. Strictly following the +user’s language list would result in the user being served English. Instead, +Yesod translates that list into:

+
pt-BR, es, he, pt
+

In other words: unless you’re giving different translations based on locale, +just stick to the two-letter language codes.

+

Now what about these message files? The syntax should be very familiar after +your work with Hamlet and Persistent. The line starts off with the name of the +message. Since this is a data constructor, it must start with a capital letter. +Next, you can have individual parameters, which must be given as lower case. +These will be arguments to the data constructor.

+

The argument list is terminated by a colon, and then followed by the translated +string, which allows usage of our typical variable interpolation syntax +translation helper functions to deal with issues like pluralization, you can +create all the translated messages you need.

+
+

Specifying types

+

Since we will be creating a datatype out of our message specifications, each +parameter to a data constructor must be given a data type. We use a @-syntax +for this. For example, to create the datatype data MyMessage = MsgHello | +MsgSayAge Int, we would write:

+
Hello: Hi there!
+SayAge age@Int: Your age is: #{show age}
+

But there are two problems with this:

+
    +
  1. +

    +It’s not very DRY (don’t repeat yourself) to have to specify this datatype in every file. +

    +
  2. +
  3. +

    +Translators will be confused having to specify these datatypes. +

    +
  4. +
+

So instead, the type specification is only required in the main language file. +This is specified as the third argument in the mkMessage function. This also +specifies what the backup language will be, to be used when none of the +languages provided by your application match the user’s language list.

+
+
+
+

RenderMessage typeclass

+

Your call to mkMessage creates an instance of the RenderMessage typeclass, +which is the core of Yesod’s i18n. It is defined as:

+
class RenderMessage master message where
+    renderMessage :: master
+                  -> [Text] -- ^ languages
+                  -> message
+                  -> Text
+

Notice that there are two parameters to the RenderMessage class: the master +site and the message type. In theory, we could skip the master type here, but +that would mean that every site would need to have the same set of translations +for each message type. When it comes to shared libraries like forms, that would +not be a workable solution.

+

The renderMessage function takes a parameter for each of the class’s type +parameters: master and message. The extra parameter is a list of languages the +user will accept, in descending order of priority. The method then returns a +user-ready Text that can be displayed.

+

A simple instance of RenderMessage may involve no actual translation of +strings; instead, it will just display the same value for every language. For +example:

+
data MyMessage = Hello | Greet Text
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ _ Hello = "Hello"
+    renderMessage _ _ (Greet name) = "Welcome, " <> name <> "!"
+

Notice how we ignore the first two parameters to renderMessage. We can now +extend this to support multiple languages:

+
renderEn Hello = "Hello"
+renderEn (Greet name) = "Welcome, " <> name <> "!"
+renderHe Hello = "שלום"
+renderHe (Greet name) = "ברוכים הבאים, " <> name <> "!"
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ ("en":_) = renderEn
+    renderMessage _ ("he":_) = renderHe
+    renderMessage master (_:langs) = renderMessage master langs
+    renderMessage _ [] = renderEn
+

The idea here is fairly straight-forward: we define helper functions to support +each language. We then add a clause to catch each of those languages in the +renderMessage definition. We then have two final cases: if no languages +matched, continue checking with the next language in the user’s priority list. +If we’ve exhausted all languages the user specified, then use the default +language (in our case, English).

+

But odds are that you will never need to worry about writing this stuff +manually, as the message file interface does all this for you. But it’s always +a good idea to have an understanding of what’s going on under the surface.

+
+
+

Interpolation

+

One way to use your new RenderMessage instance would be to directly call the +renderMessage function. This would work, but it’s a bit tedious: you need to +pass in the foundation value and the language list manually. Instead, Hamlet +provides a specialized i18n interpolation, which looks like _{…}.

+ +

Hamlet will then automatically translate that to a call to renderMessage. +Once Hamlet gets the output Text value, it uses the toHtml function to +produce an Html value, meaning that any special characters (<, &, +>) will be automatically escaped.

+
+
+

Phrases, not words

+

As a final note, I’d just like to give some general i18n advice. Let’s say you +have an application for selling turtles. You’re going to use the word "turtle" +in multiple places, like "You have added 4 turtles to your cart." and "You have +purchased 4 turtles, congratulations!" As a programmer, you’ll immediately +notice the code reuse potential: we have the phrase "4 turtles" twice. So you +might structure your message file as:

+
AddStart: You have added
+AddEnd: to your cart.
+PurchaseStart: You have purchased
+PurchaseEnd: , congratulations!
+Turtles count@Int: #{show count} #{plural count "turtle" "turtles"}
+

STOP RIGHT THERE! This is all well and good from a programming perspective, but translations are not programming. There are a many things that could go wrong with this, such as:

+
    +
  • +

    +Some languages might put "to your cart" before "You have added." +

    +
  • +
  • +

    +Maybe "added" will be constructed differently depending whether you added 1 or more turtles. +

    +
  • +
  • +

    +There are a bunch of whitespace issues as well. +

    +
  • +
+

So the general rule is: translate entire phrases, not just words.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/introduction.html b/public/book-1.2/introduction.html new file mode 100644 index 00000000..0c3e260d --- /dev/null +++ b/public/book-1.2/introduction.html @@ -0,0 +1,244 @@ + Introduction :: Yesod Web Framework Book- Version 1.2 +
+

Introduction

+ + +

Since web programming began, people have been trying to make the development +process a more pleasant one. As a community, we have continually pushed new +techniques to try and solve some of the lingering difficulties of security +threats, the stateless nature of HTTP, the multiple languages (HTML, CSS, +Javascript) necessary to create a powerful web application, and more.

+

Yesod attempts to ease the web development process by playing to the strengths +of the Haskell programming language. Haskell’s strong compile-time guarantees +of correctness not only encompass types; referential transparency ensures that +we don’t have any unintended side effects. Pattern matching on algebraic data +types can help guarantee we’ve accounted for every possible case. By building +upon Haskell, entire classes of bugs disappear.

+

Unfortunately, using Haskell isn’t enough. The web, by its very nature, is +not type safe. Even the simplest case of distinguishing between an integer +and string is impossible: all data on the web is transferred as raw bytes, +evading our best efforts at type safety. Every app writer is left with the task +of validating all input. I call this problem the boundary issue: as much as +your application is type safe on the inside, every boundary with the outside +world still needs to be sanitized.

+
+

Type Safety

+

This is where Yesod comes in. By using high-level declarative techniques, you +can specify the exact input types you are expecting. And the process works the +other way as well: using a process of type-safe URLs, you can make sure that +the data you send out is also guaranteed to be well formed.

+

The boundary issue is not just a problem when dealing with the client: the same +problem exists when persisting and loading data. Once again, Yesod saves you on +the boundary by performing the marshaling of data for you. You can specify your +entities in a high-level definition and remain blissfully ignorant of the +details.

+
+
+

Concise

+

We all know that there is a lot of boilerplate coding involved in web +applications. Wherever possible, Yesod tries to use Haskell’s features to save +your fingers the work:

+
    +
  • +

    +The forms library reduces the amount of code used for common cases by + leveraging the Applicative type class. +

    +
  • +
  • +

    +Routes are declared in a very terse format, without sacrificing type safety. +

    +
  • +
  • +

    +Serializing your data to and from a database is handled automatically via + code generation. +

    +
  • +
+

In Yesod, we have two kinds of code generation. To get your project started, we +provide a scaffolding tool to set up your file and folder structure. However, +most code generation is done at compile time via meta programming. This means +your generated code will never get stale, as a simple library upgrade will +bring all your generated code up-to-date.

+

But for those who like to stay in control, and know exactly what their code is +doing, you can always run closer to the compiler and write all your code +yourself.

+
+
+

Performance

+

Haskell’s main compiler, the GHC, has amazing performance characteristics, and +is improving all the time. This choice of language by itself gives Yesod a +large performance advantage over other offerings. But that’s not enough: we +need an architecture designed for performance.

+

Our approach to templates is one example: by allowing HTML, CSS and JavaScript +to be analyzed at compile time, Yesod both avoids costly disk I/O at runtime +and can optimize the rendering of this code. But the architectural decisions go +deeper: we use advanced techniques such as conduits and builders in the +underlying libraries to make sure our code runs in constant memory, without +exhausting precious file handles and other resources. By offering high-level +abstractions, you can get highly compressed and properly cached CSS and +JavaScript.

+

Yesod’s flagship web server, Warp, is the fastest Haskell web server around. +When these two pieces of technology are combined, it produces one of the +fastest web application deployment solutions available.

+
+
+

Modular

+

Yesod has spawned the creation of dozens of packages, most of which are usable +in a context outside of Yesod itself. One of the goals of the project is to +contribute back to the community as much as possible; as such, even if you are +not planning on using Yesod in your next project, a large portion of this book +may still be relevant for your needs.

+

Of course, these libraries have all been designed to integrate well together. +Using the Yesod Framework should give you a strong feeling of consistency +throughout the various APIs.

+
+
+

A solid foundation

+

I remember once seeing a PHP framework advertising support for UTF-8. This +struck me as surprising: you mean having UTF-8 support isn’t automatic? In the +Haskell world, issues like character encoding are already well addressed and +fully supported. In fact, we usually have the opposite problem: there are a +number of packages providing powerful and well-designed support for the +problem. The Haskell community is constantly pushing the boundaries finding the +cleanest, most efficient solutions for each challenge.

+

The downside of such a powerful ecosystem is the complexity of choice. By using +Yesod, you will already have most of the tools chosen for you, and you can be +guaranteed they work together. Of course, you always have the option of pulling +in your own solution.

+

As a real-life example, Yesod and Hamlet (the default templating language) use +blaze-builder for textual content generation. This choice was made because +blaze provides the fastest interface for generating UTF-8 data. Anyone who +wants to use one of the other great libraries out there, such as text, should +have no problem dropping it in.

+
+
+

Introduction to Haskell

+

Haskell is a powerful, fast, type-safe, functional programming language. This +book takes as an assumption that you are already familiar with most of the +basics of Haskell. There are two wonderful books for learning Haskell, both of +which are available for reading online:

+ +

Yesod relies on a few features in Haskell that most introductory tutorials do +not cover. Though you will rarely need to understand how these work, it’s +always best to start off with a good appreciation for what your tools are +doing. These are covered in the next chapter.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/json-web-service.html b/public/book-1.2/json-web-service.html new file mode 100644 index 00000000..941d4c34 --- /dev/null +++ b/public/book-1.2/json-web-service.html @@ -0,0 +1,222 @@ + JSON Web Service :: Yesod Web Framework Book- Version 1.2 +
+

JSON Web Service

+ + +

Let’s create a very simple web service: it takes a JSON request and returns a +JSON response. We’re going to write the server in WAI/Warp, and the client in +http-conduit. We’ll be using aeson for JSON parsing and rendering. We could +also write the server in Yesod itself, but for such a simple example, the extra +features of Yesod don’t add much.

+
+

Server

+

WAI uses the conduit package to handle streaming request bodies, and +efficiently generates responses using blaze-builder. aeson uses attoparsec for +parsing; by using attoparsec-conduit we get easy interoperability with WAI. +This plays out as:

+
{-# LANGUAGE OverloadedStrings #-}
+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.ByteString          (ByteString)
+import           Data.Conduit             (($$))
+import           Data.Conduit.Attoparsec  (sinkParser)
+import           Network.HTTP.Types       (status200, status400)
+import           Network.Wai              (Application, Response, requestBody,
+                                           responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req = handle invalidJson $ do
+    value <- requestBody req $$ sinkParser json
+    newValue <- liftIO $ modValue value
+    return $ responseLBS
+        status200
+        [("Content-Type", "application/json")]
+        $ encode newValue
+
+invalidJson :: SomeException -> IO Response
+invalidJson ex = return $ responseLBS
+    status400
+    [("Content-Type", "application/json")]
+    $ encode $ object
+        [ ("message" .= show ex)
+        ]
+
+-- Application-specific logic would go here.
+modValue :: Value -> IO Value
+modValue = return
+
+
+

Client

+

http-conduit was written as a companion to WAI. It too uses conduit and +blaze-builder pervasively, meaning we once again get easy interop with +aeson. A few extra comments for those not familiar with http-conduit:

+
    +
  • +

    +A Manager is present to keep track of open connections, so that multiple + requests to the same server use the same connection. You usually want to use + the withManager function to create and clean up this Manager, since it is + exception safe. +

    +
  • +
  • +

    +We need to know the size of our request body, which can’t be determined + directly from a Builder. Instead, we convert the Builder into a lazy + ByteString and take the size from there. +

    +
  • +
  • +

    +There are a number of different functions for initiating a request. We use + http, which allows us to directly access the data stream. There are other + higher level functions (such as httpLbs) that let you ignore the issues of + sources and get the entire body directly. +

    +
  • +
+
{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Data.Aeson              (Value (Object, String))
+import           Data.Aeson              (encode, object, (.=))
+import           Data.Aeson.Parser       (json)
+import           Data.Conduit            (($$+-))
+import           Data.Conduit.Attoparsec (sinkParser)
+import           Network.HTTP.Conduit    (RequestBody (RequestBodyLBS),
+                                          Response (..), http, method, parseUrl,
+                                          requestBody, withManager)
+
+main :: IO ()
+main = withManager $ \manager -> do
+    value <- liftIO makeValue
+    -- We need to know the size of the request body, so we convert to a
+    -- ByteString
+    let valueBS = encode value
+    req' <- liftIO $ parseUrl "http://localhost:3000/"
+    let req = req' { method = "POST", requestBody = RequestBodyLBS valueBS }
+    res <- http req manager
+    resValue <- responseBody res $$+- sinkParser json
+    liftIO $ handleResponse resValue
+
+-- Application-specific function to make the request value
+makeValue :: IO Value
+makeValue = return $ object
+    [ ("foo" .= ("bar" :: String))
+    ]
+
+-- Application-specific function to handle the response from the server
+handleResponse :: Value -> IO ()
+handleResponse = print
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/monad-control.html b/public/book-1.2/monad-control.html new file mode 100644 index 00000000..221a49dc --- /dev/null +++ b/public/book-1.2/monad-control.html @@ -0,0 +1,451 @@ + monad-control :: Yesod Web Framework Book- Version 1.2 +
+

monad-control

+ + +

monad-control is used in a few places within Yesod, most notably to ensure +proper exception handling within Persistent. It is a general purpose package to +extend standard functionality in monad transformers.

+
+

Overview

+

One of the powerful, and sometimes confusing, features in Haskell is monad +transformers. They allow you to take different pieces of functionality- such as +mutable state, error handling, or logging- and compose them together easily. +Though I swore I’d never write a monad tutorial, I’m going to employ a painful +analogy here: monads are like onions. (Monads are not like cakes.) By that, I +mean layers.

+

We have the core monad- also known as the innermost or bottom monad. On top of +this core, we add layers, each adding a new feature and spreading +outward/upward. As a motivating example, let’s consider an ErrorT transformer +stacked on top of the IO monad:

+
newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
+type MyStack = ErrorT MyError IO
+

Now pay close attention here: ErrorT is just a simple newtype around an Either +wrapped in a monad. Getting rid of the newtype, we have:

+
type ErrorTUnwrapped e m a = m (Either e a)
+

At some point, we’ll need to actually perform some IO inside our MyStack. If we +went with the unwrapped approach, it would be trivial, since there would be no +ErrorT constructor in the way. However, we need that newtype wrapper for a +whole bunch of type reasons I won’t go into here (this isn’t a monad +transformer tutorial after all). So the solution is the MonadTrans typeclass:

+
class MonadTrans t where
+    lift :: Monad m => m a -> t m a
+

I’ll admit, the first time I saw that type signature, my response was stunned +confusion, and incredulity that it actually meant anything. But looking at an +instance helps a bit:

+
instance (Error e) => MonadTrans (ErrorT e) where
+    lift m = ErrorT $ do
+        a <- m
+        return (Right a)
+

All we’re doing is wrapping the inside of the IO with a Right value, and then +applying our newtype wrapper. This allows us to take an action that lives in +IO, and "lift" it to the outer/upper monad.

+

But now to the point at hand. This works very well for simple functions. For +example:

+
sayHi :: IO ()
+sayHi = putStrLn "Hello"
+
+sayHiError :: ErrorT MyError IO ()
+sayHiError = lift $ putStrLn "Hello"
+

But let’s take something slightly more complicated, like a callback:

+
withMyFile :: (Handle -> IO a) -> IO a
+withMyFile = withFile "test.txt" WriteMode
+
+sayHi :: Handle -> IO ()
+sayHi handle = hPutStrLn handle "Hi there"
+
+useMyFile :: IO ()
+useMyFile = withMyFile sayHi
+

So far so good, right? Now let’s say that we need a version of sayHi that has +access to the Error monad:

+
sayHiError :: Handle -> ErrorT MyError IO ()
+sayHiError handle = do
+    lift $ hPutStrLn handle "Hi there, error!"
+    throwError MyError
+

We would like to write a function that combines withMyFile and sayHiError. +Unfortunately, GHC doesn’t like this very much:

+
useMyFileErrorBad :: ErrorT MyError IO ()
+useMyFileErrorBad = withMyFile sayHiError
+
+    Couldn't match expected type `ErrorT MyError IO ()'
+                with actual type `IO ()'
+

Why does this happen, and how can we work around it?

+
+
+

Intuition

+

Let’s try and develop an external intuition of what’s happening here. The +ErrorT monad transformer adds extra functionality to the IO monad. We’ve +defined a way to "tack on" that extra functionality to normal IO actions: we +add that Right constructor and wrap it all in ErrorT. Wrapping in Right is our +way of saying "it went OK," there wasn’t anything wrong with this action.

+

Now this intuitively makes sense: since the IO monad doesn’t have the concept +of returning a MyError when something goes wrong, it will always succeed in the +lifting phase. (Note: This has nothing to do with runtime exceptions, don’t +even think about them.) What we have is a guaranteed one-directional +translation up the monad stack.

+

Let’s take another example: the Reader monad. A Reader has access to some extra +piece of data floating around. Whatever is running in the inner monad doesn’t +know about that extra piece of information. So how would you do a lift? You +just ignore that extra information. The Writer monad? Don’t write anything. +State? Don’t change anything. I’m seeing a pattern here.

+

But now let’s try and go in the opposite direction: I have something in a +Reader, and I’d like to run it in the base monad (e.g., IO). Well… that’s not +going to work, is it? I need that extra piece of information, I’m relying on +it, and it’s not there. There’s simply no way to go in the opposite direction +without providing that extra value.

+

Or is there? If you remember, we’d pointed out earlier that ErrorT is just a +simple wrapper around the inner monad. In other words, if I have errorValue +:: ErrorT MyError IO MyValue, I can apply runErrorT and get a value of +type IO (Either MyError MyValue). The looks quite a bit like bi-directional +translation, doesn’t it?

+

Well, not quite. We originally had an ErrorT MyError IO monad, with a value +of type MyValue. Now we have a monad of type IO with a value of type +Either MyError MyValue. So this process has in fact changed the value, while +the lifting process leaves it the same.

+

But still, with a little fancy footwork we can unwrap the ErrorT, do some +processing, and then wrap it back up again.

+
useMyFileError1 :: ErrorT MyError IO ()
+useMyFileError1 =
+    let unwrapped :: Handle -> IO (Either MyError ())
+        unwrapped handle = runErrorT $ sayHiError handle
+        applied :: IO (Either MyError ())
+        applied = withMyFile unwrapped
+        rewrapped :: ErrorT MyError IO ()
+        rewrapped = ErrorT applied
+     in rewrapped
+

This is the crucial point of this whole article, so look closely. We first +unwrap our monad. This means that, to the outside world, it’s now just a plain +old IO value. Internally, we’ve stored all the information from our ErrorT +transformer. Now that we have a plain old IO, we can easily pass it off to +withMyFile. withMyFile takes in the internal state and passes it back out +unchanged. Finally, we wrap everything back up into our original ErrorT.

+

This is the entire pattern of monad-control: we embed the extra features of our +monad transformer inside the value. Once in the value, the type system ignores +it and focuses on the inner monad. When we’re done playing around with that +inner monad, we can pull our state back out and reconstruct our original monad +stack.

+
+
+

Types

+

I purposely started with the ErrorT transformer, as it is one of the simplest +for this inversion mechanism. Unfortunately, others are a bit more complicated. +Take for instance ReaderT. It is defined as newtype ReaderT r m a = ReaderT { +runReaderT :: r -> m a }. If we apply runReaderT to it, we get a +function that returns a monadic value. So we’re going to need some extra +machinery to deal with all that stuff. And this is when we leave Kansas behind.

+

There are a few approaches to solving these problems. In the past, I +implemented a solution using type families in the neither package. Anders +Kaseorg implemented a much more straight-forward solution in monad-peel. And +for efficiency, in monad-control, Bas van Dijk uses CPS (continuation passing +style) and existential types.

+ +

The first type we’re going to look at is:

+
type Run t = forall n o b. (Monad n, Monad o, Monad (t o)) => t n b -> n (t o b)
+

That’s incredibly dense, let’s talk it out. The only "input" datatype to this +thing is t, a monad transformer. A Run is a function that will then work with +any combination of types n, o and b (that’s what the forall means). n and o +are both monads, while b is a simple value contained by them.

+

The left hand side of the Run function, t n b, is our monad transformer +wrapped around the n monad and holding a b value. So for example, that could be +a MyTrans FirstMonad MyValue. It then returns a value with the transformer +"popped" inside, with a brand new monad at its core. In other words, +FirstMonad (MyTrans NewMonad MyValue).

+

That might sound pretty scary at first, but it actually isn’t as foreign as +you’d think: this is essentially what we did with ErrorT. We started with +ErrorT on the outside, wrapping around IO, and ended up with an IO by itself +containing an Either. Well guess what: another way to represent an Either is +ErrorT MyError Identity. So essentially, we pulled the IO to the outside and +plunked an Identity in its place. We’re doing the same thing in a Run: pulling +the FirstMonad outside and replacing it with a NewMonad.

+ +

Alright, now we’re getting somewhere. If we had access to one of those Run +functions, we could use it to peel off the ErrorT on our sayHiError function +and pass it to withMyFile. With the magic of undefined, we can play such a +game:

+
errorRun :: Run (ErrorT MyError)
+errorRun = undefined
+
+useMyFileError2 :: IO (ErrorT MyError Identity ())
+useMyFileError2 =
+    let afterRun :: Handle -> IO (ErrorT MyError Identity ())
+        afterRun handle = errorRun $ sayHiError handle
+        applied :: IO (ErrorT MyError Identity ())
+        applied = withMyFile afterRun
+     in applied
+

This looks eerily similar to our previous example. In fact, errorRun is acting +almost identically to runErrorT. However, we’re still left with two problems: +we don’t know where to get that errorRun value from, and we still need to +restructure the original ErrorT after we’re done.

+
+

MonadTransControl

+

Obviously in the specific case we have before us, we could use our knowledge of +the ErrorT transformer to beat the types into submission and create our Run +function manually. But what we really want is a general solution for many +transformers. At this point, you know we need a typeclass.

+

So let’s review what we need: access to a Run function, and some way to +restructure our original transformer after the fact. And thus was born +MonadTransControl, with its single method liftControl:

+
class MonadTrans t => MonadTransControl t where
+    liftControl :: Monad m => (Run t -> m a) -> t m a
+

Let’s look at this closely. liftControl takes a function (the one we’ll be +writing). That function is provided with a Run function, and must return a +value in some monad (m). liftControl will then take the result of that function +and reinstate the original transformer on top of everything.

+
useMyFileError3 :: Monad m => ErrorT MyError IO (ErrorT MyError m ())
+useMyFileError3 =
+    liftControl inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

Close, but not exactly what I had in mind. What’s up with the double monads? +Well, let’s start at the end: sayHiError handle returns a value of type ErrorT +MyError IO (). This we knew already, no surprises. What might be a little +surprising (it got me, at least) is the next two steps.

+

First we apply run to that value. Like we’d discussed before, the result is +that the IO inner monad is popped to the outside, to be replaced by some +arbitrary monad (represented by m here). So we end up with an IO (ErrorT +MyError m ()). Ok… We then get the same result after applying withMyFile. Not +surprising.

+

The last step took me a long time to understand correctly. Remember how we said +that we reconstruct the original transformer? Well, so we do: by plopping it +right on top of everything else we have. So our end result is the previous +type- IO (ErrorT MyError m ())- with a ErrorT MyError stuck on the front.

+

Well, that seems just about utterly worthless, right? Well, almost. But don’t +forget, that "m" can be any monad, including IO. If we treat it that way, we +get ErrorT MyError IO (ErrorT MyError IO ()). That looks a lot like m (m +a), and we want just plain old m a. Fortunately, now we’re in luck:

+
useMyFileError4 :: ErrorT MyError IO ()
+useMyFileError4 = join useMyFileError3
+

And it turns out that this usage is so common, that Bas had mercy on us and +defined a helper function:

+
control :: (Monad m, Monad (t m), MonadTransControl t)
+        => (Run t -> m (t m a)) -> t m a
+control = join . liftControl
+

So all we need to write is:

+
useMyFileError5 :: ErrorT MyError IO ()
+useMyFileError5 =
+    control inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

And just to make it a little shorter:

+
useMyFileError6 :: ErrorT MyError IO ()
+useMyFileError6 = control $ \run -> withMyFile $ run . sayHiError
+
+
+

MonadControlIO

+

The MonadTrans class provides the lift method, which allows you to lift an +action one level in the stack. There is also the MonadIO class that provides +liftIO, which lifts an IO action as far in the stack as desired. We have the +same breakdown in monad-control. But first, we need a corrolary to Run:

+
type RunInBase m base = forall b. m b -> base (m b)
+

Instead of dealing with a transformer, we’re dealing with two monads. base is +the underlying monad, and m is a stack built on top of it. RunInBase is a +function that takes a value of the entire stack, pops out that base, and puts +in on the outside. Unlike in the Run type, we don’t replace it with an +arbitrary monad, but with the original one. To use some more concrete types:

+
RunInBase (ErrorT MyError IO) IO = forall b. ErrorT MyError IO b -> IO (ErrorT MyError IO b)
+

This should look fairly similar to what we’ve been looking at so far, the only +difference is that we want to deal with a specific inner monad. Our +MonadControlIO class is really just an extension of MonadControlTrans using +this RunInBase.

+
class MonadIO m => MonadControlIO m where
+    liftControlIO :: (RunInBase m IO -> IO a) -> m a
+

Simply put, liftControlIO takes a function which receives a RunInBase. That +RunInBase can be used to strip down our monad to just an IO, and then +liftControlIO builds everything back up again. And like MonadControlTrans, it +comes with a helper function

+
controlIO :: MonadControlIO m => (RunInBase m IO -> IO (m a)) -> m a
+controlIO = join . liftControlIO
+

We can easily rewrite our previous example with it:

+
useMyFileError7 :: ErrorT MyError IO ()
+useMyFileError7 = controlIO $ \run -> withMyFile $ run . sayHiError
+

And as an advantage, it easily scales to multiple transformers:

+
sayHiCrazy :: Handle -> ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+sayHiCrazy handle = liftIO $ hPutStrLn handle "Madness!"
+
+useMyFileCrazy :: ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+useMyFileCrazy = controlIO $ \run -> withMyFile $ run . sayHiCrazy
+
+
+
+

Real Life Examples

+

Let’s solve some real-life problems with this code. Probably the biggest +motivating use case is exception handling in a transformer stack. For example, +let’s say that we want to automatically run some cleanup code when an exception +is thrown. If this were normal IO code, we’d use:

+
onException :: IO a -> IO b -> IO a
+

But if we’re in the ErrorT monad, we can’t pass in either the action or the +cleanup. In comes controlIO to the rescue:

+
onExceptionError :: ErrorT MyError IO a
+                 -> ErrorT MyError IO b
+                 -> ErrorT MyError IO a
+onExceptionError action after = controlIO $ \run ->
+    run action `onException` run after
+

Let’s say we need to allocate some memory to store a Double in. In the IO +monad, we could just use the alloca function. Once again, our solution is +simple:

+
allocaError :: (Ptr Double -> ErrorT MyError IO b)
+            -> ErrorT MyError IO b
+allocaError f = controlIO $ \run -> alloca $ run . f
+
+
+

Lost State

+

Let’s rewind a bit to our onExceptionError. It uses onException under the +surface, which has a type signature: IO a -> IO b -> IO a. Let me ask +you something: what happened to the b in the output? Well, it was thoroughly +ignored. But that seems to cause us a bit of a problem. After all, we store our +transformer state information in the value of the inner monad. If we ignore it, +we’re essentially ignoring the monadic side effects as well!

+

And the answer is that, yes, this does happen with monad-control. Certain functions will drop some of the monadic side effects. This is put best by Bas, in the comments on the relevant functions:[quote]

+
+

Note, any monadic side effects in m of the "release" computation will be discarded; it is run only for its side effects in IO.

+
+

In practice, monad-control will usually be doing the right thing for you, but +you need to be aware that some side effects may disappear.

+
+
+

More Complicated Cases

+

In order to make our tricks work so far, we’ve needed to have functions that +give us full access to play around with their values. Sometimes, this isn’t the +case. Take, for instance:

+
addMVarFinalizer :: MVar a -> IO () -> IO ()
+

In this case, we are required to have no value inside our finalizer function. +Intuitively, the first thing we should notice is that there will be no way to +capture our monadic side effects. So how do we get something like this to +compile? Well, we need to explicitly tell it to drop all of its state-holding +information:

+
addMVarFinalizerError :: MVar a -> ErrorT MyError IO () -> ErrorT MyError IO ()
+addMVarFinalizerError mvar f = controlIO $ \run ->
+    return $ liftIO $ addMVarFinalizer mvar (run f >> return ())
+

Another case from the same module is:

+
modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b
+

Here, we have a restriction on the return type in the second argument: it must +be a tuple of the value passed to that function and the final return value. +Unfortunately, I can’t see a way of writing a little wrapper around modifyMVar +to make it work for ErrorT. Instead, in this case, I copied the definition of +modifyMVar and modified it:

+
modifyMVar :: MVar a
+           -> (a -> ErrorT MyError IO (a, b))
+           -> ErrorT MyError IO b
+modifyMVar m io =
+  Control.Exception.Control.mask $ \restore -> do
+    a      <- liftIO $ takeMVar m
+    (a',b) <- restore (io a) `onExceptionError` liftIO (putMVar m a)
+    liftIO $ putMVar m a'
+    return b
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/persistent.html b/public/book-1.2/persistent.html new file mode 100644 index 00000000..1d3829c3 --- /dev/null +++ b/public/book-1.2/persistent.html @@ -0,0 +1,1514 @@ + Persistent :: Yesod Web Framework Book- Version 1.2 +
+

Persistent

+ + + +

Forms deal with the boundary between the user and the application. Another +boundary we need to deal with is between the application and the storage layer. +Whether it be a SQL database, a YAML file, or a binary blob, odds are your +storage layer does not natively understand your applications data types, and +you’ll need to perform some marshaling. Persistent is Yesod’s answer to data +storage- a type-safe, universal data store interface for Haskell.

+

Haskell has many different database bindings available. However, most of these +have little knowledge of a schema and therefore do not provide useful static +guarantees. They also force database-dependent APIs and data types on the +programmer.

+

Some Haskellers have attempted a more revolutionary route: creating Haskell +specific data stores that allow one to easily store any strongly typed Haskell +data. These options are great for certain use cases, but they constrain one to +the storage techniques provided by the library and do not interface well with +other languages.

+

In contrast, Persistent allows us to choose among existing databases that are +highly tuned for different data storage use cases, interoperate with other +programming languages, and to use a safe and productive query interface, while +still keeping the type safety of Haskell datatypes.

+

Persistent follows the guiding principles of type safety and concise, +declarative syntax. Some other nice features are:

+
    +
  • +

    +Database-agnostic. There is first class support for PostgreSQL, SQLite, MySQL + and MongoDB, with experimental Redis support. +

    +
  • +
  • +

    +Convenient data modeling. + Persistent lets you model relationships and use them in type-safe ways. + The default type-safe persistent API does not support joins, allowing support for a + wider number of storage layers. + Joins and other SQL specific functionality can be achieved through using + a raw SQL layer (with very little type safety). + An additional library, Esqueleto, + builds on top of the Persistent data model, adding type-safe joins and SQL functionality. +

    +
  • +
  • +

    +Automatically perform database migrations +

    +
  • +
+

Persistent works well with Yesod, but it is quite +usable on its own as a standalone library. Most of this chapter will address +Persistent on its own.

+
+

Synopsis

+
{-# LANGUAGE EmptyDataDecls    #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE GADTs             #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Sqlite
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+    janeId <- insert $ Person "Jane Doe" Nothing
+
+    insert $ BlogPost "My fr1st p0st" johnId
+    insert $ BlogPost "One more for good measure" johnId
+
+    oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+    liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+    john <- get johnId
+    liftIO $ print (john :: Maybe Person)
+
+    delete janeId
+    deleteWhere [BlogPostAuthorId ==. johnId]
+
+
+

Solving the boundary issue

+

Suppose you are storing information on people in a SQL database. Your table +might look something like:

+
CREATE TABLE person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)
+

And if you are using a database like PostgreSQL, you can be guaranteed that the +database will never store some arbitrary text in your age field. (The same +cannot be said of SQLite, but let’s forget about that for now.) To mirror this +database table, you would likely create a Haskell datatype that looks something +like:

+
data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+

It looks like everything is type safe: the database schema matches our Haskell +datatypes, the database ensures that invalid data can never make it into our +data store, and everything is generally awesome. Well, until:

+
    +
  • +

    +You want to pull data from the database, and the database layer gives you the + data in an untyped format. +

    +
  • +
  • +

    +You want to find everyone older than 32, and you accidentally write "thirtytwo" + in your SQL statement. Guess what: that will compile just fine, and you won’t + find out you have a problem until runtime. +

    +
  • +
  • +

    +You decide you want to find the first 10 people alphabetically. No problem… + until you make a typo in your SQL. Once again, you don’t find out until + runtime. +

    +
  • +
+

In dynamic languages, the answer to these issues is unit testing. For +everything that can go wrong, make sure you write a test case. But as I am +sure you are aware by now, that doesn’t jive well with the Yesod approach to +things. We like to take advantage of Haskell’s strong typing to save us +wherever possible, and data storage is no exception.

+

So the question remains: how can we use Haskell’s type system to save the day?

+
+

Types

+

Like routing, there is nothing intrinsically difficult about type-safe data +access. It just requires a lot of monotonous, error prone, boiler plate code. +As usual, this means we can use the type system to keep us honest. And to avoid +some of the drudgery, we’ll use a sprinkling of Template Haskell.

+ +

PersistValue is the basic building block of Persistent. It is a sum type that +can represent data that gets sent to and from a database. Its definition is:

+
data PersistValue = PersistText Text
+                  | PersistByteString ByteString
+                  | PersistInt64 Int64
+                  | PersistDouble Double
+                  | PersistRational Rational
+                  | PersistBool Bool
+                  | PersistDay Day
+                  | PersistTimeOfDay TimeOfDay
+                  | PersistUTCTime UTCTime
+                  | PersistZonedTime ZT
+                  | PersistNull
+                  | PersistList [PersistValue]
+                  | PersistMap [(Text, PersistValue)]
+                  | PersistObjectId ByteString -- ^ intended especially for MongoDB backend
+

Each Persistent backend needs to know how to translate the relevant values into +something the database can understand. However, it would be awkward to have to +express all of our data simply in terms of these basic types. The next layer is +the PersistField typeclass, which defines how an arbitrary Haskell datatype +can be marshaled to and from a PersistValue. A PersistField correlates to a +column in a SQL database. In our person example above, name and age would be +our PersistFields.

+

To tie up the user side of the code, our last typeclass is PersistEntity. An +instance of PersistEntity correlates with a table in a SQL database. This +typeclass defines a number of functions and some associated types. To review, +we have the following correspondence between Persistent and SQL:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SQLPersistent

Datatypes (VARCHAR, INTEGER, etc)

PersistValue

Column

PersistField

Table

PersistEntity

+
+
+

Code Generation

+

In order to ensure that the PersistEntity instances match up properly with your +Haskell datatypes, Persistent takes responsibility for both. This is also good +from a DRY (Don’t Repeat Yourself) perspective: you only need to define your +entities once. Let’s see a quick example:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+mkPersist sqlSettings [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+

We use a combination of Template Haskell and Quasi-Quotation (like when +defining routes): persistLowerCase is a quasi-quoter which converts a +whitespace-sensitive syntax into a list of entity definitions. "Lower case" +refers to the format of the generated table names. In this scheme, an +entity like SomeTable would become the SQL table some_table. You can also +declare your entities in a separate file using persistFileWith. mkPersist +takes that list of entities and declares:

+
    +
  • +

    +One Haskell datatype for each entity. +

    +
  • +
  • +

    +A PersistEntity instance for each datatype defined. +

    +
  • +
+

The example above generates code that looks like the following:

+
{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+import Control.Applicative
+
+data Person = Person
+    { personName :: !String
+    , personAge :: !Int
+    }
+  deriving (Show, Read, Eq)
+
+type PersonId = Key Person
+
+instance PersistEntity Person where
+    -- 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 Unique Person
+    type PersistEntityBackend Person = SqlBackend
+
+    toPersistFields (Person name age) =
+        [ SomePersistField name
+        , SomePersistField age
+        ]
+
+    fromPersistValues [nameValue, ageValue] = Person
+        <$> fromPersistValue nameValue
+        <*> fromPersistValue ageValue
+    fromPersistValues _ = Left "Invalid fromPersistValues input"
+
+    -- Information on each field, used internally to generate SQL statements
+    persistFieldDef PersonId = FieldDef
+        (HaskellName "Id")
+        (DBName "id")
+        (FTTypeCon Nothing "PersonId")
+        SqlInt64
+        []
+        True
+        Nothing
+    persistFieldDef PersonName = FieldDef
+        (HaskellName "name")
+        (DBName "name")
+        (FTTypeCon Nothing "String")
+        SqlString
+        []
+        True
+        Nothing
+    persistFieldDef PersonAge = FieldDef
+        (HaskellName "age")
+        (DBName "age")
+        (FTTypeCon Nothing "Int")
+        SqlInt64
+        []
+        True
+        Nothing
+

As you might expect, our Person datatype closely matches the definition we +gave in the original Template Haskell version. We also have a Generalized +Algebraic Datatype (GADT) which gives a separate constructor for each field. +This GADT encodes both the type of the entity and the type of the field. We use +its constructors throughout Persistent, such as to ensure that when we apply a +filter, the types of the filtering value match the field.

+

We can use the generated Person type like any other Haskell type, and then +pass it off to other Persistent functions.

+
{-# LANGUAGE EmptyDataDecls    #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE GADTs             #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Sqlite
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    michaelId <- insert $ Person "Michael" $ Just 26
+    michael <- get michaelId
+    liftIO $ print michael
+

We start off with some standard database connection code. In this case, we used +the single-connection functions. Persistent also comes built in with connection +pool functions, which we will generally want to use in production.

+

In this example, we have seen two functions: insert creates a new record in +the database and returns its ID. Like everything else in Persistent, IDs are +type safe. We’ll get into more details of how these IDs work later. So when you +call insert $ Person "Michael" 26, it gives you a value back of type +PersonId.

+

The next function we see is get, which attempts to load a value from the +database using an Id. In Persistent, you never need to worry that you are +using the key from the wrong table: trying to load up a different entity (like +House) using a PersonId will never compile.

+
+
+

PersistStore

+

One last detail is left unexplained from the previous example: what exactly +does runSqlite do, and what is that monad that our database actions are +running in?

+

All database actions need to occur within an instance of PersistStore. As its +name implies, every data store (PostgreSQL, SQLite, MongoDB) has an instance of +PersistStore. This is where all the translations from PersistValue to +database-specific values occur, where SQL query generation happens, and so on.

+ +

runSqlite creates a single connection to a database using its supplied +connection string. For our test cases, we will use :memory:, which uses an +in-memory database. All of the SQL backends share the same instance of +PersistStore: SqlPersist. runSqlite runs the SqlPersist action by +providing it with the connection value it generated.

+ +

One important thing to note is that everything which occurs inside a single +call to runSqlite runs in a single transaction. This has two important +implications:

+
    +
  • +

    +For many databases, committing a transaction can be a costly activity. By + putting multiple steps into a single transaction, you can speed up code + dramatically. +

    +
  • +
  • +

    +If an exception is thrown anywhere inside a single call to runSqlite, all + actions will be rolled back (assuming your backend has rollback support). +

    + +
  • +
+
+
+
+

Migrations

+

I’m sorry to tell you, but so far I have lied to you a bit: the example from +the previous section does not actually work. If you try to run it, you will get +an error message about a missing table.

+

For SQL databases, one of the major pains can be managing schema changes. +Instead of leaving this to the user, Persistent steps in to help, but you have +to ask it to help. Let’s see what this looks like:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+share [mkPersist sqlSettings, mkSave "entityDefs"] [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    -- this line added: that's it!
+    runMigration $ migrate entityDefs $ entityDef (Nothing :: Maybe Person)
+    michaelId <- insert $ Person "Michael" 26
+    michael <- get michaelId
+    liftIO $ print michael
+

With this one little code change, Persistent will automatically create your +Person table for you. This split between runMigration and migrate allows +you to migrate multiple tables simultaneously.

+

This works when dealing with just a few entities, but can quickly get tiresome +once we are dealing with a dozen entities. Instead of repeating yourself, +Persistent provides a helper function, mkMigrate:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+Car
+    color String
+    make String
+    model String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do runMigration migrateAll
+

mkMigrate is a Template Haskell function which creates a new function that +will automatically call migrate on all entities defined in the persist +block. The share function is just a little helper that passes the information +from the persist block to each Template Haskell function and concatenates the +results.

+

Persistent has very conservative rules about what it will do during a +migration. It starts by loading up table information from the database, +complete with all defined SQL datatypes. It then compares that against the +entity definition given in the code. For the following cases, it will +automatically alter the schema:

+
    +
  • +

    +The datatype of a field changed. However, the database may object to this + modification if the data cannot be translated. +

    +
  • +
  • +

    +A field was added. However, if the field is not null, no default value is + supplied (we’ll discuss defaults later) and there is already data in the + database, the database will not allow this to happen. +

    +
  • +
  • +

    +A field is converted from not null to null. In the opposite case, Persistent + will attempt the conversion, contingent upon the database’s approval. +

    +
  • +
  • +

    +A brand new entity is added. +

    +
  • +
+

However, there are some cases that Persistent will not handle:

+
    +
  • +

    +Field or entity renames: Persistent has no way of knowing that "name" has now + been renamed to "fullName": all it sees is an old field called name and a new + field called fullName. +

    +
  • +
  • +

    +Field removals: since this can result in data loss, Persistent by default + will refuse to perform the action (you can force the issue by using + runMigrationUnsafe instead of runMigration, though it is not + recommended). +

    +
  • +
+

runMigration will print out the migrations it is running on stderr (you can +bypass this by using runMigrationSilent). Whenever possible, it uses ALTER +TABLE calls. However, in SQLite, ALTER TABLE has very limited abilities, and +therefore Persistent must resort to copying the data from one table to another.

+

Finally, if instead of performing a migration, you want Persistent to give +you hints about what migrations are necessary, use the printMigration +function. This function will print out the migrations which runMigration +would perform for you. This may be useful for performing migrations that +Persistent is not capable of, for adding arbitrary SQL to a migration, or just +to log what migrations occurred.

+
+
+

Uniqueness

+

In addition to declaring fields within an entity, you can also declare +uniqueness constraints. A typical example would be requiring that a username be +unique.

+
User
+    username Text
+    UniqueUsername username
+

While each field name must begin with a lowercase letter, the uniqueness +constraints must begin with an uppercase letter, since it will be represented +in Haskell as a data constructor.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+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|
+Person
+    firstName String
+    lastName String
+    age Int
+    PersonName firstName lastName
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael" "Snoyman" 26
+    michael <- getBy $ PersonName "Michael" "Snoyman"
+    liftIO $ print michael
+

To declare a unique combination of fields, we add an extra line to our +declaration. Persistent knows that it is defining a unique constructor, since +the line begins with a capital letter. Each following word must be a field in +this entity.

+

The main restriction on uniqueness is that it can only be applied non-null +fields. The reason for this is that the SQL standard is ambiguous on how +uniqueness should be applied to NULL (e.g., is NULL=NULL true or false?). +Besides that ambiguity, most SQL engines in fact implement rules which would be +contrary to what the Haskell datatypes anticipate (e.g., PostgreSQL says that +NULL=NULL is false, whereas Haskell says Nothing == Nothing is True).

+

In addition to providing nice guarantees at the database level about +consistency of your data, uniqueness constraints can also be used to perform +some specific queries within your Haskell code, like the getBy demonstrated +above. This happens via the Unique associated type. In the example above, we +end up with a new constructor:

+
PersonName :: String -> String -> Unique Person
+ +
+
+

Queries

+

Depending on what your goal is, there are different approaches to querying the +database. Some commands query based on a numeric ID, while others will filter. +Queries also differ in the number of results they return: some lookups should +return no more than one result (if the lookup key is unique) while others can +return many results.

+

Persistent therefore provides a few different query functions. As usual, we try +to encode as many invariants in the types as possible. For example, a query +that can return only 0 or 1 results will use a Maybe wrapper, whereas a query +returning many results will return a list.

+
+

Fetching by ID

+

The simplest query you can perform in Persistent is getting based on an ID. +Since this value may or may not exist, its return type is wrapped in a Maybe.

+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    maybePerson <- get personId
+    case maybePerson of
+        Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+        Just person -> liftIO $ print person
+

This can be very useful for sites that provide URLs like /person/5. However, +in such a case, we don’t usually care about the Maybe wrapper, and just want +the value, returning a 404 message if it is not found. Fortunately, the +get404 (provided by the yesod-persistent package) function helps us out here. +We’ll go into more details when we see integration with Yesod.

+
+
+

Fetching by unique constraint

+

getBy is almost identical to get, except:

+
    +
  1. +

    +it takes a uniqueness constraint; that is, instead of an ID it takes a Unique value. +

    +
  2. +
  3. +

    +it returns an Entity instead of a value. An Entity is a combination of database ID and value. +

    +
  4. +
+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    maybePerson <- getBy $ UniqueName "Michael" "Snoyman"
+    case maybePerson of
+        Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+        Just (Entity personId person) -> liftIO $ print person
+

Like get404, there is also a getBy404 function.

+
+
+

Select functions

+

Most likely, you’re going to want more powerful queries. You’ll want to find +everyone over a certain age; all cars available in blue; all users without a +registered email address. For this, you need one of the select functions.

+

All the select functions use a similar interface, with slightly different outputs:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionReturns

selectSource

A Source 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 +recommend reading the +School +of Haskell 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.

+

selectList is the most commonly used, so we will cover it specifically. Understanding the others should be trivial after that.

+

selectList takes two arguments: a list of Filters, and a list of +SelectOpts. The former is what limits your results based on +characteristics; it allows for equals, less than, is member of, and such. +SelectOpts provides for three different features: sorting, limiting output +to a certain number of rows, and offsetting results by a certain number of +rows.

+ +

Let’s jump straight into an example of filtering, and then analyze it.

+
    people <- selectList [PersonAge >. 25, PersonAge <=. 30] []
+    liftIO $ print people
+

As simple as that example is, we really need to cover three points:

+
    +
  1. +

    +PersonAge is a constructor for an associated phantom type. That might sound +scary, but what’s important is that it uniquely identifies the "age" column of +the "person" table, and that it knows that the age field is an Int. (That’s +the phantom part.) +

    +
  2. +
  3. +

    +We have a bunch of Persistent filtering operators. They’re all pretty +straight-forward: just tack a period to the end of what you’d expect. There are +three gotchas here, I’ll explain below. +

    +
  4. +
  5. +

    +The list of filters is ANDed together, so that our constraint means "age is +greater than 25 AND age is less than or equal to 30". We’ll describe ORing +later. +

    +
  6. +
+

The one operator that’s surprisingly named is "not equals." We use !=., since +/=. is used for updates (for "divide-and-set", described later). Don’t worry: +if you use the wrong one, the compiler will catch you. The other two surprising +operators are the "is member" and "is not member". They are, respectively, +←. and /←. (both end with a period).

+

And regarding ORs, we use the ||. operator. For example:

+
    people <- selectList
+        (       [PersonAge >. 25, PersonAge <=. 30]
+            ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
+            ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
+        )
+        []
+    liftIO $ print people
+

This (completely nonsensical) example means: find people who are 26-30, +inclusive, OR whose names are neither Adam or Bonny, OR whose age is either 50 +or 60.

+
+

SelectOpt

+

All of our selectList calls have included an empty list as the second +parameter. That specifies no options, meaning: sort however the database wants, +return all results, and don’t skip any results. A SelectOpt has four +constructors that can be used to change all that.

+
+
+Asc +
+

+Sort by the given column in ascending order. This uses the same phantom type as filtering, such as PersonAge. +

+
+
+Desc +
+

+Same as Asc, in descending order. +

+
+
+LimitTo +
+

+Takes an Int argument. Only return up to the specified number of results. +

+
+
+OffsetBy +
+

+Takes an Int argument. Skip the specified number of results. +

+
+
+

The following code defines a function that will break down results into pages. +It returns all people aged 18 and over, and then sorts them by age (oldest +person first). For people with the same age, they are sorted alphabetically by +last name, then first name.

+
resultsForPage pageNumber = do
+    let resultsPerPage = 10
+    selectList
+        [ PersonAge >=. 18
+        ]
+        [ Desc PersonAge
+        , Asc PersonLastName
+        , Asc PersonFirstName
+        , LimitTo resultsPerPage
+        , OffsetBy $ (pageNumber - 1) * resultsPerPage
+        ]
+
+
+
+
+

Manipulation

+

Querying is only half the battle. We also need to be able to add data to and +modify existing data in the database.

+
+

Insert

+

It’s all well and good to be able to play with data in the database, but how +does it get there in the first place? The answer is the insert function. You +just give it a value, and it gives back an ID.

+

At this point, it makes sense to explain a bit of the philosophy behind +Persistent. In many other ORM solutions, the datatypes used to hold data are +opaque: you need to go through their defined interfaces to get at and modify +the data. That’s not the case with Persistent: we’re using plain old Algebraic +Data Types for the whole thing. This means you still get all the great benefits +of pattern matching, currying and everything else you’re used to.

+

However, there are a few things we can’t do. For one, there’s no way to +automatically update values in the database every time the record is updated in +Haskell. Of course, with Haskell’s normal stance of purity and immutability, +this wouldn’t make much sense anyway, so I don’t shed any tears over it.

+

However, there is one issue that newcomers are often bothered by: why are IDs +and values completely separate? It seems like it would be very logical to embed +the ID inside the value. In other words, instead of having:

+
data Person = Person { name :: String }
+

have

+
data Person = Person { personId :: PersonId, name :: String }
+

Well, there’s one problem with this right off the bat: how do we do an insert? If a Person needs to have an ID, and we get the ID by inserting, and an insert needs a Person, we have an impossible loop. We could solve this with undefined, but that’s just asking for trouble.

+

OK, you say, let’s try something a bit safer:

+
data Person = Person { personId :: Maybe PersonId, name :: String }
+

I definitely prefer insert $ Person Nothing "Michael" to insert $ Person +undefined "Michael". And now our types will be much simpler, right? For +example, selectList could return a simple [Person] instead of that ugly +[Entity SqlPersist Person].

+

The problem is that the "ugliness" is incredibly useful. Having Entity Person +makes it obvious, at the type level, that we’re dealing with a value that +exists in the database. Let’s say we want to create a link to another page that +requires the PersonId (not an uncommon occurrence as we’ll discuss later). +The Entity Person form gives us unambiguous access to that information; +embedding PersonId within Person with a Maybe wrapper means an extra +runtime check for Just, instead of a more error-proof compile time check.

+

Finally, there’s a semantic mismatch with embedding the ID within the value. +The Person is the value. Two people are identical (in the context of a +database) if all their fields are the same. By embedding the ID in the value, +we’re no longer talking about a person, but about a row in the database. +Equality is no longer really equality, it’s identity: is this the same +person, as opposed to an equivalent person.

+

In other words, there are some annoyances with having the ID separated out, but +overall, it’s the right approach, which in the grand scheme of things leads +to better, less buggy code.

+
+
+

Update

+

Now, in the context of that discussion, let’s think about updating. The simplest way to update is:

+
let michael = Person "Michael" 26
+    michaelAfterBirthday = michael { personAge = 27 }
+

But that’s not actually updating anything, it’s just creating a new Person +value based on the old one. When we say update, we’re not talking about +modifications to the values in Haskell. (We better not be of course, since +data in Haskell is immutable.)

+

Instead, we’re looking at ways of modifying rows in a table. And the simplest +way to do that is with the update function.

+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    update personId [PersonAge =. 27]
+

update takes two arguments: an ID, and a list of Updates. The simplest +update is assignment, but it’s not always the best. What if you want to +increase someone’s age by 1, but you don’t have their current age? Persistent +has you covered:

+
haveBirthday personId = update personId [PersonAge +=. 1]
+

And as you might expect, we have all the basic mathematical operators: ++=., -=., \*=., and /=. (full stop). These can be convenient for +updating a single record, but they are also essential for proper ACID +guarantees. Imagine the alternative: pull out a Person, increment the age, +and update the new value. If you have two threads/processes working on this +database at the same time, you’re in for a world of hurt (hint: race +conditions).

+

Sometimes you’ll want to update many rows at once (give all your employees a +5% pay increase, for example). updateWhere takes two parameters: a list of +filters, and a list of updates to apply.

+
    updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- it's been a long day
+

Occasionally, you’ll just want to completely replace the value in a database +with a different value. For that, you use (surprise) the replace function.

+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    replace personId $ Person "John" "Doe" 20
+
+
+

Delete

+

As much as it pains us, sometimes we must part with our data. To do so, we have three functions:

+
+
+delete +
+

+Delete based on an ID +

+
+
+deleteBy +
+

+Delete based on a unique constraint +

+
+
+deleteWhere +
+

+Delete based on a set of filters +

+
+
+
    personId <- insert $ Person "Michael" "Snoyman" 26
+    delete personId
+    deleteBy $ UniqueName "Michael" "Snoyman"
+    deleteWhere [PersonFirstName ==. "Michael"]
+

We can even use deleteWhere to wipe out all the records in a table, we just +need to give some hints to GHC as to what table we’re interested in:

+
    deleteWhere ([] :: [Filter Person])
+
+
+
+

Attributes

+

So far, we have seen a basic syntax for our persistLowerCase blocks: a line +for the name of our entities, and then an indented line for each field with two +words: the name of the field and the datatype of the field. Persistent handles +more than this: you can assign an arbitrary list of attributes after the first +two words on a line.

+

Suppose we want to have a Person entity with an (optional) age and the +timestamp of when he/she was added to the system. For entities already in the +database, we want to just use the current date-time for that timestamp.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+import Control.Monad.IO.Class
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    time <- liftIO getCurrentTime
+    runMigration migrateAll
+    insert $ Person "Michael" (Just 26) time
+    insert $ Person "Greg" Nothing time
+    return ()
+

Maybe is a built in, single word attribute. It makes the field optional. In +Haskell, this means it is wrapped in a Maybe. In SQL, it makes the column +nullable.

+

The default attribute is backend specific, and uses whatever syntax is +understood by the database. In this case, it uses the database’s built-in +CURRENT_TIME function. Suppose that we now want to add a field for a person’s +favorite programming language:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+ +

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:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    country String "default='El Salvador'"
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+

One last trick you can do with attributes is to specify the names to be used +for the SQL tables and columns. This can be convenient when interacting with +existing databases.

+
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"
+    UniqueName firstName lastName
+    deriving Show
+|]
+

There are a number of other features to the entity definition syntax. An +up-to-date list is maintained +on the +Yesod wiki.

+
+
+

Relations

+

Persistent allows references between your data types in a manner that is +consistent with supporting non-SQL databases. We do this by embedding an ID in +the related entity. So if a person has many cars:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+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
+    name String
+    deriving Show
+Car
+    ownerId PersonId
+    name String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    bruce <- insert $ Person "Bruce Wayne"
+    insert $ Car bruce "Bat Mobile"
+    insert $ Car bruce "Porsche"
+    -- this could go on a while
+    cars <- selectList [CarOwnerId ==. bruce] []
+    liftIO $ print cars
+

Using this technique, you can define one-to-many relationships. To define +many-to-many relationships, we need a join entity, which has a one-to-many +relationship with each of the original tables. It is also a good idea to use +uniqueness constraints on these. For example, to model a situation where we +want to track which people have shopped in which stores:

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+Store
+    name String
+PersonStore
+    personId PersonId
+    storeId StoreId
+    UniquePersonStore personId storeId
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    bruce <- insert $ Person "Bruce Wayne"
+    michael <- insert $ Person "Michael"
+
+    target <- insert $ Store "Target"
+    gucci <- insert $ Store "Gucci"
+    sevenEleven <- insert $ Store "7-11"
+
+    insert $ PersonStore bruce gucci
+    insert $ PersonStore bruce sevenEleven
+
+    insert $ PersonStore michael target
+    insert $ PersonStore michael sevenEleven
+
+    return ()
+ +
+
+

Closer look at types

+

So far, we’ve spoken about Person and PersonId without really explaining +what they are. In the simplest sense, for a SQL-only system, the PersonId +could just be type PersonId = Int64. However, that means there is nothing +binding a PersonId at the type level to the Person entity. As a result, you +could accidentally use a PersonId and get a Car. In order to model this +relationship, we use phantom types. So, our next naive step would be:

+
newtype Key entity = Key Int64
+type PersonId = Key Person
+

And that works out really well, until you get to a backend that doesn’t use +Int64 for its IDs. And that’s not just a theoretical question; MongoDB uses +ByteStrings instead. So what we need is a key value that can contain an +Int and a ByteString. Seems like a great time for a sum type:

+
data Key entity = KeyInt Int64 | KeyByteString ByteString
+

But that’s just asking for trouble. Next we’ll have a backend that uses +timestamps, so we’ll need to add another constructor to Key. This could go on +for a while. Fortunately, we already have a sum type intended for representing +arbitrary data: PersistValue:

+
newtype Key entity = Key PersistValue
+

But this has another problem. Let’s say we have a web application that takes an +ID as a parameter from the user. It will need to receive that parameter as +Text and then try to convert it to a Key. Well, that’s simple: write a +function to convert a Text to a PersistValue, and then wrap the result in +the Key constructor, right?

+

Wrong. We tried this, and there’s a big problem. We end up getting Keys +that could never be. For example, if we’re dealing with SQL, a key must be an +integer. But the approach described above would allow arbitrary textual data +in. The result was a bunch of 500 server errors as the database choked on +comparing an integer column to text.

+

So what we need is a way to convert text to a Key, but have it dependent on +the rules of the backend in question. And once phrased that way, the answer is +simple: just add another phantom. The real, actual definition of Key in +Persistent is:

+
newtype KeyBackend backend entity = Key { unKey :: PersistValue }
+type Key val = KeyBackend (PersistEntityBackend val) val
+

What this slightly intimidating formulation says is: we have a type +KeyBackend which is parameterized on both the backend and entity. However, we +also have a simplified type Key which assumes the same backend for both the +entity and the key, which is almost always the correct assumption.

+

In practice, this works great: we can have a Text → KeyBackend MongoDB +entity function and a Text → KeyBackend SqlPersist entity function, and +everything runs smoothly.

+
+

More complicated, more generic

+

By default, Persistent will hard-code your datatypes to work with a specific +database backend. When using sqlSettings, this is the SqlBackend type. But +if you want to write Persistent code that can be used on multiple backends, you +can enable more generic types by replacing sqlSettings with sqlSettings { +mpsGeneric = True }.

+

To understand why this is necessary, consider relations. Let’s say we want to +represent blogs and blog posts. We would use the entity definition:

+
Blog
+    title Text
+Post
+    title Text
+    blogId BlogId
+

But what would that look like in terms of our Key datatype?

+
data Blog = Blog { blogTitle :: Text }
+data Post = Post { postTitle :: Text, postBlogId :: KeyBackend <what goes here?> Blog }
+

We need something to fill in as the backend. In theory, we could hardcode this +to SqlPersist, or Mongo, but then our datatypes will only work for a single +backend. For an individual application, that might be acceptable, but what +about libraries defining datatypes to be used by multiple applications, using +multiple backends?

+

So things got a little more complicated. Our types are actually:

+
data BlogGeneric backend = Blog { blogTitle :: Text }
+data PostGeneric backend = Post { postTitle :: Text, postBlogId :: KeyBackend backend (BlogGeneric backend) }
+

Notice that we still keep the short names for the constructors and the records. +Finally, to give a simple interface for normal code, we define some type +synonyms:

+
type Blog = BlogGeneric SqlPersist
+type BlogId = Key SqlPersist Blog
+type Post = PostGeneric SqlPersist
+type PostId = Key SqlPersist Post
+

And no, SqlPersist isn’t hard-coded into Persistent anywhere. That +sqlSettings parameter you’ve been passing to mkPersist is what tells us to +use SqlPersist. Mongo code will use mongoSettings instead.

+

This might be quite complicated under the surface, but user code hardly ever +touches this. Look back through this whole chapter: not once did we need to +deal with the Key or Generic stuff directly. The most common place for it +to pop up is in compiler error messages. So it’s important to be aware that +this exists, but it shouldn’t affect you on a day-to-day basis.

+
+
+
+

Custom Fields

+

Occasionally, you will want to define a custom field to be used in your +datastore. The most common case is an enumeration, such as employment status. +For this, Persistent provides a helper Template Haskell function:

+
-- @Employment.hs
+{-# LANGUAGE TemplateHaskell #-}
+module Employment where
+
+import Database.Persist.TH
+
+data Employment = Employed | Unemployed | Retired
+    deriving (Show, Read, Eq)
+derivePersistField "Employment"
+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, TemplateHaskell,
+             OverloadedStrings, GADTs, FlexibleContexts #-}
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Employment
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    employment Employment
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    insert $ Person "Bruce Wayne" Retired
+    insert $ Person "Peter Parker" Unemployed
+    insert $ Person "Michael" Employed
+
+    return ()
+

derivePersistField stores the data in the database using a string field, and +performs marshaling using the Show and Read instances of the datatype. This +may not be as efficient as storing via an integer, but it is much more future +proof: even if you add extra constructors in the future, your data will still +be valid.

+ +
+
+

Persistent: Raw SQL

+

The Persistent package provides a type safe interface to data stores. It tries +to be backend-agnostic, such as not relying on relational features of SQL. My +experience has been you can easily perform 95% of what you need to do with the +high-level interface. (In fact, most of my web apps use the high level +interface exclusively.)

+

But occassionally you’ll want to use a feature that’s specific to a backend. One feature I’ve used in the past is full text search. In this case, we’ll use the SQL "LIKE" operator, which is not modeled in Persistent. We’ll get all people with the last name "Snoyman" and print the records out.

+ +
{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving, GADTs, FlexibleContexts #-}
+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
+    name Text
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael Snoyman"
+    insert $ Person "Miriam Snoyman"
+    insert $ Person "Eliezer Snoyman"
+    insert $ Person "Gavriella Snoyman"
+    insert $ Person "Greg Weber"
+    insert $ Person "Rick Richardson"
+
+    -- 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)
+

There is also higher-level support that allows for automated data marshaling. +Please see the Haddock API docs for more details.

+
+
+

Integration with Yesod

+

So you’ve been convinced of the power of Persistent. How do you integrate it +with your Yesod application? If you use the scaffolding, most of the work is +done for you already. But as we normally do, we’ll build up everything manually +here to point out how it works under the surface.

+

The yesod-persistent package provides the meeting point between Persistent and +Yesod. It provides the YesodPersist typeclass, which standardizes access to +the database via the runDB method. Let’s see this in action.

+
{-# LANGUAGE QuasiQuotes, TypeFamilies, GeneralizedNewtypeDeriving, FlexibleContexts #-}
+{-# LANGUAGE TemplateHaskell, OverloadedStrings, GADTs, MultiParamTypeClasses #-}
+import Yesod
+import Database.Persist.Sqlite
+import Control.Monad.Trans.Resource (runResourceT)
+import Control.Monad.Logger (runStderrLoggingT)
+
+-- Define our entities as usual
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    firstName String
+    lastName String
+    age Int
+    deriving Show
+|]
+
+-- We keep our connection pool in the foundation. At program initialization, we
+-- create our initial pool, and each time we need to perform an action we check
+-- out a single connection from the pool.
+data PersistTest = PersistTest ConnectionPool
+
+-- We'll create a single route, to access a person. It's a very common
+-- occurrence to use an Id type in routes.
+mkYesod "PersistTest" [parseRoutes|
+/ HomeR GET
+/person/#PersonId PersonR GET
+|]
+
+-- Nothing special here
+instance Yesod PersistTest
+
+-- Now we need to define a YesodPersist instance, which will keep track of
+-- which backend we're using and how to run an action.
+instance YesodPersist PersistTest where
+    type YesodPersistBackend PersistTest = SqlPersistT
+
+    runDB action = do
+        PersistTest pool <- getYesod
+        runSqlPool action pool
+
+-- List all people in the database
+getHomeR :: Handler Html
+getHomeR = do
+    people <- runDB $ selectList [] [Asc PersonAge]
+    defaultLayout
+        [whamlet|
+            <ul>
+                $forall Entity personid person <- people
+                    <li>
+                        <a href=@{PersonR personid}>#{personFirstName person}
+        |]
+
+-- We'll just return the show value of a person, or a 404 if the Person doesn't
+-- exist.
+getPersonR :: PersonId -> Handler String
+getPersonR personId = do
+    person <- runDB $ get404 personId
+    return $ show person
+
+openConnectionCount :: Int
+openConnectionCount = 10
+
+main :: IO ()
+main = withSqlitePool "test.db3" openConnectionCount $ \pool -> do
+    runResourceT $ runStderrLoggingT $ flip runSqlPool pool $ do
+        runMigration migrateAll
+        insert $ Person "Michael" "Snoyman" 26
+    warp 3000 $ PersistTest pool
+

There are two important pieces here for general use. runDB is used to run a +DB action from within a Handler. Within the runDB, you can use any of the +functions we’ve spoken about so far, such as insert and selectList.

+ +

The other new feature is get404. It works just like get, but instead of +returning a Nothing when a result can’t be found, it returns a 404 message +page. The getPersonR function is a very common approach used in real-world +Yesod applications: get404 a value and then return a response based on it.

+
+
+

More complex SQL

+

Persistent strives to be backend-agnostic. The advantage of this approach is +code which easily moves from different backend types. The downside is that you +lose out on some backend-specific features. Probably the biggest casualty is +SQL join support.

+

Fortunately, thanks to Felipe Lessa, you can have your cake and eat it too. The +Esqueleto library provides +support for writing type safe SQL queries, using the existing Persistent +infrastructure. The Haddocks for that package provide a good introduction to +its usage. And since it uses many Persistent concepts, most of your existing +Persistent knowledge should transfer over easily.

+
+
+

Something besides SQLite

+

To keep the examples in this chapter simple, we’ve used the SQLite backend. Just to round things out, here’s our original synopsis rewritten to work with PostgreSQL:

+
{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE GADTs             #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Postgresql
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+connStr = "host=localhost dbname=test user=test password=test port=5432"
+
+main :: IO ()
+main = withPostgresqlPool connStr 10 $ \pool -> do
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        johnId <- insert $ Person "John Doe" $ Just 35
+        janeId <- insert $ Person "Jane Doe" Nothing
+
+        insert $ BlogPost "My fr1st p0st" johnId
+        insert $ BlogPost "One more for good measure" johnId
+
+        oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+        liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+        john <- get johnId
+        liftIO $ print (john :: Maybe Person)
+
+        delete janeId
+        deleteWhere [BlogPostAuthorId ==. johnId]
+
+
+

Summary

+

Persistent brings the type safety of Haskell to your data access layer. Instead +of writing error-prone, untyped data access, or manually writing boilerplate +marshal code, you can rely on Persistent to automate the process for you.

+

The goal is to provide everything you need, most of the time. For the times +when you need something a bit more powerful, Persistent gives you direct access +to the underlying data store, so you can write whatever 5-way joins you want.

+

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.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/restful-content.html b/public/book-1.2/restful-content.html new file mode 100644 index 00000000..3fbd1f38 --- /dev/null +++ b/public/book-1.2/restful-content.html @@ -0,0 +1,623 @@ + RESTful Content :: Yesod Web Framework Book- Version 1.2 +
+

RESTful Content

+ + +

One of the stories from the early days of the web is how search engines wiped +out entire websites. When dynamic web sites were still a new concept, +developers didn’t appreciate the difference between a GET and POST request. +As a result, they created pages- accessed with the GET method- that would +delete pages. When search engines started crawling these sites, they could wipe +out all the content.

+

If these web developers had followed the HTTP spec properly, this would not +have happened. A GET request is supposed to cause no side effects (you know, +like wiping out a site). Recently, there has been a move in web development to +properly embrace Representational State Transfer, also known as REST. This +chapter describes the RESTful features in Yesod and how you can use them to +create more robust web applications.

+
+

Request methods

+

In many web frameworks, you write one handler function per resource. In Yesod, +the default is to have a separate handler function for each request method. The +two most common request methods you will deal with in creating web sites are +GET and POST. These are the most well-supported methods in HTML, since they +are the only ones supported by web forms. However, when creating RESTful APIs, +the other methods are very useful.

+

Technically speaking, you can create whichever request methods you like, but it +is strongly recommended to stick to the ones spelled out in the HTTP spec. The +most common of these are:

+
+
+GET +
+

+Read-only requests. Assuming no other changes occur on the server, +calling a GET request multiple times should result in the same response, +barring such things as "current time" or randomly assigned results. +

+
+
+POST +
+

+A general mutating request. A POST request should never be submitted +twice by the user. A common example of this would be to transfer funds from one +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. +

+
+
+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 +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.

+

See the routing and handlers chapter for more information on the syntax +of defining handler functions for each request method.

+
+
+

Representations

+

Suppose we have a Haskell datatype and value:

+
data Person = Person { name :: String, age :: Int }
+michael = Person "Michael" 25
+

We could represent that data as HTML:

+
<table>
+    <tr>
+        <th>Name</th>
+        <td>Michael</td>
+    </tr>
+    <tr>
+        <th>Age</th>
+        <td>25</td>
+    </tr>
+</table>
+

or we could represent it as JSON:

+
{"name":"Michael","age":25}
+

or as XML:

+
<person>
+    <name>Michael</name>
+    <age>25</age>
+</person>
+

Often times, web applications will use a different URL to get each of these +representations; perhaps /person/michael.html, /person/michael.json, etc. +Yesod follows the RESTful principle of a single URL for each resource. So in +Yesod, all of these would be accessed from /person/michael.

+

Then the question becomes how do we determine which representation to serve. +The answer is the HTTP Accept header: it gives a prioritized list of content +types the client is expecting. Yesod provides a pair of functions to abstract +away the details of parsing that header directly, and instead allows you to +talk at a much higher level of representations. Let’s make that last sentence +a bit more concrete with some code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideRep $ return $ object
+        [ "name" .= name
+        , "age" .= age
+        ]
+  where
+    name = "Michael" :: Text
+    age = 28 :: Int
+
+main :: IO ()
+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 +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 +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 +little bit.

+

To test this out, start up the server and then try running the following +different curl commands:

+
curl http://localhost:3000 --header "accept: application/json"
+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 +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 +"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 +from your browser. To try this out, you can visit +http://localhost:3000/?_accept=application/json.

+
+

JSON conveniences

+

Since JSON is such a commonly used data format in web applications today, we +have some built-in helper functions for providing JSON representations. These +are built off of the wonderful aeson library, so let’s start off with a quick +explanation of how that library works.

+

aeson has a core datatype, Value, which represents any valid JSON value. It +also provides two typeclasses- ToJSON and FromJSON- to automate +marshaling to and from JSON values, respectively. For our purposes, we’re +currently interested in ToJSON. Let’s look at a quick example of creating a +ToJSON instance for our ever-recurring Person data type examples.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+import           Data.Aeson
+import qualified Data.ByteString.Lazy.Char8 as L
+import           Data.Text                  (Text)
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+main :: IO ()
+main = L.putStrLn $ encode $ Person "Michael" 28
+

I won’t go into further detail on aeson, as +the Haddock documentation +already provides a great introduction to the library. What I’ve described so +far is enough to understand our convenience functions.

+

Let’s suppose that you have such a Person datatype, with a corresponding +value, and you’d like to use it as the representation for your current page. +For that, you can use the returnJson function.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Value
+getHomeR = returnJson $ Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

returnJson is actually a trivial function; it is implemented as return . +toJSON. However, it makes things just a bit more convenient. Similarly, if you +would like to provide a JSON value as a representation inside a selectRep, +you can use provideJson.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideJson person
+  where
+    person@Person {..} = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

provideJson is similarly trivial, in this case provideRep . returnJson.

+
+
+

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 +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.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+getHomeR :: Handler TypedContent
+getHomeR =
+    return $ TypedContent mimeType $ toContent $ show person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

There are a few important things to note here.

+
    +
  • +

    +We’ve used the toContent function. This is a typeclass function that can + convert a number of data types to raw data ready to be sent over the wire. In + this case, we’ve used the instance for String, which uses UTF8 encoding. + Other common data types with instances are Text, ByteString, Html, and + 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 + alias for a strict ByteString. +

    +
  • +
+

That’s all well and good, but it bothers me that the type signature for +getHomeR is so uninformative. Also, the implementation of getHomeR looks +pretty boilerplate. I’d rather just have a datatype representing "Haskell Show" +data, and provide some simple means of creating such values. Let’s try this on +for size:

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+
+getHomeR :: Handler HaskellShow
+getHomeR =
+    return $ HaskellShow person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

The magic here lies in two typeclasses. As we mentioned before, ToContent +tells how to convert a value into a raw response. In our case, we would like to +show the original value to get a String, and then convert that String +into the raw content. Often times, instances of ToContent will build on each +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.

+

Finally, let’s make this a bit more complicated, and get this to play well with +selectRep.

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE RecordWildCards           #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+instance HasContentType HaskellShow where
+    getContentType _ = mimeType
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return $ HaskellShow person
+    provideJson person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+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. +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.

+ +
+
+
+

Other request headers

+

There are a great deal of other request headers available. Some of them only +affect the transfer of data between the server and client, and should not +affect the application at all. For example, Accept-Encoding informs the +server which compression schemes the client understands, and Host informs the +server which virtual host to serve up.

+

Other headers do affect the application, but are automatically read by Yesod. +For example, the Accept-Language header specifies which human language +(English, Spanish, German, Swiss-German) the client prefers. See the i18n +chapter for details on how this header is used.

+
+
+

Stateless

+

I’ve saved this section for the last, not because it is less important, but +rather because there are no specific features in Yesod to enforce this.

+

HTTP is a stateless protocol: each request is to be seen as the beginning of a +conversation. This means, for instance, it doesn’t matter to the server if you +requested five pages previously, it will treat your sixth request as if it’s +your first one.

+

On the other hand, some features on websites won’t work without some kind of +state. For example, how can you implement a shopping cart without saving +information about items in between requests?

+

The solution to this is cookies, and built on top of this, sessions. We have a +whole section addressing the sessions features in Yesod. However, I cannot +stress enough that this should be used sparingly.

+

Let me give you an example. There’s a popular bug tracking system that I deal +with on a daily basis which horribly abuses sessions. There’s a little +drop-down on every page to select the current project. Seems harmless, right? +What that dropdown does is set the current project in your session.

+

The result of all this is that clicking on the "view issues" link is entirely +dependent on the last project you selected. There’s no way to create a bookmark +to your "Yesod" issues and a separate link for your "Hamlet" issues.

+

The proper RESTful approach to this is to have one resource for all of the +Yesod issues and a separate one for all the Hamlet issues. In Yesod, this is +easily done with a route definition like:

+
/                    ProjectsR      GET
+/projects/#ProjectID ProjectIssuesR GET
+/issues/#IssueID     IssueR         GET
+

Be nice to your users: proper stateless architecture means that basic features +like bookmarks, permalinks and the back/forward button will always work.

+
+
+

Summary

+

Yesod adheres to the following tenets of REST:

+
    +
  • +

    +Use the correct request method. +

    +
  • +
  • +

    +Each resource should have precisely one URL. +

    +
  • +
  • +

    +Allow multiple representations of data on the same URL. +

    +
  • +
  • +

    +Inspect request headers to determine extra information about what the client wants. +

    +
  • +
+

This makes it easy to use Yesod not just for building websites, but for +building APIs. In fact, using techniques such as selectRep/provideRep, you +can serve both a user-friendly, HTML page and a machine-friendly, JSON page +from the same URL.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/route-attributes.html b/public/book-1.2/route-attributes.html new file mode 100644 index 00000000..0dad9b28 --- /dev/null +++ b/public/book-1.2/route-attributes.html @@ -0,0 +1,210 @@ + Route attributes :: Yesod Web Framework Book- Version 1.2 +
+

Route attributes

+ + +

Route attributes allow you to set some metadata on each of your routes, in the +routes description itself. The syntax is trivial: just an exclamation point +followed by a value. Using it is also trivial: just use the routeAttrs +function.

+

It’s easiest to understand how it all fits together, and when you might want it, with a motivating example. The case I personally most use this for is annotating administrative routes. Imagine having a website with about 12 different admin actions. You could manually add a call to requireAdmin or some such at the beginning of each action, but:

+
    +
  1. +

    +It’s tedious. +

    +
  2. +
  3. +

    +It’s error prone: you could easily forget one. +

    +
  4. +
  5. +

    +Worse yet, it’s not easy to notice that you’ve missed one. +

    +
  6. +
+

Modifying your isAuthorized method with an explicit list of administrative +routes is a bit better, but it’s still difficult to see at a glance when you’ve +missed one.

+

This is why I like to use route attributes for this: you add a single word to +each relevant part of the route definition, and then you just check for that +attribute in isAuthorized. Let’s see the code!

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/unprotected UnprotectedR GET
+/admin1 Admin1R GET !admin
+/admin2 Admin2R GET !admin
+/admin3 Admin3R GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+    isAuthorized route _writable
+        | "admin" `member` routeAttrs route = do
+            muser <- maybeAuthId
+            case muser of
+                Nothing -> return AuthenticationRequired
+                Just ident
+                    -- Just a hack since we're using the dummy module
+                    | ident == "admin" -> return Authorized
+                    | otherwise -> return $ Unauthorized "Admin access only"
+        | otherwise = return Authorized
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Hacky YesodAuth instance for just the dummy auth plugin
+instance YesodAuth App where
+    type AuthId App = Text
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authPlugins _ = [authDummy]
+    maybeAuthId = lookupSession credsKey
+    authHttpManager = error "no http manager provided"
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Route attr homepage"
+    [whamlet|
+        <p>
+            <a href=@{UnprotectedR}>Unprotected
+        <p>
+            <a href=@{Admin1R}>Admin 1
+        <p>
+            <a href=@{Admin2R}>Admin 2
+        <p>
+            <a href=@{Admin3R}>Admin 3
+    |]
+
+getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
+getUnprotectedR = defaultLayout [whamlet|Unprotected|]
+getAdmin1R = defaultLayout [whamlet|Admin1|]
+getAdmin2R = defaultLayout [whamlet|Admin2|]
+getAdmin3R = defaultLayout [whamlet|Admin3|]
+
+main :: IO ()
+main = warp 3000 App
+

And it was so glaring, I bet you even caught the security hole about Admin3R.

+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/routing-and-handlers.html b/public/book-1.2/routing-and-handlers.html new file mode 100644 index 00000000..708ae402 --- /dev/null +++ b/public/book-1.2/routing-and-handlers.html @@ -0,0 +1,756 @@ + Routing and Handlers :: Yesod Web Framework Book- Version 1.2 +
+

Routing and Handlers

+ + +

If we look at Yesod as a Model-View-Controller framework, routing and handlers +make up the controller. For contrast, let’s describe two other routing +approaches used in other web development environments:

+
    +
  • +

    +Dispatch based on file name. This is how PHP and ASP work, for example. +

    +
  • +
  • +

    +Have a centralized routing function that parses routes based on regular + expressions. Django and Rails follow this approach. +

    +
  • +
+

Yesod is closer in principle to the latter technique. Even so, there are +significant differences. Instead of using regular expressions, Yesod matches on +pieces of a route. Instead of having a one-way route-to-handler mapping, Yesod +has an intermediate data type (called the route datatype, or a type-safe URL) +and creates two-way conversion functions.

+

Coding this more advanced system manually is tedious and error prone. +Therefore, Yesod defines a Domain Specific Language (DSL) for specifying +routes, and provides Template Haskell functions to convert this DSL to Haskell +code. This chapter will explain the syntax of the routing declarations, give +you a glimpse of what code is generated for you, and explain the interaction +between routing and handler functions.

+
+

Route Syntax

+

Instead of trying to shoe-horn route declarations into an existing syntax, +Yesod’s approach is to use a simplified syntax designed just for routes. This +has the advantage of making the code not only easy to write, but simple enough +for someone with no Yesod experience to read and understand the sitemap of your +application.

+

A basic example of this syntax is:

+
/             HomeR     GET
+/blog         BlogR     GET POST
+/blog/#BlogId BlogPostR GET POST
+
+/static       StaticR   Static getStatic
+

The next few sections will explain the full details of what goes on in the +route declaration.

+
+

Pieces

+

One of the first thing Yesod does when it gets a request is split up the +requested path into pieces. The pieces are tokenized at all forward slashes. +For example:

+
toPieces "/" = []
+toPieces "/foo/bar/baz/" = ["foo", "bar", "baz", ""]
+

You may notice that there are some funny things going on with trailing slashes, +or double slashes ("/foo//bar//"), or a few other things. Yesod believes in +having canonical URLs; if users request a URL with a trailing slash, or with a +double slash, they are automatically redirected to the canonical version. This +ensures you have one URL for one resource, and can help with your search +rankings.

+

What this means for you is that you needn’t concern yourself with the exact +structure of your URLs: you can safely think about pieces of a path, and Yesod +automatically handles intercalating the slashes and escaping problematic +characters.

+

If, by the way, you want more fine-tuned control of how paths are split into +pieces and joined together again, you’ll want to look at the cleanPath and +joinPath methods in the Yesod typeclass chapter.

+
+

Types of Pieces

+

When you are declaring your routes, you have three types of pieces at your +disposal:

+
+
+Static +
+

+This is a plain string that must be matched against precisely in the URL. +

+
+
+Dynamic single +
+

+This is a single piece (ie, between two forward slashes), but +represents a user-submitted value. This is the primary method of receiving +extra user input on a page request. These pieces begin with a hash (#) and are +followed by a data type. The datatype must be an instance of PathPiece. +

+
+
+Dynamic multi +
+

+The same as before, but can receive multiple pieces of the URL. +This must always be the last piece in a resource pattern. It is specified by an +asterisk (*) followed by a datatype, which must be an instance of +PathMultiPiece. Multi pieces are not as common as the other two, though they +are very important for implementing features like static trees representing +file structure or wikis with arbitrary hierarchies. +

+
+
+

Let us take a look at some standard kinds of resource patterns you may want to +write. Starting simply, the root of an application will just be /. Similarly, +you may want to place your FAQ at /page/faq.

+

Now let’s say you are going to write a Fibonacci website. You may construct +your URLs like /fib/#Int. But there’s a slight problem with this: we do not +want to allow negative numbers or zero to be passed into our application. +Fortunately, the type system can protect us:

+
newtype Natural = Natural Int
+instance PathPiece Natural where
+    toPathPiece (Natural i) = T.pack $ show i
+    fromPathPiece s =
+        case reads $ T.unpack s of
+            (i, ""):_
+                | i < 1 -> Nothing
+                | otherwise -> Just $ Natural i
+            [] -> Nothing
+

On line 1 we define a simple newtype wrapper around Int to protect ourselves +from invalid input. We can see that PathPiece is a typeclass with two +methods. toPathPiece does nothing more than convert to a Text. +fromPathPiece attempts to convert a Text to our datatype, returning +Nothing when this conversion is impossible. By using this datatype, we can +ensure that our handler function is only ever given natural numbers, allowing +us to once again use the type system to battle the boundary issue.

+ +

Defining a PathMultiPiece is just as simple. Let’s say we want to have a Wiki +with at least two levels of hierarchy; we might define a datatype such as:

+
data Page = Page Text Text [Text] -- 2 or more
+instance PathMultiPiece Page where
+    toPathMultiPiece (Page x y z) = x : y : z
+    fromPathMultiPiece (x:y:z) = Just $ Page x y z
+    fromPathMultiPiece _ = Nothing
+
+
+
+

Resource name

+

Each resource pattern also has a name associated with it. That name will become +the constructor for the type safe URL datatype associated with your +application. Therefore, it has to start with a capital letter. By convention, +these resource names all end with a capital R. There is nothing forcing you to +do this, it is just common practice.

+

The exact definition of our constructor depends upon the resource pattern it is +attached to. Whatever datatypes are included in single and multi pieces of the +pattern become arguments to the datatype. This gives us a 1-to-1 correspondence +between our type-safe URL values and valid URLs in our application.

+ +

Let’s get some real examples going here. If you had the resource patterns +/person/#Text named PersonR, /year/#Int named YearR and /page/faq +named FaqR, you would end up with a route data type roughly looking like:

+
data MyRoute = PersonR Text
+             | YearR Int
+             | FaqR
+

If a user requests /year/2009, Yesod will convert it into the value YearR +2009. /person/Michael becomes PersonR "Michael" and /page/faq becomes +FaqR. On the other hand, /year/two-thousand-nine, /person/michael/snoyman +and /page/FAQ would all result in 404 errors without ever seeing your code.

+
+
+

Handler specification

+

The last piece of the puzzle when declaring your resources is how they will be +handled. There are three options in Yesod:

+
    +
  • +

    +A single handler function for all request methods on a given route. +

    +
  • +
  • +

    +A separate handler function for each request method on a given route. Any + other request method will generate a 405 Method Not Allowed response. +

    +
  • +
  • +

    +You want to pass off to a subsite. +

    +
  • +
+

The first two can be easily specified. A single handler function will be a line +with just a resource pattern and the resource name, such as /page/faq FaqR. +In this case, the handler function must be named handleFaqR.

+

A separate handler for each request method will be the same, plus a list of +request methods. The request methods must be all capital letters. For example, +/person/#String PersonR GET POST DELETE. In this case, you would need to +define three handler functions: getPersonR, postPersonR and +deletePersonR.

+

Subsites are a very useful— but more complicated— topic in Yesod. We will cover +writing subsites later, but using them is not too difficult. The most commonly +used subsite is the static subsite, which serves static files for your +application. In order to serve static files from /static, you would need a +resource line like:

+
/static StaticR Static getStatic
+

In this line, /static just says where in your URL structure to serve the +static files from. There is nothing magical about the word static, you could +easily replace it with /my/non-dynamic/files.

+

The next word, StaticR, gives the resource name. The next two words +specify that we are using a subsite. Static is the name of the subsite +foundation datatype, and getStatic is a function that gets a Static value +from a value of your master foundation datatype.

+

Let’s not get too caught up in the details of subsites now. We will look more +closely at the static subsite in the scaffolded site chapter.

+
+
+
+

Dispatch

+

Once you have specified your routes, Yesod will take care of all the pesky +details of URL dispatch for you. You just need to make sure to provide the +appropriate handler functions. For subsite routes, you do not need to write any +handler functions, but you do for the other two. We mentioned the naming rules +above (MyHandlerR GET becomes getMyHandlerR, MyOtherHandlerR becomes +handleMyOtherHandlerR).

+

Now that we know which functions we need to write, let’s figure out what their +type signatures should be.

+
+

Return Type

+

Let’s look at a simple handler function:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

There are two components to this returns type: Handler and Html. Let’s +analyze each in more depth.

+
+

Handler monad

+

Like the Widget type, the Handler data type is not defined anywhere in the +Yesod libraries. Instead, the libraries provide the data type:

+
data HandlerT site m a
+

And like WidgetT, this has three arguments: a base monad m, a monadic value +a, and the foundation data type site. Each application defines a Handler +synonym which constrains site to that application’s foundation data type, and +sets m to IO. If your foundation is MyApp, in other words, you’d have the +synonym:

+
type Handler = HandlerT MyApp IO
+

We need to be able to modify the underlying monad when writing subsites, but +otherwise we’ll use IO.

+

The HandlerT monad provides access to information about the user request +(e.g. query-string parameters), allows modifying the response (e.g., response +headers), and more. This is the monad that most of your Yesod code will live +in.

+

In addition, there’s a type class called MonadHandler. Both HandlerT and +WidgetT are instances of this type class, allowing many common functions to +be used in both monads. If you see MonadHandler in any API documentation, you +should remember that the function can be used in your Handler functions.

+
+
+

Html

+

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 respond with +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: +the content type (e.g., text/html, image/png) and how to serialize it to a +stream of bytes. This is represented by the TypedContent data type:

+
data TypedContent = TypedContent !ContentType !Content
+

We also have a type class for all data types which can be converted to a +TypedContent:

+
class ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

Many common data types are instances of this type class, including Html, +Value (from the aeson package, representing JSON), Text, and even () (for +representing an empty response).

+
+
+
+

Arguments

+

Let’s return to our simple example from above:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

Not every route is as simple as this HomeR. Take for instance our PersonR +route from earlier. The name of the person needs to be passed to the handler +function. This translation is very straight-forward, and hopefully intuitive. +For example:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Yesod
+
+data App = App
+instance Yesod App
+
+mkYesod "App" [parseRoutes|
+/person/#Text PersonR GET
+/year/#Integer/month/#Text/day/#Int DateR
+/wiki/*Texts WikiR GET
+|]
+
+getPersonR :: Text -> Handler Html
+getPersonR name = defaultLayout [whamlet|<h1>Hello #{name}!|]
+
+handleDateR :: Integer -> Text -> Int -> Handler Text -- text/plain
+handleDateR year month day =
+    return $
+        T.concat [month, " ", T.pack $ show day, ", ", T.pack $ show year]
+
+getWikiR :: [Text] -> Handler Text
+getWikiR = return . T.unwords
+
+main :: IO ()
+main = warp 3000 App
+

The arguments have the types of the dynamic pieces for each route, in the order +specified. Also, notice how we are able to use both Html and Text return +values.

+
+
+
+

The Handler functions

+

Since the majority of your code will live in the Handler monad, it’s +important to invest some time in understanding it better. The remainder of this +chapter will give a brief introduction to some of the most common functions +living in the Handler monad. I am specifically not covering any of the +session functions; that will be addressed in the sessions chapter.

+
+

Application Information

+

There are a number of functions that return information about your application +as a whole, and give no information about individual requests. Some of these +are:

+
+
+getYesod +
+

+Returns your applicaton foundation value. If you store configuration +values in your foundation, you will probably end up using this function a lot. +

+
+
+getUrlRender +
+

+Returns the URL rendering function, which converts a type-safe +URL into a Text. Most of the time- like with Hamlet- Yesod calls this +function for you, but you may occasionally need to call it directly. +

+
+
+getUrlRenderParams +
+

+A variant of getUrlRender that converts both a type-safe +URL and a list of query-string parameters. This function handles all +percent-encoding necessary. +

+
+
+
+
+

Request Information

+

The most common information you will want to get about the current request is +the requested path, the query string parameters and POSTed form data. The +first of those is dealt with in the routing, as described above. The other two +are best dealt with using the forms module.

+

That said, you will sometimes need to get the data in a more raw format. For +this purpose, Yesod exposes the YesodRequest datatype along with the +getRequest function to retrieve it. This gives you access to the full list of +GET parameters, cookies, and preferred languages. There are some convenient +functions to make these lookups easier, such as lookupGetParam, +lookupCookie and languages. For raw access to the POST parameters, you +should use runRequestBody.

+

If you need even more raw data, like request headers, you can use waiRequest +to access the Web Application Interface (WAI) request value. See the WAI +appendix for more details.

+
+
+

Short Circuiting

+

The following functions immediately end execution of a handler function and +return a result to the user.

+
+
+redirect +
+

+Sends a redirect response to the user (a 303 response). If you want to use a different response code (e.g., a permanent 301 redirect), you can use redirectWith. +

+
+
+ +
+
+notFound +
+

+Return a 404 response. This can be useful if a user requests a +database value that doesn’t exist. +

+
+
+permissionDenied +
+

+Return a 403 response with a specific error message. +

+
+
+invalidArgs +
+

+A 400 response with a list of invalid arguments. +

+
+
+sendFile +
+

+Sends a file from the filesystem with a specified content type. This +is the preferred way to send static files, since the underlying WAI handler may +be able to optimize this to a sendfile system call. Using readFile for +sending static files should not be necessary. +

+
+
+sendResponse +
+

+Send a normal response with a 200 status code. This is really +just a convenience for when you need to break out of some deeply nested code +with an immediate response. Any instance of ToTypedContent may be used. +

+
+
+sendWaiResponse +
+

+When you need to get low-level and send out a raw WAI +response. This can be especially useful for creating streaming responses or a +technique like server-sent events. +

+
+
+
+
+

Response Headers

+
+
+setCookie +
+

+Set a cookie on the client. Instead of taking an expiration date, +this function takes a cookie duration in minutes. Remember, you won’t see this +cookie using lookupCookie until the following request. +

+
+
+deleteCookie +
+

+Tells the client to remove a cookie. Once again, lookupCookie +will not reflect this change until the next request. +

+
+
+setHeader +
+

+Set an arbitrary response header. +

+
+
+setLanguage +
+

+Set the preferred user language, which will show up in the result +of the languages function. +

+
+
+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 +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. +

+
+
+alreadyExpired +
+

+Sets the Expires header to the past. +

+
+
+expiresAt +
+

+Sets the Expires header to the specified date/time. +

+
+
+
+
+
+

I/O and debugging

+

The HandlerT and WidgetT monad transformers are both instances of a number +of typeclasses. For this section, the important typeclasses are MonadIO and +MonadLogger. The former allows you to perform arbitrary IO actions inside +your handler, such as reading from a file. In order to achieve this, you just +need to prepend liftIO to the call.

+

MonadLogger provides a built-in logging system. There are many ways you can +customize this system, including what messages get logged and where logs are +sent. By default, logs are sent to standard output, in development all messages +are logged, and in production, warnings and errors are logged.

+

Often times when logging, we want to know where in the source code the logging +occured. For this, MonadLogger provides a number of convenience Template +Haskell functions which will automatically insert source code location into the +log messages. These functions are $logDebug, $logInfo, $logWarn, and +$logError. Let’s look at a short example of some of these functions.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Exception (IOException, try)
+import           Control.Monad     (when)
+import           Yesod
+
+data App = App
+instance Yesod App where
+    -- This function controls which messages are logged
+    shouldLog App src level =
+        True -- good for development
+        -- level == LevelWarn || level == LevelError -- good for production
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = do
+    $logDebug "Trying to read data file"
+    edata <- liftIO $ try $ readFile "datafile.txt"
+    case edata :: Either IOException String of
+        Left e -> do
+            $logError $ "Could not read datafile.txt"
+            defaultLayout [whamlet|An error occurred|]
+        Right str -> do
+            $logInfo "Reading of data file succeeded"
+            let ls = lines str
+            when (length ls < 5) $ $logWarn "Less than 5 lines of data"
+            defaultLayout
+                [whamlet|
+                    <ol>
+                        $forall l <- ls
+                            <li>#{l}
+                |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Query string and hash fragments

+

We’ve looked at a number of functions which work on URL-like things, such as redirect. These functions all work with type-safe URLs, but what else do they work with? There’s a typeclass called RedirectUrl which contains the logical for converting some type into a textual URL. This includes type-safe URLs, textual URLs, and two special instances:

+
    +
  1. +

    +A tuple of a URL and a list of key/value pairs of query string parameters. +

    +
  2. +
  3. +

    +The Fragment datatype, used for adding a hash fragment to the end of a URL. +

    +
  4. +
+

Both of these instances allow you to "add on" extra information to a type-safe +URL. Let’s see some examples of how these can be used:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+/link1 Link1R GET
+/link2 Link2R GET
+/link3 Link3R GET
+/link4 Link4R GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Redirects"
+    [whamlet|
+        <p>
+            <a href=@{Link1R}>Click to start the redirect loop!
+    |]
+
+getLink1R, getLink2R, getLink3R :: Handler ()
+getLink1R = redirect Link2R -- /link1
+getLink2R = redirect (Link3R, [("foo", "bar")]) -- /link3?foo=bar
+getLink3R = redirect $ Link4R :#: ("baz" :: Text) -- /link4#baz
+
+getLink4R :: Handler Html
+getLink4R = defaultLayout
+    [whamlet|
+        <p>You made it!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Of course, inside a Hamlet template this is usually not necessary, as you can simply include the hash after the URL directly, e.g.:

+
<a href=@{Link1R}#somehash>Link to hash
+
+
+

Summary

+

Routing and dispatch is arguably the core of Yesod: it is from here that our +type-safe URLs are defined, and the majority of our code is written within the +Handler monad. This chapter covered some of the most important and central +concepts of Yesod, so it is important that you properly digest it.

+

This chapter also hinted at a number of more complex Yesod topics that we will +be covering later. But you should be able to write some very sophisticated web +applications with just the knowledge you have learned up until here.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/scaffolding-and-the-site-template.html b/public/book-1.2/scaffolding-and-the-site-template.html new file mode 100644 index 00000000..130e251f --- /dev/null +++ b/public/book-1.2/scaffolding-and-the-site-template.html @@ -0,0 +1,503 @@ + Scaffolding and the Site Template :: Yesod Web Framework Book- Version 1.2 +
+

Scaffolding and the Site Template

+ + +

So you’re tired of running small examples, and ready to write a real site? Then +you’re at the right chapter. Even with the entire Yesod library at your +fingertips, there are still a lot of steps you need to go through to get a +production-quality site setup:

+
    +
  • +

    +Config file parsing +

    +
  • +
  • +

    +Signal handling (*nix) +

    +
  • +
  • +

    +More efficient static file serving +

    +
  • +
  • +

    +A good file layout +

    +
  • +
+

The scaffolded site is a combination of many Yesoders' best practices combined +together into a ready-to-use skeleton for your sites. It is highly recommended +for all sites. This chapter will explain the overall structure of the +scaffolding, how to use it, and some of its less-than-obvious features.

+

For the most part, this chapter will not contain code samples. It is +recommended that you follow along with an actual scaffolded site.

+ +
+

How to Scaffold

+

The yesod-bin package installs an executable (conveniently named yesod as +well). This executable provides a few commands (run yesod by itself to get a +list). In order to generate a scaffolding, the command is yesod init. This +will start a question-and-answer process where you get to provide basic +details. After answering the questions, you will have a site template in a +subfolder with the name of your project.

+

The most important of these questions is the database backend. You get a few +choices here, including SQL and MongoDB backends, as well as "simple" to +include no database support. This last option also turns off a few extra +dependencies, giving you a leaner overall site. The remainder of this chapter +will focus on the scaffoldings for one of the database backends. There will be +minor differences for the simple backend.

+

After creating your files, the scaffolder will print a message about getting +started. The suggested command runs cabal sandbox init. This will ensure +that your packages are installed only to your project directory, +avoiding conflicts with other projects. +Note that you really do need to use the cabal install --only-dependencies +command. Most likely, you do not yet +have all the dependencies in place needed by your site. For example, none of +the database backends, nor the Javascript minifier (hjsmin) are installed when +installing the yesod package.

+

Finally, to launch your development site, you would use yesod devel. +This site will automatically rebuild and reload whenever +you change your code.

+
+
+

File Structure

+

The scaffolded site is built as a fully cabalized Haskell package. In addition +to source files, config files, templates, and static files are produced as +well.

+
+

Cabal file

+

Whether directly using cabal, or indirectly using yesod devel, building +your code will always go through the cabal file. If you open the file, you’ll +see that there are both library and executable blocks. If the library-only +flag is turned on, then the executable block is not built. This is how yesod +devel calls your app. Otherwise, the executable is built.

+

The library-only flag should only be used by yesod devel; you should never +be explicitly passing it into cabal. There is an additional flag, dev, that +allows cabal to build an executable, but turns on some of the same features as +the library-only flag, i.e., no optimizations and reload versions of the +Shakespearean template functions.

+

In general, you will build as follows:

+
    +
  • +

    +When developing, use yesod devel exclusively. +

    +
  • +
  • +

    +When building a production build, perform cabal clean && cabal + configure && cabal build. This will produce an optimized executable + in your dist folder. +

    +
  • +
+

You might be surprised to see the NoImplicitPrelude extension. We turn this +on since the site includes its own module, Import, with a few changes to the +Prelude that make working with Yesod a little more convenient.

+

The last thing to note is the exported-modules list. If you add any modules to +your application, you must update this list to get yesod devel to work +correctly. Unfortunately, neither Cabal nor GHC will give you a warning if you +forgot to make this update, and instead you’ll get a very scary-looking error +message from yesod devel.

+
+
+

Routes and entities

+

Multiple times in this book, you’ve seen a comment like "We’re declaring our +routes/entities with quasiquotes for convenience. In a production site, you +should use an external file." The scaffolding uses such an external file.

+

Routes are defined in config/routes, and entities in config/models. They +have the exact same syntax as the quasiquoting you’ve seen throughout the book, +and yesod devel knows to automatically recompile the appropriate modules when +these files change.

+

The models files is referenced by Model.hs. You are free to declare +whatever you like in this file, but here are some guidelines:

+
    +
  • +

    +Any data types used in entities must be imported/declared in Model.hs, + above the persistFile call. +

    +
  • +
  • +

    +Helper utilities should either be declared in Import.hs or, if very + model-centric, in a file within the Model folder and imported into + Import.hs. +

    +
  • +
+
+
+

Foundation and Application modules

+

The mkYesod function which we have used throughout the book declares a few +things:

+
    +
  • +

    +Route type +

    +
  • +
  • +

    +Route render function +

    +
  • +
  • +

    +Dispatch function +

    +
  • +
+

The dispatch function refers to all of the handler functions, and therefore all +of those must either be defined in the same file as the dispatch function, or +be imported into the module containing the dispatch function.

+

Meanwhile, the handler functions will almost certainly refer to the route type. +Therefore, they must be either in the same file where the route type is +defined, or must import that file. If you follow the logic here, your entire +application must essentially live in a single file!

+

Clearly this isn’t what we want. So instead of using mkYesod, the scaffolding +site uses a decomposed version of the function. Foundation calls +mkYesodData, which declares the route type and render function. Since it does +not declare the dispatch function, the handler functions need not be in scope. +Import.hs imports Foundation.hs, and all the handler modules import +Import.hs.

+

In Application.hs, we call mkYesodDispatch, which creates our dispatch +function. For this to work, all handler functions must be in scope, so be sure +to add an import statement for any new handler modules you create.

+

Other than that, Application.hs is pretty simple. It provides two primary +functions: getApplicationDev is used by yesod devel to launch your app, and +makeApplication is used by the executable to launch.

+

Foundation.hs is much more exciting. It:

+
    +
  • +

    +Declares your foundation datatype +

    +
  • +
  • +

    +Declares a number of instances, such as Yesod, YesodAuth, and + YesodPersist +

    +
  • +
  • +

    +Imports the messages files. If you look for the line starting with + mkMessage, you will see that it specifies the folder containing the + messages (messages) and the default language (en, for English). +

    +
  • +
+

This is the right file for adding extra instances for your foundation, such as +YesodAuthEmail or YesodBreadcrumbs.

+

We’ll be referring back to this file later, as we discussed some of the special +implementations of Yesod typeclass methods.

+
+
+

Import

+

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’m always adding the same five import statements (Data.Text, + Control.Applicative, etc) to every handler module. +

    +
  • +
  • +

    +I want to make sure I never use some evil function (head, readFile, …) from Prelude. +

    +
  • +
+ +

The solution is to turn on the NoImplicitPrelude language extension, +re-export the parts of Prelude we want, add in all the other stuff we want, +define our own functions as well, and then import this file in all handlers.

+
+
+

Handler modules

+

Handler modules should go inside the Handler folder. The site template +includes one module: Handler/Home.hs. How you split up your handler functions +into individual modules is your decision, but a good rule of thumb is:

+
    +
  • +

    +Different methods for the same route should go in the same file, e.g. + getBlogR and postBlogR. +

    +
  • +
  • +

    +Related routes can also usually go in the same file, e.g., getPeopleR and + getPersonR. +

    +
  • +
+

Of course, it’s entirely up to you. When you add a new handler file, make sure +you do the following:

+
    +
  • +

    +Add it to version control (you are using version control, right?). +

    +
  • +
  • +

    +Add it to the cabal file. +

    +
  • +
  • +

    +Add it to the Application.hs file. +

    +
  • +
  • +

    +Put a module statement at the top, and an import Import line below it. +

    +
  • +
+

You can use the yesod add-handler command to automate the last three steps.

+
+
+
+

widgetFile

+

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.

+

If you have a handler function:

+
getHomeR = defaultLayout $(widgetFile "homepage")
+

, Yesod will look for the following files:

+
    +
  • +

    +templates/homepage.hamlet +

    +
  • +
  • +

    +templates/homepage.lucius +

    +
  • +
  • +

    +templates/homepage.cassius +

    +
  • +
  • +

    +templates/homepage.julius +

    +
  • +
+

If any of those files are present, they will be automatically included in the +output.

+ +
+
+

defaultLayout

+

One of the first things you’re going to want to customize is the look of your +site. The layout is actually broken up into two files:

+
    +
  • +

    +templates/default-layout-wrapper.hamlet contains just the basic shell of a + page. This file is interpreted as plain Hamlet, not as a Widget, and + therefore cannot refer to other widgets, embed i18n strings, or add extra + CSS/JS. +

    +
  • +
  • +

    +templates/default-layout.hamlet is where you would put the bulk of your + page. You must remember to include the widget value in the page, as that + contains the per-page contents. This file is interpreted as a Widget. +

    +
  • +
+

Also, since default-layout is included via the widgetFile function, any +Lucius, Cassius, or Julius files named default-layout.* will automatically be +included as well.

+
+
+

Static files

+

The scaffolded site automatically includes the static file subsite, optimized +for serving files that will not change over the lifetime of the current build. +What this means is that:

+
    +
  • +

    +When your static file identifiers are generated (e.g., static/mylogo.png + becomes mylogo_png), a query-string parameter is added to it with a hash of + the contents of the file. All of this happens at compile time. +

    +
  • +
  • +

    +When yesod-static serves your static files, it sets expiration headers far + in the future, and incldues an etag based on a hash of your content. +

    +
  • +
  • +

    +Whenever you embed a link to mylogo_png, the rendering includes the + query-string parameter. If you change the logo, recompile, and launch your + new app, the query string will have changed, causing users to ignore the + cached copy and download a new version. +

    +
  • +
+

Additionally, you can set a specific static root in your Settings.hs file to +serve from a different domain name. This has the advantage of not requiring +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 +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:

+
    +
  1. +

    +Caching works properly. +

    +
  2. +
  3. +

    +Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with the same hash already exists. +

    +
  4. +
+

Finally, all of your Javascript is automatically minified via hjsmin.

+
+
+

Conclusion

+

The purpose of this chapter was not to explain every line that exists in the +scaffolded site, but instead to give a general overview to how it works. The +best way to become more familiar with it is to jump right in and start writing +a Yesod site with it.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/sessions.html b/public/book-1.2/sessions.html new file mode 100644 index 00000000..44a9cdb0 --- /dev/null +++ b/public/book-1.2/sessions.html @@ -0,0 +1,459 @@ + Sessions :: Yesod Web Framework Book- Version 1.2 +
+

Sessions

+ + +

HTTP is a stateless protocol. While some view this as a disadvantage, advocates +of RESTful web development laud this as a plus. When state is removed from the +picture, we get some automatic benefits, such as easier scalability and +caching. You can draw many parallels with the non-mutable nature of Haskell in +general.

+

As much as possible, RESTful applications should avoid storing state about an +interaction with a client. However, it is sometimes unavoidable. Features like +shopping carts are the classic example, but other more mundane interactions +like proper login handling can be greatly enhanced by proper usage of sessions.

+

This chapter will describe how Yesod stores session data, how you can access +this data, and some special functions to help you make the most of sessions.

+
+

Clientsession

+

One of the earliest packages spun off from Yesod was clientsession. This +package uses encryption and signatures to store data in a client-side cookie. +The encryption prevents the user from inspecting the data, and the signature +ensures that the session can be neither hijacked nor tampered with.

+

It might sound like a bad idea from an efficiency standpoint to store data in a +cookie. After all, this means that the data must be sent on every request. +However, in practice, clientsession can be a great boon for performance.

+
    +
  • +

    +No server side database lookup is required to service a request. +

    +
  • +
  • +

    +We can easily scale horizontally: each request contains all the information + we need to send a response. +

    +
  • +
  • +

    +To avoid undue bandwidth overhead, production sites can serve their static + content from a separate domain name, thereby skipping transmission of the + session cookie for each request. +

    +
  • +
+

Storing megabytes of information in the session will be a bad idea. But for +that matter, most session implementations recommend against such practices. If +you really need massive storage for a user, it is best to store a lookup key in +the session, and put the actual data in a database.

+

All of the interaction with clientsession is handled by Yesod internally, but +there are a few spots where you can tweak the behavior just a bit.

+
+
+

Controlling sessions

+

By default, your Yesod application will use clientsession for its session +storage, getting the encryption key from the client client-session-key.aes +and giving a session a two hour timeout. (Note: timeout is measured from the +last time the client sent a request to the site, not from when then session +was first created.) However, all of those points can be modified by overriding +the makeSessionBackend method in the Yesod typeclass.

+

One simple way to override this method is to simply turn off session handling; +to do so, return Nothing. If your app has absolutely no session needs, +disabling them can give a bit of a performance increase. But be careful about +disabling sessions: this will also disable such features as Cross-Site Request +Forgery protection.

+
instance Yesod App where
+    makeSessionBackend _ = return Nothing
+

Another common approach is to modify the filepath or timeout value, but +continue using client-session. To do so, use the defaultClientSessionBackend +helper function:

+
instance Yesod App where
+    makeSessionBackend _ = do
+        let minutes = 24 * 60 -- 1 day
+            filepath = "mykey.aes"
+        backend <- defaultClientSessionBackend minutes filepath
+

There are a few other functions to grant you more fine-grained control of +client-session, but they will rarely be necessary. Please see Yesod.Core's +documentation if you are interested. It’s also possible to implement some other +form of session, such as a server side session. To my knowledge, at the time of +writing, no other such implementations exist.

+ +
+
+

Session Operations

+

Like most frameworks, a session in Yesod is a key-value store. The base session +API boils down to four functions: lookupSession gets a value for a key (if +available), getSession returns all of the key/value pairs, setSession sets +a value for a key, and deleteSession clears a value for a key.

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+import           Control.Applicative ((<$>), (<*>))
+import qualified Web.ClientSession   as CS
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+getHomeR :: Handler Html
+getHomeR = do
+    sess <- getSession
+    defaultLayout
+        [whamlet|
+            <form method=post>
+                <input type=text name=key>
+                <input type=text name=val>
+                <input type=submit>
+            <h1>#{show sess}
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    (key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
+    case mval of
+        Nothing -> deleteSession key
+        Just val -> setSession key val
+    liftIO $ print (key, mval)
+    redirect HomeR
+
+instance Yesod App where
+    -- Make the session timeout 1 minute so that it's easier to play with
+    makeSessionBackend _ = do
+        backend <- defaultClientSessionBackend 1 "keyfile.aes"
+        return $ Just backend
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Messages

+

One usage of sessions previously alluded to is messages. They come to solve a +common problem in web development: the user performs a POST request, the web +app makes a change, and then the web app wants to simultaneously redirect the +user to a new page and send the user a success message. (This is known as +Post/Redirect/Get.)

+

Yesod provides a pair of functions to enable this workflow: setMessage stores +a value in the session, and getMessage both reads the value most recently put +into the session, and clears the old value so it is not displayed twice.

+

It is recommended to have a call to getMessage in defaultLayout so that any +available message is shown to a user immediately, without having to add +getMessage calls to every handler.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/            HomeR       GET
+/set-message SetMessageR POST
+|]
+
+instance Yesod App where
+    defaultLayout widget = do
+        pc <- widgetToPageContent widget
+        mmsg <- getMessage
+        giveUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <p>Your message was: #{msg}
+                        ^{pageBody pc}
+            |]
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{SetMessageR}>
+            My message is: #
+            <input type=text name=message>
+            <button>Go
+    |]
+
+postSetMessageR :: Handler ()
+postSetMessageR = do
+    msg <- runInputPost $ ireq textField "message"
+    setMessage $ toHtml msg
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+

Initial page load, no message

+ + + + + + +
+

New message entered in text box

+ + + + + + +
+

After form submit, message appears at top of page

+ + + + + + +
+

After refresh, the message is cleared

+ + + + + + +
+
+
+

Ultimate Destination

+

Not to be confused with a horror film, ultimate destination is a technique +originally developed for Yesod’s authentication framework, but which has more +general usefulness. Suppose a user requests a page that requires +authentication. If the user is not yet logged in, you need to send him/her to +the login page. A well-designed web app will then send them back to the first +page they requested. That’s what we call the ultimate destination.

+

redirectUltDest sends the user to the ultimate destination set in his/her +session, clearing that value from the session. It takes a default destination +as well, in case there is no destination set. For setting the session, there +are three options:

+
    +
  • +

    +setUltDest sets the destination to the given URL, which can be given + either as a textual URL or a type-safe URL. +

    +
  • +
  • +

    +setUltDestCurrent sets the destination to the currently requested URL. +

    +
  • +
  • +

    +setUltDestReferer sets the destination based on the Referer header (the + page that led the user to the current page). +

    +
  • +
+

Additionally, there is the clearUltDest function, to drop the ultimate +destination value from the session if present.

+

Let’s look at a small sample app. It will allow the user to set his/her name in +the session, and then tell the user his/her name from another route. If the +name hasn’t been set yet, the user will be redirected to the set name page, +with an ultimate destination set to come back to the current page.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/         HomeR     GET
+/setname  SetNameR  GET POST
+/sayhello SayHelloR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{SetNameR}>Set your name
+        <p>
+            <a href=@{SayHelloR}>Say hello
+    |]
+
+-- Display the set name form
+getSetNameR :: Handler Html
+getSetNameR = defaultLayout
+    [whamlet|
+        <form method=post>
+            My name is #
+            <input type=text name=name>
+            . #
+            <input type=submit value="Set name">
+    |]
+
+-- Retreive the submitted name from the user
+postSetNameR :: Handler ()
+postSetNameR = do
+    -- Get the submitted name and set it in the session
+    name <- runInputPost $ ireq textField "name"
+    setSession "name" name
+
+    -- After we get a name, redirect to the ultimate destination.
+    -- If no destination is set, default to the homepage
+    redirectUltDest HomeR
+
+getSayHelloR :: Handler Html
+getSayHelloR = do
+    -- Lookup the name value set in the session
+    mname <- lookupSession "name"
+    case mname of
+        Nothing -> do
+            -- No name in the session, set the current page as
+            -- the ultimate destination and redirect to the
+            -- SetName page
+            setUltDestCurrent
+            setMessage "Please tell me your name"
+            redirect SetNameR
+        Just name -> defaultLayout [whamlet|<p>Welcome #{name}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Summary

+

Sessions are the primary means by which we bypass the statelessness imposed by +HTTP. We shouldn’t consider this an escape hatch to perform whatever actions we +want: statelessness in web applications is a virtue, and we should respect it +whenever possible. However, there are specific cases where it is vital to +retain some state.

+

The session API in Yesod is very simple. It provides a key-value store, and a +few convenience functions built on top for common use cases. If used properly, +with small payloads, sessions should be an unobtrusive part of your web +development.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/settings-types.html b/public/book-1.2/settings-types.html new file mode 100644 index 00000000..987acf6e --- /dev/null +++ b/public/book-1.2/settings-types.html @@ -0,0 +1,196 @@ + Settings Types :: Yesod Web Framework Book- Version 1.2 +
+

Settings Types

+ + +

Let’s say you’re writing a webserver. You want the server to take a port to +listen on, and an application to run. So you create the following function:

+
run :: Int -> Application -> IO ()
+

But suddenly you realize that some people will want to customize their timeout +durations. So you modify your API:

+
run :: Int -> Int -> Application -> IO ()
+

So, which Int is the timeout, and which is the port? Well, you could create +some type aliases, or comment your code. But there’s another problem creeping +into our code: this run function is getting unmanageable. Soon we’ll need to +take an extra parameter to indicate how exceptions should be handled, and then +another one to control which host to bind to, and so on.

+

A more extensible solution is to introduce a settings datatype:

+
data Settings = Settings
+    { settingsPort :: Int
+    , settingsHost :: String
+    , settingsTimeout :: Int
+    }
+

And this makes the calling code almost self-documenting:

+
run Settings
+    { settingsPort = 8080
+    , settingsHost = "127.0.0.1"
+    , settingsTimeout = 30
+    } myApp
+

Great, couldn’t be clearer, right? True, but what happens when you have 50 +settings to your webserver. Do you really want to have to specify all of those +each time? Of course not. So instead, the webserver should provide a set of +defaults:

+
defaultSettings = Settings 3000 "127.0.0.1" 30
+

And now, instead of needing to write that long bit of code above, we can get +away with:

+
run defaultSettings { settingsPort = 8080 } myApp -- (1)
+

This is great, except for one minor hitch. Let’s say we now decide to add an +extra record to Settings. Any code out in the wild looking like this:

+
run (Settings 8080 "127.0.0.1" 30) myApp -- (2)
+

will be broken, since the Settings constructor now takes 4 arguments. The +proper thing to do would be to bump the major version number so that dependent +packages don’t get broken. But having to change major versions for every minor +setting you add is a nuisance. The solution? Don’t export the Settings +constructor:

+
module MyServer
+    ( Settings
+    , settingsPort
+    , settingsHost
+    , settingsTimeout
+    , run
+    , defaultSettings
+    ) where
+

With this approach, no one can write code like (2), so you can freely add new +records without any fear of code breaking.

+

The one downside of this approach is that it’s not immediately obvious from the +Haddocks that you can actually change the settings via record syntax. That’s +the point of this chapter: to clarify what’s going on in the libraries that use +this technique.

+

I personally use this technique in a few places, feel free to have a look at +the Haddocks to see what I mean.

+
    +
  • +

    +Warp: Settings +

    +
  • +
  • +

    +http-conduit: Request and ManagerSettings +

    +
  • +
  • +

    +xml-conduit +

    +
  • +
  • +

    +Parsing: ParseSettings +

    +
  • +
  • +

    +Rendering: RenderSettings +

    +
  • +
+

As a tangential issue, http-conduit and xml-conduit actually create +instances of the Default typeclass instead of declaring a brand new +identifier. This means you can just type def instead of +defaultParserSettings.

+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/shakespearean-templates.html b/public/book-1.2/shakespearean-templates.html new file mode 100644 index 00000000..2967ea50 --- /dev/null +++ b/public/book-1.2/shakespearean-templates.html @@ -0,0 +1,1031 @@ + Shakespearean Templates :: Yesod Web Framework Book- Version 1.2 +
+

Shakespearean Templates

+ + +

Yesod uses the Shakespearean family of template languages as its standard +approach to HTML, CSS and Javascript creation. This language family shares some +common syntax, as well as overarching principles:

+
    +
  • +

    +As little interference to the underlying language as possible, while +providing conveniences where unobtrusive. +

    +
  • +
  • +

    +Compile-time guarantees on well-formed content. +

    +
  • +
  • +

    +Static type safety, greatly helping the prevention of + XSS (cross-site + scripting) attacks. +

    +
  • +
  • +

    +Automated checking of valid URLs, whenever possible, through type-safe + URLs. +

    +
  • +
+

There is nothing inherently tying Yesod to these languages, or the other way +around: each can be used independently of the other. This chapter will address +these template languages on their own, while the remainder of the book will use +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 +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 +Javascript; the only added feature is variable interpolation.

+ +
+

Hamlet (HTML)

+
$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle} - My Site
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1 .page-title>#{pageTitle}
+        <p>Here is a list of your friends:
+        $if null friends
+            <p>Sorry, I lied, you don't have any friends.
+        $else
+            <ul>
+                $forall Friend name age <- friends
+                    <li>#{name} (#{age} years old)
+        <footer>^{copyright}
+
+
+

Cassius (CSS)

+
#myid
+    color: #{red}
+    font-size: #{bodyFontSize}
+foo bar baz
+    background-image: url(@{MyBackgroundR})
+
+
+

Lucius (CSS)

+
section.blog {
+    padding: 1em;
+    border: 1px solid #000;
+    h1 {
+        color: #{headingColor};
+    }
+}
+
+
+

Julius (Javascript)

+
$(function(){
+    $("section.#{sectionClass}").hide();
+    $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
+    ^{addBling}
+});
+
+
+
+

Types

+

Before we jump into syntax, let’s take a look at the various types involved. We +mentioned in the introduction that types help protect us from XSS attacks. For +example, let’s say that we have an HTML template that should display someone’s +name. It might look like this:

+
<p>Hello, my name is #{name}
+ +

What should happen to name, and what should its datatype be? A naive approach +would be to use a Text value, and insert it verbatim. But that would give us +quite a problem when name is equal to something like:

+
<script src='http://nefarious.com/evil.js'></script>
+

What we want is to be able to entity-encode the name, so that < becomes &lt;.

+

An equally naive approach is to simply entity-encode every piece of text that +gets embedded. What happens when you have some preexisting HTML generated from +another process? For example, on the Yesod website, all Haskell code snippets +are run through a colorizing function that wraps up words in appropriate span +tags. If we entity escaped everything, code snippets would be completely +unreadable!

+

Instead, we have an Html datatype. In order to generate an Html value, we +have two options for APIs: the ToMarkup typeclass provides a way to convert +String and Text values into Html, via its toHtml function, +automatically escaping entities along the way. This would be the approach we’d +want for the name above. For the code snippet example, we would use the +preEscapedToMarkup function.

+

When you use variable interpolation in Hamlet (the HTML Shakespeare language), +it automatically applies a toHtml call to the value inside. So if you +interpolate a String, it will be entity-escaped. But if you provide an Html +value, it will appear unmodified. In the code snippet example, we might +interpolate with something like #{preEscapedToMarkup myHaskellHtml}.

+ +

Similarly, we have Css/ToCss, as well as Javascript/ToJavascript. These +provide some compile-time sanity checks that we haven’t accidentally stuck some +HTML in our CSS.

+ +
+

Type-safe URLs

+

Possibly the most unique feature in Yesod is type-safe URLs, and the ability to +use them conveniently is provided directly by Shakespeare. Usage is nearly +identical to variable interpolation; we just use the at-sign (@) instead of the +hash (#). We’ll cover the syntax later; first, let’s clarify the intuition.

+

Suppose we have an application with two routes: +http://example.com/profile/home is the homepage, and +http://example.com/display/time displays the current time. And let’s say we +want to link from the homepage to the time. I can think of three different ways +of constructing the URL:

+
    +
  1. +

    +As a relative link: ../display/time +

    +
  2. +
  3. +

    +As an absolute link, without a domain: /display/time +

    +
  4. +
  5. +

    +As an absolute link, with a domain: http://example.com/display/time +

    +
  6. +
+

There are problems with each approach: the first will break if either URL +changes. Also, it’s not suitable for all use cases; RSS and Atom feeds, for +instance, require absolute URLs. The second is more resilient to change than +the first, but still won’t be acceptable for RSS and Atom. And while the third +works fine for all use cases, you’ll need to update every single URL in your +application whenever your domain name changes. You think that doesn’t happen +often? Just wait till you move from your development to staging and finally +production server.

+

But more importantly, there is one huge problem with all approaches: if you +change your routes at all, the compiler won’t warn you about the broken links. +Not to mention that typos can wreak havoc as well.

+

The goal of type-safe URLs is to let the compiler check things for us as much +as possible. In order to facilitate this, our first step must be to move away +from plain old text, which the compiler doesn’t understand, to some well +defined datatypes. For our simple application, let’s model our routes with a +sum type:

+
data MyRoute = Home | Time
+

Instead of placing a link like /display/time in our template, we can use the +Time constructor. But at the end of the day, HTML is made up of text, not +data types, so we need some way to convert these values to text. We call this a +URL rendering function, and a simple one is:

+
renderMyRoute :: MyRoute -> Text
+renderMyRoute Home = "http://example.com/profile/home"
+renderMyRoute Time = "http://example.com/display/time"
+ +

OK, we have our render function, and we have type-safe URLs embedded in the +templates. How does this fit together exactly? Instead of generating an Html +(or Css or Javascript) value directly, Shakespearean templates actually +produce a function, which takes this render function and produces HTML. To see +this better, let’s have a quick (fake) peek at how Hamlet would work under the +surface. Supposing we had a template:

+
<a href=@{Time}>The time
+

this would translate roughly into the Haskell code:

+
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
+
+
+
+

Syntax

+

All Shakespearean languages share the same interpolation syntax, and are able +to utilize type-safe URLs. They differ in the syntax specific for their target +language (HTML, CSS, or Javascript).

+
+

Hamlet Syntax

+

Hamlet is the most sophisticated of the languages. Not only does it provide +syntax for generating HTML, it also allows for basic control structures: +conditionals, looping, and maybes.

+
+

Tags

+

Obviously tags will play an important part of any HTML template language. In +Hamlet, we try to stick very close to existing HTML syntax to make the language +more comfortable. However, instead of using closing tags to denote nesting, we +use indentation. So something like this in HTML:

+
<body>
+<p>Some paragraph.</p>
+<ul>
+<li>Item 1</li>
+<li>Item 2</li>
+</ul>
+</body>
+

would be

+
<body>
+    <p>Some paragraph.
+    <ul>
+        <li>Item 1
+        <li>Item 2
+

In general, we find this to be easier to follow than HTML once you get +accustomed to it. The only tricky part comes with dealing with whitespace +before and after tags. For example, let’s say you want to create the HTML

+
<p>Paragraph <i>italic</i> end.</p>
+

We want to make sure that there is a whitespace preserved after the word +"Paragraph" and before the word "end". To do so, we use two simple escape +characters:

+
<p>
+    Paragraph #
+    <i>italic
+    \ end.
+

The whitespace escape rules are actually quite simple:

+
    +
  1. +

    +If the first non-space character in a line is a backslash, the backslash is ignored. (Note: this will also cause any tag on this line to be treated as plain text.) +

    +
  2. +
  3. +

    +If the last character in a line is a hash, it is ignored. +

    +
  4. +
+

One other thing. Hamlet does not escape entities within its content. This is +done on purpose to allow existing HTML to be more easily copied in. So the +example above could also be written as:

+
<p>Paragraph <i>italic</i> end.
+

Notice that the first tag will be automatically closed by Hamlet, while the +inner "i" tag will not. You are free to use whichever approach you want, there +is no penalty for either choice. Be aware, however, that the only time you +use closing tags in Hamlet is for such inline tags; normal tags are not closed.

+
+
+

Interpolation

+

What we have so far is a nice, simplified HTML, but it doesn’t let us interact +with our Haskell code at all. How do we pass in variables? Simple: with +interpolation:

+
<head>
+    <title>#{title}
+

The hash followed by a pair of braces denotes variable interpolation. In the +case above, the title variable from the scope in which the template was +called will be used. Let me state that again: Hamlet automatically has access +to the variables in scope when it’s called. There is no need to specifically +pass variables in.

+

You can apply functions within an interpolation. You can use string and numeric +literals in an interpolation. You can use qualified modules. Both parentheses +and the dollar sign can be used to group statements together. And at the end, +the toHtml function is applied to the result, meaning any instance of +ToHtml can be interpolated. Take, for instance, the following code.

+
-- Just ignore the quasiquote stuff for now, and that shamlet thing.
+-- It will be explained later.
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (shamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Char (toLower)
+import Data.List (sort)
+
+data Person = Person
+    { name :: String
+    , age  :: Int
+    }
+
+main :: IO ()
+main = putStrLn $ renderHtml [shamlet|
+<p>Hello, my name is #{name person} and I am #{show $ age person}.
+<p>
+    Let's do some funny stuff with my name: #
+    <b>#{sort $ map toLower (name person)}
+<p>Oh, and in 5 years I'll be #{show ((+) 5 (age person))} years old.
+|]
+  where
+    person = Person "Michael" 26
+

What about our much-touted type-safe URLs? They are almost identical to +variable interpolation in every way, except they start with an at-sign (@) +instead. In addition, there is embedding via a caret (^) which allows you to +embed another template of the same type. The next code sample demonstrates both +of these.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text)
+
+data MyRoute = Home
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+
+footer :: HtmlUrl MyRoute
+footer = [hamlet|
+<footer>
+    Return to #
+    <a href=@{Home}>Homepage
+    .
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ [hamlet|
+<body>
+    <p>This is my page.
+    ^{footer}
+|] render
+

Additionally, there is a variant of URL interpolation which allows you to embed +query string parameters. This can be useful, for example, for creating +paginated responses. Instead of using @{…}, you add a question mark +(@?{…}) to indicate the presence of a query string. The value you provide +must be a two-tuple with the first value being a type-safe URL and the second +being a list of query string parameter pairs. See the next code snippet for an +example.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text, append, pack)
+import Control.Arrow (second)
+import Network.HTTP.Types (renderQueryText)
+import Data.Text.Encoding (decodeUtf8)
+import Blaze.ByteString.Builder (toByteString)
+
+data MyRoute = SomePage
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render SomePage params = "/home" `append`
+    decodeUtf8 (toByteString $ renderQueryText True (map (second Just) params))
+
+main :: IO ()
+main = do
+    let currPage = 2 :: Int
+    putStrLn $ renderHtml $ [hamlet|
+<p>
+    You are currently on page #{currPage}.
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage - 1)])}>Previous
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage + 1)])}>Next
+|] render
+

This generates the expected HTML:

+
<p>You are currently on page 2.
+<a href="/home?page=1">Previous</a>
+<a href="/home?page=3">Next</a>
+</p>
+
+
+

Attributes

+

In that last example, we put an href attribute on the "a" tag. Let’s elaborate on the syntax:

+
    +
  • +

    +You can have interpolations within the attribute value. +

    +
  • +
  • +

    +The equals sign and value for an attribute are optional, just like in HTML. + So <input type=checkbox checked> is perfectly valid. +

    +
  • +
  • +

    +There are two convenience attributes: for id, you can use the hash, and for + classes, the period. In other words, <p #paragraphid .class1 .class2>. +

    +
  • +
  • +

    +While quotes around the attribute value are optional, they are required if + you want to embed spaces. +

    +
  • +
  • +

    +You can add an attribute optionally by using colons. To make a checkbox only + checked if the variable isChecked is True, you would write + <input type=checkbox :isChecked:checked>. To have a paragraph be optionally red, + you could use <p :isRed:style="color:red">. +

    +
  • +
+
+
+

Conditionals

+

Eventually, you’ll want to put in some logic in your page. The goal of Hamlet +is to make the logic as minimalistic as possible, pushing the heavy lifting +into Haskell. As such, our logical statements are very basic… so basic, that +it’s if, elseif, and else.

+
$if isAdmin
+    <p>Welcome to the admin section.
+$elseif isLoggedIn
+    <p>You are not the administrator.
+$else
+    <p>I don't know who you are. Please log in so I can decide if you get access.
+

All the same rules of normal interpolation apply to the content of the conditionals.

+
+
+

Maybe

+

Similarly, we have a special construct for dealing with Maybe values. This +could technically be dealt with using if, isJust and fromJust, but this +is more convenient and avoids partial functions.

+
$maybe name <- maybeName
+    <p>Your name is #{name}
+$nothing
+    <p>I don't know your name.
+

In addition to simple identifiers, you can use a few other, more complicated +values on the left hand side, such as constructors and tuples.

+
$maybe Person firstName lastName <- maybePerson
+    <p>Your name is #{firstName} #{lastName}
+

The right-hand-side follows the same rules as interpolations, allow variables, +function application, and so on.

+
+
+

Forall

+

And what about looping over lists? We have you covered there too:

+
$if null people
+    <p>No people.
+$else
+    <ul>
+        $forall person <- people
+            <li>#{person}
+
+
+

Case

+

Pattern matching is one of the great strengths of Haskell. Sum types let you +cleanly model many real-world types, and case statements let you safely +match, letting the compiler warn you if you missed a case. Hamlet gives you the +same power.

+
$case foo
+    $of Left bar
+        <p>It was left: #{bar}
+    $of Right baz
+        <p>It was right: #{baz}
+
+
+

With

+

Rounding out our statements, we have with. It’s basically just a convenience +for declaring a synonym for a long expression.

+
$with foo <- some very (long ugly) expression that $ should only $ happen once
+    <p>But I'm going to use #{foo} multiple times. #{foo}
+
+
+

Doctype

+

Last bit of syntactic sugar: the doctype statement. We have support for a +number of different versions of a doctype, though we recommend $doctype 5 +for modern web applications, which generates <!DOCTYPE html>.

+
$doctype 5
+<html>
+    <head>
+        <title>Hamlet is Awesome
+    <body>
+        <p>All done.
+ +
+
+
+

Lucius Syntax

+

Lucius is one of two CSS templating languages in the Shakespeare family. It is +intended to be a superset of CSS, leveraging the existing syntax while adding +in a few more features.

+
    +
  • +

    +Like Hamlet, we allow both variable and URL interpolation. +

    +
  • +
  • +

    +CSS blocks are allowed to nest. +

    +
  • +
  • +

    +You can declare variables in your templates. +

    +
  • +
  • +

    +A set of CSS properties can be created as a mixin, and reused in multiple + declarations. +

    +
  • +
+

Starting with the second point: let’s say you want to have some special styling +for some tags within your article. In plain ol' CSS, you’d have to write:

+
article code { background-color: grey; }
+article p { text-indent: 2em; }
+article a { text-decoration: none; }
+

In this case, there aren’t that many clauses, but having to type out article +each time is still a bit of a nuisance. Imagine if you had a dozen or so of +these. Not the worst thing in the world, but a bit of an annoyance. Lucius +helps you out here:

+
article {
+    code { background-color: grey; }
+    p { text-indent: 2em; }
+    a { text-decoration: none; }
+    > h1 { color: green; }
+}
+

Having Lucius variables allows you to avoid repeating yourself. A simple +example would be to define a commonly used color:

+
@textcolor: #ccc; /* just because we hate our users */
+body { color: #{textcolor} }
+a:link, a:visited { color: #{textcolor} }
+

Mixins are a relatively new addition to Lucius. The idea is to declare a mixin +providing a collection of properties, and then embed that mixin in a template +using caret interpolation (^). The following example demonstrates how we +could use a mixin to deal with vendor prefixes.

+
{-# LANGUAGE QuasiQuotes #-}
+import Text.Lucius
+import qualified Data.Text.Lazy.IO as TLIO
+
+-- Dummy render function.
+render = undefined
+
+-- Our mixin, which provides a number of vendor prefixes for transitions.
+transition val =
+    [luciusMixin|
+        -webkit-transition: #{val};
+        -moz-transition: #{val};
+        -ms-transition: #{val};
+        -o-transition: #{val};
+        transition: #{val};
+    |]
+
+-- Our actual Lucius template, which uses the mixin.
+myCSS =
+    [lucius|
+        .some-class {
+            ^{transition "all 4s ease"}
+        }
+    |]
+
+main = TLIO.putStrLn $ renderCss $ myCSS render
+
+
+

Cassius Syntax

+

Cassius is a whitespace-sensitive alternative to Lucius. As mentioned in the +synopsis, it uses the same processing engine as Lucius, but preprocesses all +input to insert braces to enclose subblocks and semicolons to terminate lines. +This means you can leverage all features of Lucius when writing Cassius. As a +simple example:

+
#banner
+    border: 1px solid #{bannerColor}
+    background-image: url(@{BannerImageR})
+
+
+

Julius Syntax

+

Julius is the simplest of the languages discussed here. In fact, some might +even say it’s really just Javascript. Julius allows the three forms of +interpolation we’ve mentioned so far, and otherwise applies no transformations +to your content.

+ +
+
+
+

Calling Shakespeare

+

The question of course arises at some point: how do I actually use this stuff? +There are three different ways to call out to Shakespeare from your Haskell +code:

+
+
+Quasiquotes +
+

+Quasiquotes allow you to embed arbitrary content within your Haskell, and for it to be converted into Haskell code at compile time. +

+
+
+External file +
+

+In this case, the template code is in a separate file which is referenced via Template Haskell. +

+
+
+Reload mode +
+

+Both of the above modes require a full recompile to see any changes. In reload mode, your template is kept in a separate file and referenced via Template Haskell. But at runtime, the external file is reparsed from scratch each time. +

+
+
+ +

One of the first two approaches should be used in production. They both embed +the entirety of the template in the final executable, simplifying deployment +and increasing performance. The advantage of the quasiquoter is the simplicity: +everything stays in a single file. For short templates, this can be a very good +fit. However, in general, the external file approach is recommended because:

+
    +
  • +

    +It follows nicely in the tradition of separate logic from presentation. +

    +
  • +
  • +

    +You can easily switch between external file and debug mode with some simple + CPP macros, meaning you can keep rapid development and still achieve high + performance in production. +

    +
  • +
+

Since these are special QuasiQuoters and Template Haskell functions, you need +to be sure to enable the appropriate language extensions and use correct +syntax. You can see a simple example of each in the following code snippets.

+

Quasiquoter

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Data.Text (Text)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: Text -> HtmlUrl MyRoute
+template title = [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{title}
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1>#{title}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ template "My Title" render
+

+

External file

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-} -- to control production versus debug
+import Text.Lucius (CssUrl, luciusFile, luciusFileDebug, renderCss)
+import Data.Text (Text)
+import qualified Data.Text.Lazy.IO as TLIO
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: CssUrl MyRoute
+#if PRODUCTION
+template = $(luciusFile "template.lucius")
+#else
+template = $(luciusFileDebug "template.lucius")
+#endif
+
+main :: IO ()
+main = TLIO.putStrLn $ renderCss $ template render
+

+
-- @template.lucius
+foo { bar: baz }
+

The naming scheme for the functions is very consistent.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageQuasiquoterExternal fileReload

Hamlet

hamlet

hamletFile

N/A

Cassius

cassius

cassiusFile

cassiusFileReload

Lucius

lucius

luciusFile

luciusFileReload

Julius

julius

juliusFile

juliusFileReload

+
+

Alternate Hamlet Types

+

So far, we’ve seen how to generate an HtmlUrl value from Hamlet, which is a +piece of HTML with embedded type-safe URLs. There are currently three other +values we can generate using Hamlet: plain HTML, HTML with URLs and +internationalized messages, and widgets. That last one will be covered in the +widgets chapter.

+

To generate plain HTML without any embedded URLs, we use "simplified Hamlet". +There are a few changes:

+
    +
  • +

    +We use a different set of functions, prefixed with an "s". So the quasiquoter + is shamlet and the external file function is shamletFile. How we + pronounce those is still up for debate. +

    +
  • +
  • +

    +No URL interpolation is allowed. Doing so will result in a compile-time + error. +

    +
  • +
  • +

    +Embedding (the caret-interpolator) no longer allows arbitrary HtmlUrl + values. The rule is that the embedded value must have the same type as the + template itself, so in this case it must be Html. That means that for + shamlet, embedding can be completely replaced with normal variable + interpolation (with a hash). +

    +
  • +
+

Dealing with internationalization (i18n) in Hamlet is a bit complicated. Hamlet +supports i18n via a message datatype, very similar in concept and +implementation to a type-safe URL. As a motivating example, let’s say we want +to have an application that tells you hello and how many apples you have eaten. +We could represent those messages with a datatype.

+
data Msg = Hello | Apples Int
+

Next, we would want to be able to convert that into something human-readable, +so we define some render functions:

+
renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+

Now we want to interpolate those Msg values directly in the template. For that, we use underscore interpolation.

+
$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+

This kind of a template now needs some way to turn those values into HTML. So +just like type-safe URLs, we pass in a render function. To represent this, we +define a new type synonym:

+
type Render url = url -> [(Text, Text)] -> Text
+type Translate msg = msg -> Html
+type HtmlUrlI18n msg url = Translate msg -> Render url -> Html
+

At this point, you can pass renderEnglish, renderSpanish, or +renderKlingon to this template, and it will generate nicely translated output +(depending, of course, on the quality of your translators). The complete +program is:

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Data.Text (Text)
+import qualified Data.Text as T
+import Text.Hamlet (HtmlUrlI18n, ihamlet)
+import Text.Blaze.Html (toHtml)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+renderUrl :: MyRoute -> [(Text, Text)] -> Text
+renderUrl Home _ = "/home"
+renderUrl Time _ = "/time"
+renderUrl Stylesheet _ = "/style.css"
+
+data Msg = Hello | Apples Int
+
+renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+
+template :: Int -> HtmlUrlI18n Msg MyRoute
+template count = [ihamlet|
+$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml
+     $ (template 5) (toHtml . renderEnglish) renderUrl
+
+
+
+

Other Shakespeare

+

In addition to HTML, CSS and Javascript helpers, there is also some more +general-purpose Shakespeare available. shakespeare-text provides a simple way +to create interpolated strings, much like people are accustomed to in scripting +languages like Ruby and Python. This package’s utility is definitely not +limited to Yesod.

+
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
+import Text.Shakespeare.Text
+import qualified Data.Text.Lazy.IO as TLIO
+import Data.Text (Text)
+import Control.Monad (forM_)
+
+data Item = Item
+    { itemName :: Text
+    , itemQty :: Int
+    }
+
+items :: [Item]
+items =
+    [ Item "apples" 5
+    , Item "bananas" 10
+    ]
+
+main :: IO ()
+main = forM_ items $ \item -> TLIO.putStrLn
+    [lt|You have #{show $ itemQty item} #{itemName item}.|]
+

Some quick points about this simple example:

+
    +
  • +

    +Notice that we have three different textual datatypes involved (String, + strict Text and lazy Text). They all play together well. +

    +
  • +
  • +

    +We use a quasiquoter named lt, which generates lazy text. There is also + st. +

    +
  • +
  • +

    +Also, there are longer names for these quasiquoters (ltext and stext). +

    +
  • +
+
+
+

General Recommendations

+

Here are some general hints from the Yesod community on how to get the most out +of Shakespeare.

+
    +
  • +

    +For actual sites, use external files. For libraries, it’s OK to use + quasiquoters, assuming they aren’t too long. +

    +
  • +
  • +

    +Patrick Brisbin has put together a + Vim code + highlighter that can help out immensely. +

    +
  • +
  • +

    +You should almost always start Hamlet tags on their own line instead of + embedding start/end tags after an existing tag. The only exception to this is + the occasional <i> or <b> tag inside a large block of text. +

    +
  • +
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/single-process-pubsub.html b/public/book-1.2/single-process-pubsub.html new file mode 100644 index 00000000..e15645cf --- /dev/null +++ b/public/book-1.2/single-process-pubsub.html @@ -0,0 +1,326 @@ + Single process pub-sub :: Yesod Web Framework Book- Version 1.2 +
+

Single process pub-sub

+ + +

The previous example was admittedly quite simple. Let’s build on that +foundation (pun intended) to do something a bit more interesting. Suppose we +have a workflow on our site like the following:

+
    +
  1. +

    +Enter some information on page X, and submit. +

    +
  2. +
  3. +

    +Submission starts a background job, and the user is redirected to a page to view status of that job. +

    +
  4. +
  5. +

    +That second page will subscribe to updates from the background job and display them to the user. +

    +
  6. +
+

The core principle here is the ability to let one thread publish updates, and +have another thread subscribe to receive those updates. This is known generally +as pub/sub, and fortunately is very easy to achieve in Haskell via STM.

+

Like the previous chapter, let me start off with the caveat: this technique +only works properly if you have a single web application process. If you have +two different servers and a load balancer, you’d either need sticky sessions or +some other solution to make sure that the requests from a single user are going +to the same machine. In those situations, you may want to consider using an +external pubsub solution, such as Redis.

+

With that caveat out of the way, let’s get started.

+
+

Foundation datatype

+

We’ll need two different mutable references in our foundation. The first will +keep track of the next "job id" we’ll hand out. Each of these background jobs +we’ll be represented by a unique identifier, which will be used in our URLs. +The second piece of data will be a map from the job ID to the broadcast channel +used for publishing updates. In code:

+
data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+

Notice that our TChan contains Maybe Text values. The reason for the +Maybe wrapper is so that we can indicate that the channel is complete, by +providing a Nothing value.

+
+
+

Allocating a job

+

In order to allocate a job, we need to:

+
    +
  1. +

    +Get a job ID. +

    +
  2. +
  3. +

    +Create a new broadcast channel. +

    +
  4. +
  5. +

    +Add the channel to the channel map. +

    +
  6. +
+

Due to the beauty of STM, this is pretty easy.

+
(jobId, chan) <- liftIO $ atomically $ do
+    jobId <- readTVar nextJob
+    writeTVar nextJob $! jobId + 1
+    chan <- newBroadcastTChan
+    m <- readTVar jobs
+    writeTVar jobs $ IntMap.insert jobId chan m
+    return (jobId, chan)
+
+
+

Fork our background job

+

There are many different ways we could go about this, and they depend entirely +on what the background job is going to be. Here’s a minimal example of a +background job that prints out a few messages, with a 1 second delay between +each message. Note how after our final message, we broadcast a Nothing value +and remove our channel from the map of channels.

+
liftIO $ forkIO $ do
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something\n"
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something else\n"
+    threadDelay 1000000
+    atomically $ do
+        writeTChan chan $ Just "All done\n"
+        writeTChan chan Nothing
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.delete jobId m
+
+
+

View progress

+

For this demonstration, I’ve elected for a very simple progress viewing: a +plain text page with stream response. There are a few other possibilities here: +an HTML page that auto-refreshes every X seconds or using eventsource or +websockets. I encourage you to give those a shot also, but here’s the simplest +implementation I can think of:

+
getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+

We start off by looking up the channel in the map. If we can’t find it, it +means the job either never existed, or has already been completed. In either +event, we return a 404. (Another possible enhancement would be to store some +information on all previously completed jobs and let the user know if they’re +done.)

+

Assuming the channel exists, we use respondSource to start a streaming +response. We then repeatedly call readTChan until we get a Nothing value, +at which point we exit (via return ()). Notice that on each iteration, we +call both sendChunkText and sendFlush. Without that second call, the user +won’t receive any updates until the output buffer completely fills up, which is +not what we want for a real-time update system.

+
+
+

Complete application

+

For completeness, here’s the full source code for this application:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Concurrent     (forkIO, threadDelay)
+import           Control.Concurrent.STM
+import           Data.IntMap            (IntMap)
+import qualified Data.IntMap            as IntMap
+import           Data.Text              (Text)
+import           Yesod
+
+data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+/view-progress/#Int ViewProgressR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "PubSub example"
+    [whamlet|
+        <form method=post>
+            <button>Start new background job
+    |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    App {..} <- getYesod
+    (jobId, chan) <- liftIO $ atomically $ do
+        jobId <- readTVar nextJob
+        writeTVar nextJob $! jobId + 1
+        chan <- newBroadcastTChan
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.insert jobId chan m
+        return (jobId, chan)
+    liftIO $ forkIO $ do
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something\n"
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something else\n"
+        threadDelay 1000000
+        atomically $ do
+            writeTChan chan $ Just "All done\n"
+            writeTChan chan Nothing
+            m <- readTVar jobs
+            writeTVar jobs $ IntMap.delete jobId m
+    redirect $ ViewProgressR jobId
+
+getViewProgressR :: Int -> Handler TypedContent
+getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+
+main :: IO ()
+main = do
+    jobs <- newTVarIO IntMap.empty
+    nextJob <- newTVarIO 1
+    warp 3000 App {..}
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/understanding-request.html b/public/book-1.2/understanding-request.html new file mode 100644 index 00000000..9f41aa9a --- /dev/null +++ b/public/book-1.2/understanding-request.html @@ -0,0 +1,660 @@ + Understanding a Request :: Yesod Web Framework Book- Version 1.2 +
+

Understanding a Request

+ + +

You can often times get away with using Yesod for quite a while without needing +to understand its internal workings. However, such an understanding is often +times advantageous. This chapter will walk you through the request handling +process for a fairly typical Yesod application. Note that a fair amount of this +discussion involves code changes in Yesod 1.2. Most of the concepts are the +same in previous versions, though the data types involved were a bit messier.

+

Yesod’s usage of Template Haskell to bypass boilerplate code can make it +a bit difficult to understand this process sometimes. If beyond the information +in this chapter you wish to further analyze things, it can be useful to view +GHC’s generated code using -ddump-splices.

+ +
+

Handlers

+

When trying to understand Yesod request handling, we need to look at two +components: how a request is dispatched to the appropriate handler code, and +how handler functions are processed. We’ll start off with the latter, and +then circle back to understanding the dispatch process itself.

+
+

Layers

+

Yesod builds itself on top of WAI, which provides a protocol for web servers +(or, more generally, handlers) and applications to communicate with each +other. This is expressed through two datatypes: Request and Response. Then, an +Application is defined as type Application = Request -> ResourceT IO +Response. A WAI handler will take an application and run it.

+

Request and Response are both very low-level, trying to represent the HTTP +protocol without too much embellishment. This keeps WAI as a generic tool, but +also leaves out a lot of the information we need in order to implement a web +framework. For example, WAI will provide us with the raw data for all request +headers. But Yesod needs to parse that to get cookie information, and then +parse the cookies in order to extract session information.

+

To deal with this dichotomy, Yesod introduces two new data types: +YesodRequest and YesodResponse. YesodRequest contains a WAI Request, and +also adds in such request information as cookies and session variables, and on +the response side can either be a standard WAI Response, or be a higher-level +representation of such a response including such things as updated session +information and extra response headers. To parallel WAI’s Application, we +have type YesodApp = YesodRequest -> ResourceT IO YesodResponse.

+

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: Handler. When you write +handler functions, you need to have access to three different things:

+
    +
  • +

    +The YesodRequest value for the current request. +

    +
  • +
  • +

    +Some basic environment information, like how to log messages or handle error conditions. This is provided by the datatype RunHandlerEnv. +

    +
  • +
  • +

    +A mutable variable to keep track of updateable information, such as the headers to be returned and the user session state. This is called GHState. +

    +
  • +
+ +

So when you’re writing a handler function, you’re essentially just +writing in a Reader monad that has access to all of this information. The +runHandler function will turn a Handler into a YesodApp. yesodRunner takes this +a step further and converts all the way to a WAI Application.

+
+
+

Content

+

Our example above, and many others you’ve already seen, give a handler +with a type of Handler Html. We’ve just described what the Handler means, +but how does Yesod know how to deal with Html? The answer lies in the +ToTypedContent typeclass. The relevants bit of code are:

+
data Content = ContentBuilder !BBuilder.Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush BBuilder.Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+data TypedContent = TypedContent !ContentType !Content
+
+class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

The Content datatype represents the different ways you can provide a response +body. The first three mirror WAI’s representation directly. The fourth +(ContentDontEvaluate) is used to indicate to Yesod whether response bodies +should be fully evaluated before being returned to users. The advantage to +fully evaluating is that we can provide meaningful error messages if an +exception is thrown from pure code. The downside is possibly increased time and +memory usage.

+

In any event, Yesod knows how to turn a Content into a response body. The +ToContent typeclass provides a way to allow many different datatypes to be +converted into response bodies. Many commonly used types are already instances +of ToContent, including strict and lazy ByteString and Text, and of course +Html.

+

TypedContent adds an extra piece of information: the content type of the value. +As you might expect, there are ToTypedContent instances for a number of common +datatypes, including HTML, JSON, and plain text.

+
instance ToTypedContent J.Value where
+    toTypedContent v = TypedContent typeJson (toContent v)
+instance ToTypedContent Html where
+    toTypedContent h = TypedContent typeHtml (toContent h)
+instance ToTypedContent T.Text where
+    toTypedContent t = TypedContent typePlain (toContent t)
+

Putting this all together: a Handler is able to return any value which is an +instance of ToTypedContent, and Yesod will handle turning it into an +appropriate representation and setting the Content-Type response header.

+
+
+

Short-circuit responses

+

One other oddity is how short-circuiting works. For example, you can call +redirect in the middle of a handler function, and the rest of the function will +not be called. The mechanism we use is standard Haskell exceptions. Calling +redirect just throws an exception of type HandlerContents. The runHandler +function will catch any exceptions thrown and produce an appropriate response. +For HandlerContents, each constructor gives a clear action to perform, be it +redirecting or sending a file. For all other exception types, an error message +is displayed to the user.

+
+
+
+

Dispatch

+

Dispatch is the act of taking an incoming request and generating an appropriate +response. We have a few different constraints regarding how we want to handle +dispatch:

+
    +
  • +

    +Dispatch based on path segments (or pieces). +

    +
  • +
  • +

    +Optionally dispatch on request method. +

    +
  • +
  • +

    +Support subsites: packaged collections of functionality providing multiple routes under a specific URL prefix. +

    +
  • +
  • +

    +Support using WAI applications as subsites, while introducing as little + runtime overhead to the process as possible. In particular, we want to avoid + performing any unnecessary parsing to generate a YesodRequest if it + won’t be used. +

    +
  • +
+

The lowest common denominator for this would be to simply use a WAI +Application. However, this doesn’t provide quite enough information: we +need access to the foundation datatype, the logger, and for subsites how a +subsite route is converted to a parent site route. To address this, we have two +helper data types (YesodRunnerEnv and YesodSubRunnerEnv) providing this extra +information for normal sites and subsites.

+

With those types, dispatch now becomes a relatively simple matter: give me an +environment and a request, and I’ll give you a response. This is +represented by the typeclasses YesodDispatch and YesodSubDispatch:

+
class Yesod site => YesodDispatch site where
+    yesodDispatch :: YesodRunnerEnv site -> W.Application
+
+class YesodSubDispatch sub m where
+    yesodSubDispatch :: YesodSubRunnerEnv sub (HandlerSite m) m
+                     -> W.Application
+

We’ll see a bit later how YesodSubDispatch is used. Let’s first +understand how YesodDispatch comes into play,__

+
+

toWaiApp, toWaiAppPlain, and warp

+

Let’s assume for the moment that you have a datatype which is an instance +of YesodDispatch. You’ll want to now actually run this thing somehow. To +do this, we need to convert it into a WAI application and pass it to some kind +of a WAI handler/server. To start this journey, we use toWaiAppPlain. It +performs any appwide initialization necessary. At the time of writing, this +means allocating a logger and setting up the session backend, but more +functionality may be added in the future. Using this data, we can now create a +YesodRunnerEnv. And when that value is passed to yesodDispatch, we get a WAI +application.

+

We’re almost done. The final remaining modification is path segment +cleanup. The Yesod typeclass includes a member function named cleanPath which +can be used to create canonical URLs. For example, the default implementation +would remove double slashes and redirect a user from /foo//bar to /foo/bar. +toWaiAppPlain adds in some pre-processing to the normal WAI request by +analyzing the requested path and performing cleanup/redirect as necessary.

+

At this point, we have a fully functional WAI application. There are two other +helper functions included. toWaiApp wraps toWaiAppPlain and additionally +includes some commonly used WAI middlewares, including request logging and GZIP +compression. (Please see the Haddocks for an up-to-date list.) We finally have +the warp function which, as you might guess, runs your application with Warp.

+ +
+
+

Generated code

+

The last remaining black box is the Template Haskell generated code. This +generated code is responsible for handling some of the tedious, error-prone +pieces of your site. If you want to, you can write these all by hand instead. +We’ll demonstrate what that translation would look like, and in the +process elucidate how YesodDispatch and YesodSubDispatch work. Let’s +start with a fairly typical Yesod application.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+
+instance Yesod App
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req = return $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+

For completeness we’ve provided a full listing, but let’s focus on +just the Template Haskell portion:

+
mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+

While this generates a few pieces of code, we only need to replicate three +components to make our site work. Let’s start with the simplest: the +Handler type synonym:

+
type Handler = HandlerT App IO
+

Next is the type-safe URL and its rendering function. The rendering function is +allowed to generate both path segments and query string parameters. Standard +Yesod sites never generate query-string parameters, but it is technically +possible. And in the case of subsites, this often does happen. Notice how we +handle the qs parameter for the MySubsiteR case:

+
instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+

You can see that there’s a fairly simple mapping from the higher-level +route syntax and the RenderRoute instance. Each route becomes a constructor, +each URL parameter becomes an argument to its constructor, we embed a route for +the subsite, and use toPathPiece to render parameters to text.

+

The final component is the YesodDispatch instance. Let’s look at this in +a few pieces.

+
instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+

As described above, yesodDispatch is handed both an environment and a WAI +Request value. We can now perform dispatch based on the requested path, or in +WAI terms, the pathInfo. Referring back to our original high-level route +syntax, we can see that our first route is going to be the single piece +only-get, which we pattern match for.

+ +

Once that match has succeeded, we additionally pattern match on the request +method; if it’s GET, we use the handler function getOnlyGetR. +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 +to the handler function, and then convert the resulting YesodResponse into a +WAI Response.

+

Wonderful; one down, three to go. The next one is even easier.

+
            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+

Unlike OnlyGetR, AnyMethodR will work for any request method, so we don’t +need to perform any further pattern matching.

+
            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+

We add in one extra complication here: a dynamic parameter. While we used +toPathPiece to render to a textual value above, we now use fromPathPiece to +perform the parsing. Assuming the parse succeeds, we then follow a very similar +dispatch system as was used for OnlyGetR. The prime difference is that our +parameter needs to be passed to both the handler function and the route data +constructor.

+

Next we’ll look at the subsite, which is quite different.

+
            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+

Unlike the other pattern matches, here we just look to see if our pattern +prefix matches. Any route beginning with /my-subsite should be passed off to +the subsite for processing. This is where we finally get to use +yesodSubDispatch. This function closely mirrors yesodDispatch. We need to +construct a new environment to be passed to it. Let’s discuss the four +records:

+
    +
  • +

    +ysreGetSub demonstrates how to get the subsite foundation type from the + master site. We provide getMySubsite, which is the function we provided in + the high-level route syntax. +

    +
  • +
  • +

    +ysreParentRunner provides a means of running a handler function. It may seem + a bit boring to just provide yesodRunner, but by having a separate parameter + we allow the construction of deeply nested subsites, which will wrap and + unwrap many layers of interleaving subsites. (This is a more advanced concept + which we won’t be covering in this chapter.) +

    +
  • +
  • +

    +ysreToParentRoute will convert a route for the subsite into a route for the + parent site. This is the purpose of the MySubsiteR constructor. This allows + subsites to use functions such as getRouteToParent. +

    +
  • +
  • +

    +ysreParentEnv simply passes on the initial environment, which contains a + number of things the subsite may need (such as logger). +

    +
  • +
+

The other interesting thing is how we modify the pathInfo. This allows subsites +to continue dispatching from where the parent site left off. To demonstrate +how this works, see some screenshots of various requests in the following +figure.

+

Path info in subsite

+ + + + + + +
+

And finally, not all requests will be valid routes. For those cases, we just +want to respond with a 404 not found.

+
            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+
+

Complete code

+

Following is the full code for the non-Template Haskell approach.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+import           Yesod.Core.Types           (YesodSubRunnerEnv (..))
+
+data App = App
+
+instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+
+type Handler = HandlerT App IO
+
+instance Yesod App
+
+instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req = return $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+
+
+
+

Conclusion

+

Yesod abstracts away quite a bit of the plumbing from you as a developer. Most +of this is boilerplate code that you’ll be happy to ignore. But it can be +empowering to understand exactly what’s going on under the surface. At +this point, you should hopefully be able- with help from the Haddocks- to write +a site without any of the autogenerated Template Haskell code. Not that +I’d recommend it; I think using the generated code is easier and safer.

+

One particular advantage of understanding this material is seeing where Yesod +sits in the world of WAI. This makes it easier to see how Yesod will interact +with WAI middleware, or how to include code from other WAI framework in a Yesod +application (or vice-versa!).

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/visitor-counter.html b/public/book-1.2/visitor-counter.html new file mode 100644 index 00000000..3cfe01e9 --- /dev/null +++ b/public/book-1.2/visitor-counter.html @@ -0,0 +1,164 @@ + Visitor counter :: Yesod Web Framework Book- Version 1.2 +
+

Visitor counter

+ + +

Remember back in the good ol' days of the internet, where no website was +complete without a little "you are visitor number 32" thingy? Ahh, those were +the good times! Let’s recreate that wonderful experience in Yesod!

+

Now, if we wanted to do this properly, we’d store this information in some kind +of persistent storage layer, like a database, so that the information could be +shared across multiple horizontally-scaled web servers, and so that the +information would survive an app restart.

+

But our goal here isn’t to demonstrate good practice (after all, if it was +about good practice, I wouldn’t be demonstrating a visitor counter, right?). +Instead, this is meant to provide a simple example of sharing some state among +multiple handlers. A real-world use case would be caching information across +requests. Just remember that when you use the technique we’ll be showing, you +need to be careful about multiple app servers and app restarts.

+

The technique is simple: we create a new field in the foundation datatype for a +mutable reference to some data, and then access it in each handler. The +technique is so simple, it’s worth just diving into the code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import           Yesod
+
+data App = App
+    { visitors :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    visitorsRef <- fmap visitors getYesod
+    visitors <-
+        liftIO $ atomicModifyIORef visitorsRef $ \i ->
+        (i + 1, i + 1)
+    defaultLayout
+        [whamlet|
+            <p>Welcome, you are visitor number #{visitors}.
+        |]
+
+main :: IO ()
+main = do
+    visitorsRef <- newIORef 0
+    warp 3000 App
+        { visitors = visitorsRef
+        }
+

I used IORef here, since we didn’t need anything more than it provided, but +you’re free to use MVars or TVars as well. In fact, a good exercise for +the reader is to modify the above program to store the visitor count in a +TVar instead.

+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/web-application-interface.html b/public/book-1.2/web-application-interface.html new file mode 100644 index 00000000..8d22d4b7 --- /dev/null +++ b/public/book-1.2/web-application-interface.html @@ -0,0 +1,346 @@ + Web Application Interface :: Yesod Web Framework Book- Version 1.2 +
+

Web Application Interface

+ + + +

It is a problem almost every language used for web development has dealt with: +the low level interface between the web server and the application. The +earliest example of a solution is the venerable and battle-worn Common Gateway Interface (CGI), +providing a language-agnostic interface using only standard input, standard +output and environment variables.

+

Back when Perl was becoming the de facto web programming language, a major +shortcoming of CGI became apparent: the process needed to be started anew for +each request. When dealing with an interpretted language and application +requiring database connection, this overhead became unbearable. FastCGI (and +later SCGI) arose as a successor to CGI, but it seems that much of the +programming world went in a different direction.

+

Each language began creating its own standard for interfacing with servers. +mod_perl. mod_python. mod_php. mod_ruby. Within the same language, multiple +interfaces arose. In some cases, we even had interfaces on top of interfaces. +And all of this led to much duplicated effort: a Python application designed to +work with FastCGI wouldn’t work with mod_python; mod_python only exists for +certain webservers; and these programming language specific web server +extensions need to be written for each programming language.

+

Haskell has its own history. We originally had the cgi package, which provided +a monadic interface. The fastcgi package then provided the same interface. +Meanwhile, it seemed that the majority of Haskell web development focused on +the standalone server. The problem is that each server comes with its own +interface, meaning that you need to target a specific backend. This means that +it is impossible to share common features, like GZIP encoding, development +servers, and testing frameworks.

+

WAI attempts to solve this, by providing a generic and efficient interface +between web servers and applications. Any handler supporting the interface +can serve any WAI application, while any application using the interface can +run on any handler.

+

At the time of writing, there are various backends, including Warp, FastCGI, +and development server. There are even more esoteric backends like +wai-handler-webkit for creating desktop apps. wai-extra provides many common +middleware components like GZIP, JSON-P and virtual hosting. wai-test makes it +easy to write unit tests, and wai-handler-devel lets you develop your +applications without worrying about stopping to compile. Yesod targets WAI, as +well as other Haskell web frameworks such as Scotty and MFlow. It’s also used +by some applications that skip the framework entirely, including Hoogle.

+ +
+

The Interface

+

The interface itself is very straight-forward: an application takes a request +and returns a response. A response is an HTTP status, a list of headers and a +response body. A request contains various information: the requested path, +query string, request body, HTTP version, and so on.

+

In order to handle resource management in an exception-safe manner, we use +continuation passing style for returning the response, similar to how the +bracket function works. This makes our definition of an application look +like:

+
type Application =
+    Request ->
+    (Response -> IO ResponseReceived) ->
+    IO ResponseReceived
+

The first argument is a Request, which shouldn’t be too surprising. The +second argument is the continuation, or what we should do with a Response. +Generally speaking, this will just be sending it to the client. We use the +special ResponseReceived type to ensure that the application does in fact +call the continuation.

+

This may seem a little strange, but usage is pretty straight-forward, as we’ll +demonstrate below.

+
+

Response Body

+

Haskell has a datatype known as a lazy bytestring. By utilizing laziness, you +can create large values without exhausting memory. Using lazy I/O, you can do +such tricks as having a value which represents the entire contents of a file, +yet only occupies a small memory footprint. In theory, a lazy bytestring is the +only representation necessary for a response body.

+

In practice, while lazy byte strings are wonderful for generating "pure" +values, the lazy I/O necessary to read a file introduces some non-determinism +into our programs. When serving thousands of small files a second, the limiting +factor is not memory, but file handles. Using lazy I/O, file handles may not be +freed immediately, leading to resource exhaustion. To deal with this, WAI +provides its own streaming data interface.

+

The core of this streaming interface is the Builder. A Builder represents +an action to fill up a buffer with bytes of data. This is more efficient than +simply passing ByteStrings around, as it can avoid multiple copies of data. +In many cases, an application needs only to provide a single Builder value. +And for that simple case, we have the ResponseBuilder constructor.

+

However, there are times when an Application will need to interleave IO +actions with yielding of data to the client. For that case, we have +ResponseStream. With ResponseStream, you provide a function. This +function in turn takes two actions: a "yield more data" action, and a "flush +the buffer" action. This allows you to yield data, perform IO actions, and +flush, as many times as you need, and with any interleaving desired.

+

There is one further optimization: many operating systems provide a sendfile +system call, which sends a file directly to a socket, bypassing a lot of the +memory copying inherent in more general I/O system calls. For that case, we +have a ResponseFile.

+

Finally, there are some cases where we need to break out of the HTTP mode +entirely. Two examples are WebSockets, where we need to upgrade a half-duplex +HTTP connection to a full-duplex connection, and HTTPS proxying, which requires +our proxy server to establish a connection, and then become a dumb data +transport agent. For these cases, we have the ResponseRaw constructor. Note +that not all WAI handlers can in fact support ResponseRaw, though the most +commonly used handler, Warp, does provide this support.

+
+
+

Request Body

+

Like response bodies, we could theoretically use a lazy ByteString for request +bodies, but in practice we want to avoid lazy I/O. Instead, the request body is +represented with a IO ByteString action (ByteString here being a strict +ByteString). Note that this does not return the entire request body, but +rather just the next chunk of data. Once you’ve consumed the entire request +body, further calls to this action will return an empty ByteString.

+

Note that, unlike response bodies, we have no need for using Builders on +the request side, since our purpose is purely for reading.

+

The request body could in theory contain any type of data, but the most common +are URL encoded and multipart form data. The wai-extra package contains +built-in support for parsing these in a memory-efficient manner.

+
+
+
+

Hello World

+

To demonstrate the simplicity of WAI, let’s look at a hello world example. In +this example, we’re going to use the OverloadedStrings language extension to +avoid explicitly packing string values into bytestrings.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.HTTP.Types (status200)
+import Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $
+  responseLBS status200 [("Content-Type", "text/plain")] "Hello World"
+
+main = run 3000 application
+

Lines 2 through 4 perform our imports. Warp is provided by the warp package, +and is the premiere WAI backend. WAI is also built on top of the http-types +package, which provides a number of datatypes and convenience values, including +status200.

+

First we define our application. Since we don’t care about the specific request +parameters, we ignore the first argument to the function, which contains the +request value. The second argument is our "send a response" function, which we +immediately use. The response value we send is built from a lazy ByteString +(thus responseLBS), with status code 200 ("OK"), a text/plain content type, +and a body containing the words "Hello World". Pretty straight-forward.

+
+
+

Resource allocation

+

Let’s make this a little more interesting, and try to allocate a resource for +our response. We’ll create an MVar in our main function to track the number +of requests, and then hold that MVar while sending each response.

+
{-# LANGUAGE OverloadedStrings #-}
+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)
+
+application countRef _ respond = do
+    modifyMVar countRef $ \count -> do
+        let count' = count + 1
+            msg = fromByteString "You are visitor number: " <>
+                  fromShow count'
+        responseReceived <- respond $ responseBuilder
+            status200
+            [("Content-Type", "text/plain")]
+            msg
+        return (count', responseReceived)
+
+main = do
+    visitorCount <- newMVar 0
+    run 3000 $ application visitorCount
+

This is where WAI’s continuation interface shines. We’re able to use the +standard modifyMVar function to acquire the MVar lock and send our +response. Note how we thread the responseReceived value through, though we +never actually use the value for anything. It is merely witness to the fact +that we have, in fact, sent a response.

+

Notice also how we take advantage of Builders in constructing our msg +value. Instead of concatenating two ByteStrings together directly, we +monoidally append two different Builder values. The advantage to this is that +the results will end up being copied directly into the final output buffer, +instead of first being copied into a temporary ByteString buffer to only +later be copied into the final buffer.

+
+
+

Streaming response

+

Let’s give our streaming interface a test as well:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (fromByteString)
+import           Control.Concurrent       (threadDelay)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai
+import           Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $ responseStream status200 [("Content-Type", "text/plain")]
+    $ \send flush -> do
+        send $ fromByteString "Starting the response...\n"
+        flush
+        threadDelay 1000000
+        send $ fromByteString "All done!\n"
+
+main = run 3000 application
+

We use responseStream, and our third argument is a function which takes our +"send a builder" and "flush the buffer" functions. Notice how we flush after +our first chunk of data, to make sure the client sees the data immediately. +However, there’s no need to flush at the end of a response. WAI requires that +the handler automatically flush at the end of a stream.

+
+
+

Middleware

+

In addition to allowing our applications to run on multiple backends without +code changes, the WAI allows us another benefits: middleware. Middleware is +essentially an application transformer, taking one application and returning +another one.

+

Middleware components can be used to provide lots of services: cleaning up +URLs, authentication, caching, JSON-P requests. But perhaps the most useful and +most intuitive middleware is gzip compression. The middleware works very +simply: it parses the request headers to determine if a client supports +compression, and if so compresses the response body and adds the appropriate +response header.

+

The great thing about middlewares is that they are unobtrusive. Let’s see how +we would apply the gzip middleware to our hello world application.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.Wai.Handler.Warp (run)
+import Network.Wai.Middleware.Gzip (gzip, def)
+import Network.HTTP.Types (status200)
+
+application _ respond = respond $ responseLBS status200 [("Content-Type", "text/plain")]
+                       "Hello World"
+
+main = run 3000 $ gzip def application
+

We added an import line to actually have access to the middleware, and then +simply applied gzip to our application. You can also chain together multiple +middlewares: a line such as gzip False $ jsonp $ othermiddleware $ +myapplication is perfectly valid. One word of warning: the order the +middleware is applied can be important. For example, jsonp needs to work on +uncompressed data, so if you apply it after you apply gzip, you’ll have +trouble.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/widgets.html b/public/book-1.2/widgets.html new file mode 100644 index 00000000..339b5048 --- /dev/null +++ b/public/book-1.2/widgets.html @@ -0,0 +1,636 @@ + Widgets :: Yesod Web Framework Book- Version 1.2 +
+

Widgets

+ + +

One of the challenges in web development is that we have to coordinate three +different client-side technologies: HTML, CSS and Javascript. Worse still, we +have to place these components in different locations on the page: CSS in a +style tag in the head, Javascript in a script tag in the head, and HTML in the +body. And never mind if you want to put your CSS and Javascript in separate +files!

+

In practice, this works out fairly nicely when building a single page, because +we can separate our structure (HTML), style (CSS) and logic (Javascript). But +when we want to build modular pieces of code that can be easily composed, it +can be a headache to coordinate all three pieces separately. Widgets are +Yesod’s solution to the problem. They also help with the issue of including +libraries, such as jQuery, one time only.

+

Our four template languages- Hamlet, Cassius, Lucius and Julius- provide the +raw tools for constructing your output. Widgets provide the glue that allows +them to work together seamlessly.

+
+

Synopsis

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR = defaultLayout $ do
+    setTitle "My Page Title"
+    toWidget [lucius| h1 { color: green; } |]
+    addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
+    toWidget
+        [julius|
+            $(function() {
+                $("h1").click(function(){
+                    alert("You clicked on the heading!");
+                });
+            });
+        |]
+    toWidgetHead
+        [hamlet|
+            <meta name=keywords content="some sample keywords">
+        |]
+    toWidget
+        [hamlet|
+            <h1>Here's one way of including content
+        |]
+    [whamlet|<h2>Here's another |]
+    toWidgetBody
+        [julius|
+            alert("This is included in the body itself");
+        |]
+
+main = warp 3000 App
+

This produces the following HTML (indentation added):

+
<!DOCTYPE html>
+<html>
+  <head>
+    <title>My Page Title</title>
+    <meta name="keywords" content="some sample keywords">
+    <style>h1{color:green}</style>
+  </head>
+  <body>
+    <h1>Here's one way of including content</h1>
+    <h2>Here's another</h2>
+    <script>
+      alert("This is included in the body itself");
+    </script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">
+    </script><script>
+      $(function() {
+        $('h1').click(function() {
+          alert("You clicked on the heading!");
+        });
+      });
+    </script>
+  </body>
+</html>
+
+
+

What’s in a Widget?

+

At a very superficial level, an HTML document is just a bunch of nested tags. +This is the approach most HTML generation tools take: you define hierarchies of +tags and are done with it. But let’s imagine that I want to write a component +of a page for displaying the navbar. I want this to be "plug and play": I call +the function at the right time, and the navbar is inserted at the correct point +in the hierarchy.

+

This is where our superficial HTML generation breaks down. Our navbar likely +consists of some CSS and JavaScript in addition to HTML. By the time we call +the navbar function, we have already rendered the <head> tag, so it is too +late to add a new <style> tag for our CSS declarations. Under normal +strategies, we would need to break up our navbar function into three parts: +HTML, CSS and JavaScript, and make sure that we always call all three pieces.

+

Widgets take a different approach. Instead of viewing an HTML document as a +monolithic tree of tags, widgets see a number of distinct components in the +page. In particular:

+
    +
  • +

    +The title +

    +
  • +
  • +

    +External stylesheets +

    +
  • +
  • +

    +External Javascript +

    +
  • +
  • +

    +CSS declarations +

    +
  • +
  • +

    +Javascript code +

    +
  • +
  • +

    +Arbitrary <head> content +

    +
  • +
  • +

    +Arbitrary <body> content +

    +
  • +
+

Different components have different semantics. For example, there can only be +one title, but there can be multiple external scripts and stylesheets. However, +those external scripts and stylesheets should only be included once. Arbitrary +head and body content, on the other hand, has no limitation (someone may want +to have five lorem ipsum blocks after all).

+

The job of a widget is to hold onto these disparate components and apply proper +logic for combining different widgets together. This consists of things like +taking the last title set and ignoring others, filtering duplicates from the +list of external scripts and stylesheets, and concatenating head and body +content.

+
+
+

Constructing Widgets

+

In order to use widgets, you’ll obviously need to be able to get your hands on +them. The most common way will be via the ToWidget typeclass, and its +toWidget method. This allows you to convert your Shakespearean templates +directly to a Widget: Hamlet code will appear in the body, Julius scripts +inside a <script>, and Cassius and Lucius in a <style> tag.

+ +

But what if you want to add some <meta> tags, which need to appear in +the head? Or if you want some Javascript to appear in the body instead of the +head? For these purposes, Yesod provides two additional type classes: +ToWidgetHead and ToWidgetBody. These work exactly as they seem they should. One example use case for this is to have fine-grained control of where your <script> tags end up getting inserted.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "toWidgetHead and toWidgetBody"
+    toWidgetBody
+        [hamlet|<script src=/included-in-body.js>|]
+    toWidgetHead
+        [hamlet|<script src=/included-in-head.js>|]
+
+main :: IO ()
+main = warp 3001 App
+

Note that even though toWidgetHead was called after toWidgetBody, the +latter <script> tag appears first in the generated HTML.

+

In addition, there are a number of other functions for creating specific kinds +of Widgets:

+
+
+setTitle +
+

+Turns some HTML into the page title. +

+
+
+toWidgetMedia +
+

+Works the same as toWidget, but takes an +additional parameter to indicate what kind of media this applies to. Useful for +creating print stylesheets, for instance. +

+
+
+addStylesheet +
+

+Adds a reference, via a <link> tag, to an external +stylesheet. Takes a type-safe URL. +

+
+
+addStylesheetRemote +
+

+Same as addStylesheet, but takes a normal URL. Useful +for referring to files hosted on a CDN, like Google’s jQuery UI CSS files. +

+
+
+addScript +
+

+Adds a reference, via a <script> tag, to an external script. +Takes a type-safe URL. +

+
+
+addScriptRemote +
+

+Same as addScript, but takes a normal URL. Useful for +referring to files hosted on a CDN, like Google’s jQuery. +

+
+
+
+
+

Combining Widgets

+

The whole idea of widgets is to increase composability. You can take these +individual pieces of HTML, CSS and Javascript, combine them together into +something more complicated, and then combine these larger entities into +complete pages. This all works naturally through the Monad instance of +Widget, meaning you can use do-notation to compose pieces together.

+
myWidget1 = do
+    toWidget [hamlet|<h1>My Title|]
+    toWidget [lucius|h1 { color: green } |]
+
+myWidget2 = do
+    setTitle "My Page Title"
+    addScriptRemote "http://www.example.com/script.js"
+
+myWidget = do
+    myWidget1
+    myWidget2
+
+-- or, if you want
+myWidget' = myWidget1 >> myWidget2
+ +
+
+

Generate IDs

+

If we’re really going for true code reuse here, we’re eventually going to run +into name conflicts. Let’s say that there are two helper libraries that both +use the class name “foo” to affect styling. We want to avoid such a +possibility. Therefore, we have the newIdent function. This function +automatically generates a word that is unique for this handler.

+
getRootR = defaultLayout $ do
+    headerClass <- newIdent
+    toWidget [hamlet|<h1 .#{headerClass}>My Header|]
+    toWidget [lucius| .#{headerClass} { color: green; } |]
+
+
+

whamlet

+

Let’s say you’ve got a fairly standard Hamlet template, that embeds another +Hamlet template to represent the footer:

+
page =
+    [hamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+
+footer =
+    [hamlet|
+        <footer>
+            <p>That's all folks!
+    |]
+

That works fine if the footer is plain old HTML, but what if we want to add +some style? Well, we can easily spice up the footer by turning it into a +Widget:

+
footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+

But now we’ve got a problem: a Hamlet template can only embed another Hamlet +template; it knows nothing about a Widget. This is where whamlet comes in. It +takes exactly the same syntax as normal Hamlet, and variable (#{…}) and URL +(@{…}) interpolation are unchanged. But embedding (^{…}) takes a Widget, +and the final result is a Widget. To use it, we can just do:

+
page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

There is also whamletFile, if you would prefer to keep your template in a +separate file.

+ +
+

Types

+

You may have noticed that I’ve been avoiding type signatures so far. The simple +answer is that each widget is a value of type Widget. But if you look through +the Yesod libraries, you’ll find no definition of the Widget type. What +gives?

+

Yesod defines a very similar type: data WidgetT site m a. This data type is a +monad transformer. The last two arguments are the underlying monad and the +monadic value, respectively. The site parameter is the individual foundation +type for your individual application. Since this type varies for each and every +site, it’s impossible for the libraries to define a single Widget datatype +which would work for every application.

+

Instead, the mkYesod Template Haskell function generates this type synonym +for you. Assuming your foundation data type is called MyApp, your Widget +synonym is defined as:

+
type Widget = WidgetT MyApp IO ()
+

We set the monadic value to be (), since a widget’s value will ultimately be +thrown away. IO is the standard base monad, and will be used in almost all +cases. The only exception is when writing a subsite. Subsites are a more +advanced topic, and will be covered later in their own chapter.

+

Once we know about our Widget type synonym, it’s easy to add signatures to +our previous code samples:

+
footer :: Widget
+footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+
+page :: Widget
+page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

When we start digging into handler functions some more, we’ll encounter a +similar situation with the HandlerT and Handler types.

+
+
+
+

Using Widgets

+

It’s all well and good that we have these beautiful Widget datatypes, but how +exactly do we turn them into something the user can interact with? The most +commonly used function is defaultLayout, which essentially has the type +signature Widget → Handler Html.

+

defaultLayout is actually a typeclass method, which can be overridden for +each application. This is how Yesod apps are themed. So we’re still left with +the question: when we’re inside defaultLayout, how do we unwrap a Widget? +The answer is widgetToPageContent. Let’s look at some (simplified) types:

+
widgetToPageContent :: Widget -> Handler (PageContent url)
+data PageContent url = PageContent
+    { pageTitle :: Html
+    , pageHead :: HtmlUrl url
+    , pageBody :: HtmlUrl url
+    }
+

This is getting closer to what we need. We now have direct access to the HTML +making up the head and body, as well as the title. At this point, we can use +Hamlet to combine them all together into a single document, along with our site +layout, and we use giveUrlRenderer to convert that Hamlet result into actual +HTML that’s ready to be shown to the user. The next figure demonstrates this +process.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent widget
+    giveUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    <style>body { font-family: verdana }
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

This is all well and good, but there’s one thing that bothers me: that style +tag. There are a few problems with it:

+
    +
  • +

    +Unlike Lucius or Cassius, it doesn’t get compile-time checked for + correctness. +

    +
  • +
  • +

    +Granted that the current example is very simple, but in something more + complicated we could get into character escaping issues. +

    +
  • +
  • +

    +We’ll now have two style tags instead of one: the one produced by myLayout, + and the one generated in the pageHead based on the styles set in the + widget. +

    +
  • +
+

We have one more trick in our bag to address this: we apply some last-minute +adjustments to the widget itself before calling widgetToPageContent. It’s +actually very easy to do: we just use do-notation again.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent $ do
+        widget
+        toWidget [lucius| body { font-family: verdana } |]
+    giveUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Using handler functions

+

We haven’t covered too much of the handler functionality yet, but once we do, +the question arises: how do we use those functions in a widget? For example, +what if your widget needs to look up a query string parameter using +lookupGetParam?

+

The first answer is the function handlerToWidget, which can convert a +Handler action into a Widget answer. However, in many cases, this won’t be +necessary. Consider the type signature of lookupGetParam:

+
lookupGetParam :: MonadHandler m => Text -> m (Maybe Text)
+

This function will live in any instance of MonadHandler. And conveniently, +Widget is also a MonadHandler instance. This means that most code can be +run in either Handler or Widget. And if you need to explicitly convert from +Handler to Widget, you can always use handlerToWidget.

+ +
+
+

Summary

+

The basic building block of each page is a widget. Individual snippets of HTML, +CSS, and Javascript can be turned into widgets via the polymorphic toWidget +function. Using do-notation, you can combine these individual widgets into +larger widgets, eventually containing all the content of your page.

+

Unwrapping these widgets is usually performed within the defaultLayout +function, which can be used to apply a unified look-and-feel to all your pages.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/wiki-chat-example.html b/public/book-1.2/wiki-chat-example.html new file mode 100644 index 00000000..d02ae504 --- /dev/null +++ b/public/book-1.2/wiki-chat-example.html @@ -0,0 +1,553 @@ + Wiki: markdown, chat subsite, event source :: Yesod Web Framework Book- Version 1.2 +
+

Wiki: markdown, chat subsite, event source

+ + +

This example will tie together a few different ideas. We’ll start with a chat +subsite, which allows us to embed a chat widget on any page. We’ll use the HTML +5 event source API to handle sending events from the server to the client. You +can view the entire project on +FP +Haskell Center.

+
+

Subsite: data

+

In order to define a subsite, we first need to create a foundation type for the +subsite, the same as we would do for a normal Yesod application. In our case, +we want to keep a channel of all the events to be sent to the individual +participants of a chat. This ends up looking like:

+
-- @Chat/Data.hs
+module Chat.Data where
+
+import           Control.Concurrent.Chan (Chan)
+import           Network.Wai.EventSource (ServerEvent)
+import           Yesod
+
+-- | Our subsite foundation. We keep a channel of events that all connections
+-- will share.
+data Chat = Chat (Chan ServerEvent)
+

We also need to define our subsite routes in the same module. We need to have +two commands: one to send a new message to all users, and another to receive +the stream of messages.

+
-- @Chat/Data.hs
+mkYesodSubData "Chat" [parseRoutes|
+/send SendR POST
+/recv ReceiveR GET
+|]
+
+
+

Subsite: handlers

+

Now that we’ve defined our foundation and routes, we need to create a separate +module for providing the subsite dispatch functionality. We’ll call this +module Chat, and it’s where we’ll start to see how a subsite functions.

+

A subsite always sits as a layer on top of some master site, which will be +provided by the user. In many cases, a subsite will require specific +functionality to be present in the master site. In the case of our chat +subsite, we want user authentication to be provided by the master site. The +subsite needs to be able to query whether the current user is logged into the +site, and to get the user’s name.

+

The way we represent this concept is to define a typeclass that encapsulates +the necessary functionality. Let’s have a look at our YesodChat typeclass:

+
-- @Chat/Data.hs
+class (Yesod master, RenderMessage master FormMessage)
+        => YesodChat master where
+    getUserName :: HandlerT master IO Text
+    isLoggedIn :: HandlerT master IO Bool
+

Any master site which wants to use the chat subsite will need to provide a +YesodChat instance. (We’ll see in a bit how this requirement is enforced.) +There are a few interesting things to note:

+
    +
  • +

    +We can put further constraints on the master site, such as providing a + Yesod instance and allowing rendering of form messages. The former allows + us to use defaultLayout, while the latter allows us to use standard form + widgets. +

    +
  • +
  • +

    +Previously in the book, we’ve used the Handler monad quite a bit. Remember + that Handler is just an application-specific type synonym around + HandlerT. Since this code is intended to work with many different + applications, we use the full HandlerT form of the transformer. +

    +
  • +
+

Speaking of the Handler type synonym, we’re going to want to have something +similar for our subsite. The question is: what does this monad look like? In a +subsite situation, we end up with two layers of HandlerT transformers: one +for the subsite, and one for the master site. We want to have a synonym that +works for any master site which is an instance of YesodChat, so we end up +with:

+
-- @Chat/Data.hs
+type ChatHandler a =
+    forall master. YesodChat master =>
+    HandlerT Chat (HandlerT master IO) a
+

Now that we have our machinery out of the way, it’s time to write our subsite +handler functions. We had two routes: one for sending messages, and one for +receiving messages. Let’s start with sending. We need to:

+
    +
  1. +

    +Get the username for the person sending the message. +

    +
  2. +
  3. +

    +Parse the message from the incoming parameters. (Note that we’re going to use GET parameters for simplicity of the client-side Ajax code.) +

    +
  4. +
  5. +

    +Write the message to the Chan. +

    +
  6. +
+

The trickiest bit of all this code is to know when to use lift. Let’s look at +the implementation, and then discuss those lift usages:

+
-- @Chat/Data.hs
+postSendR :: ChatHandler ()
+postSendR = do
+    from <- lift getUserName
+    body <- lift $ runInputGet $ ireq textField "message"
+    Chat chan <- getYesod
+    liftIO $ writeChan chan $ ServerEvent Nothing Nothing $ return $
+        fromText from <> fromText ": " <> fromText body
+

getUserName is the function we defined in our YesodChat typeclass earlier. +If we look at that type signature, we see that it lives in the master site’s +Handler monad. Therefore, we need to lift that call out of the subsite.

+

The call to runInputGet is a little more subtle. Theoretically, we could run +this in either the subsite or the master site. However, we use lift here as +well for one specific reason: message translations. By using the master site, +we can take advantage of whatever RenderMessage instance the master site +defines. This also explains why we have a RenderMessage constraint on the +YesodChat typeclass.

+

The next call to getYesod is not lifted. The reasoning here is simple: +we want to get the subsite’s foundation type in order to access the message +channel. If we instead lifted that call, we’d get the master site’s +foundation type instead, which is not what we want in this case.

+

The final line puts the new message into the channel. Since this is an IO +action, we use liftIO. ServerEvent is part of the wai-eventsource +package, and is the means by which we’re providing server-sent events in this +example.

+

The receiving side is similarly simple:

+
-- @Chat/Data.hs
+getReceiveR :: ChatHandler ()
+getReceiveR = do
+    Chat chan0 <- getYesod
+    chan <- liftIO $ dupChan chan0
+    req <- waiRequest
+    res <- liftResourceT $ eventSourceAppChan chan req
+    sendWaiResponse res
+

We use dupChan so that each new connection receives its own copy of newly +generated messages. This is a standard method in concurrent Haskell of creating +broadcast channels. The last three lines of our function expose the underlying +wai-eventsource application as a Yesod handler, by getting the raw WAI +request, running the application on that request, and then sending the raw WAI +response.

+ +

Now that we’ve defined our handler functions, we can set up our dispatch. In a +normal application, dispatching is handled by calling mkYesod, which creates +the appropriate YesodDispatch instance. In subsites, things are a little bit +more complicated, since you’ll often want to place constraints on the master +site. The formula we use is the following:

+
-- @Chat/Data.hs
+instance YesodChat master => YesodSubDispatch Chat (HandlerT master IO) where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesChat)
+

We’re stating that our Chat subsite can live on top of any master site which +is an instance of YesodChat. We then use the mkYesodSubDispatch Template +Haskell function to generate all of our dispatching logic. While this is a bit +more difficult to write than mkYesod, it provides necessary flexibility, and +is mostly identical for any subsite you’ll write.

+
+
+

Subsite: widget

+

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.

+

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 +URLs. Let’s start off our widget:

+
-- @Chat/Data.hs
+chatWidget :: YesodChat master
+           => (Route Chat -> Route master)
+           -> WidgetT master IO ()
+chatWidget toMaster = do
+

Next, we’re going to generate some identifiers to be used by our widget. It’s +always good practice to let Yesod generate unique identifiers for you instead +of creating them manually to avoid name collisions.

+
-- @Chat/Data.hs
+    chat <- newIdent   -- the containing div
+    output <- newIdent -- the box containing the messages
+    input <- newIdent  -- input field from the user
+

And next we need to check if the user is logged in, using the isLoggedIn +function in our YesodChat typeclass. Since we’re in a Widget and that +function lives in the Handler monad, we need to use handlerToWidget:

+
-- @Chat/Data.hs
+    ili <- handlerToWidget isLoggedIn  -- check if we're already logged in
+

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 +client-side code wrapped in a Widget:

+
-- @Chat/Data.hs
+    if ili
+        then do
+            -- Logged in: show the widget
+            [whamlet|
+                <div ##{chat}>
+                    <h2>Chat
+                    <div ##{output}>
+                    <input ##{input} type=text placeholder="Enter Message">
+            |]
+            -- Just some CSS
+            toWidget [lucius|
+                ##{chat} {
+                    position: absolute;
+                    top: 2em;
+                    right: 2em;
+                }
+                ##{output} {
+                    width: 200px;
+                    height: 300px;
+                    border: 1px solid #999;
+                    overflow: auto;
+                }
+            |]
+            -- And now that Javascript
+            toWidgetBody [julius|
+                // Set up the receiving end
+                var output = document.getElementById(#{toJSON output});
+                var src = new EventSource("@{toMaster ReceiveR}");
+                src.onmessage = function(msg) {
+                    // This function will be called for each new message.
+                    var p = document.createElement("p");
+                    p.appendChild(document.createTextNode(msg.data));
+                    output.appendChild(p);
+
+                    // And now scroll down within the output div so the most recent message
+                    // is displayed.
+                    output.scrollTop = output.scrollHeight;
+                };
+
+                // Set up the sending end: send a message via Ajax whenever the user hits
+                // enter.
+                var input = document.getElementById(#{toJSON input});
+                input.onkeyup = function(event) {
+                    var keycode = (event.keyCode ? event.keyCode : event.which);
+                    if (keycode == '13') {
+                        var xhr = new XMLHttpRequest();
+                        var val = input.value;
+                        input.value = "";
+                        var params = "?message=" + encodeURI(val);
+                        xhr.open("POST", "@{toMaster SendR}" + params);
+                        xhr.send(null);
+                    }
+                }
+            |]
+

And finally, if the user isn’t logged in, we’ll ask them to log in to use the +chat app.

+
-- @Chat/Data.hs
+        else do
+            -- User isn't logged in, give a not-logged-in message.
+            master <- getYesod
+            [whamlet|
+                <p>
+                    You must be #
+                    $maybe ar <- authRoute master
+                        <a href=@{ar}>logged in
+                    $nothing
+                        logged in
+                    \ to chat.
+            |]
+
+
+

Master site: data

+

Now we can proceed with writing our main application. This application will +include the chat subsite and a wiki. The first thing we need to consider is how +to store the wiki contents. Normally, we’d want to put this in some kind of a +Persistent database. For simplicity, we’ll just use an in-memory +representation. Each Wiki page is indicated by a list of names, and the contents of each page is going to be a piece of Text. So our full foundation datatype is:

+
data App = App
+    { getChat     :: Chat
+    , wikiContent :: IORef (Map.Map [Text] Text)
+    }
+

Next we want to set up our routes:

+
mkYesod "App" [parseRoutes|
+/            HomeR GET      -- the homepage
+/wiki/*Texts WikiR GET POST -- note the multipiece for the wiki hierarchy
+
+/chat        ChatR Chat getChat    -- the chat subsite
+/auth        AuthR Auth getAuth    -- the auth subsite
+|]
+
+
+

Master site: instances

+

We need to make two modifications to the default Yesod instance. Firstly, we +want to provide an implementation of authRoute, so that our chat subsite +widget can provide a proper link to a login page. Secondly, we’ll provide a +override to the defaultLayout. Besides providing login/logout links, this +function will add in the chat widget on every page.

+
instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR -- get a working login link
+
+    -- Our custom defaultLayout will add the chat widget to every page.
+    -- We'll also add login and logout links to the top.
+    defaultLayout widget = do
+        pc <- widgetToPageContent $ do
+            widget
+            chatWidget ChatR
+        mmsg <- getMessage
+        giveUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <div .message>#{msg}
+                        <nav>
+                            <a href=@{AuthR LoginR}>Login
+                            \ | #
+                            <a href=@{AuthR LogoutR}>Logout
+                        ^{pageBody pc}
+            |]
+

Since we’re using the chat subsite, we have to provide an instance of +YesodChat.

+
instance YesodChat App where
+    getUserName = do
+        muid <- maybeAuthId
+        case muid of
+            Nothing -> do
+                setMessage "Not logged in"
+                redirect $ AuthR LoginR
+            Just uid -> return uid
+    isLoggedIn = do
+        ma <- maybeAuthId
+        return $ maybe False (const True) ma
+

Our YesodAuth and RenderMessage instances, as well as the homepage handler, +are rather bland:

+
-- Fairly standard YesodAuth instance. We'll use the dummy plugin so that you
+-- can create any name you want, and store the login name as the AuthId.
+instance YesodAuth App where
+    type AuthId App = Text
+    authPlugins _ = [authDummy]
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authHttpManager = error "authHttpManager" -- not used by authDummy
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Nothing special here, just giving a link to the root of the wiki.
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Welcome to the Wiki!
+        <p>
+            <a href=@{wikiRoot}>Wiki root
+    |]
+  where
+    wikiRoot = WikiR []
+
+
+

Master site: wiki handlers

+

Now it’s time to write our wiki handlers: a GET for displaying a page, and a +POST for updating a page. We’ll also define a wikiForm function to be used on +both handlers:

+
-- A form for getting wiki content
+wikiForm :: Maybe Textarea -> Html -> MForm Handler (FormResult Textarea, Widget)
+wikiForm mtext = renderDivs $ areq textareaField "Page body" mtext
+
+-- Show a wiki page and an edit form
+getWikiR :: [Text] -> Handler Html
+getWikiR page = do
+    -- Get the reference to the contents map
+    icontent <- fmap wikiContent getYesod
+
+    -- And read the map from inside the reference
+    content <- liftIO $ I.readIORef icontent
+
+    -- Lookup the contents of the current page, if available
+    let mtext = Map.lookup page content
+
+    -- Generate a form with the current contents as the default value.
+    -- Note that we use the Textarea wrapper to get a <textarea>.
+    (form, _) <- generateFormPost $ wikiForm $ fmap Textarea mtext
+    defaultLayout $ do
+        case mtext of
+            -- We're treating the input as markdown. The markdown package
+            -- automatically handles XSS protection for us.
+            Just text -> toWidget $ markdown def $ TL.fromStrict text
+            Nothing -> [whamlet|<p>Page does not yet exist|]
+        [whamlet|
+            <h2>Edit page
+            <form method=post>
+                ^{form}
+                <div>
+                    <input type=submit>
+        |]
+
+-- Get a submitted wiki page and updated the contents.
+postWikiR :: [Text] -> Handler Html
+postWikiR page = do
+    icontent <- fmap wikiContent getYesod
+    content <- liftIO $ I.readIORef icontent
+    let mtext = Map.lookup page content
+    ((res, form), _) <- runFormPost $ wikiForm $ fmap Textarea mtext
+    case res of
+        FormSuccess (Textarea t) -> do
+            liftIO $ I.atomicModifyIORef icontent $
+                \m -> (Map.insert page t m, ())
+            setMessage "Page updated"
+            redirect $ WikiR page
+        _ -> defaultLayout
+                [whamlet|
+                    <form method=post>
+                        ^{form}
+                        <div>
+                            <input type=submit>
+                |]
+
+
+

Master site: running

+

Finally, we’re ready to run our application. Unlike many of our previous +examples in this book, we need to perform some real initialization in the +main function. The Chat subsite requires an empty Chan to be created, and +we need to create a mutable variable to hold the wiki contents. Once we have +those values, we can create an App value and pass it to the warp function.

+
main :: IO ()
+main = do
+    -- Create our server event channel
+    chan <- newChan
+
+    -- Initially have a blank database of wiki pages
+    icontent <- I.newIORef Map.empty
+
+    -- Run our app
+    warpEnv App
+        { getChat = Chat chan
+        , wikiContent = icontent
+        }
+
+
+

Conclusion

+

This example demonstrated creation of a non-trivial subsite. Some important +points to notice were the usage of typeclasses to express constraints on the +master site, how data initialization was performed in the main function, and +how lifting allowed us to operate in either the subsite or master site +context.

+

If you’re looking for a way to test out your subsite skills, I’d recommend +modifying this example so that the Wiki code also lived in its own subsite.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/xml.html b/public/book-1.2/xml.html new file mode 100644 index 00000000..7559b829 --- /dev/null +++ b/public/book-1.2/xml.html @@ -0,0 +1,834 @@ + xml-conduit :: Yesod Web Framework Book- Version 1.2 +
+

xml-conduit

+ + +

Many developers cringe at the thought of dealing with XML files. XML has the +reputation of having a complicated data model, with obfuscated libraries and +huge layers of complexity sitting between you and your goal. I’d like to posit +that a lot of that pain is actually a language and library issue, not inherent +to XML.

+

Once again, Haskell’s type system allows us to easily break down the problem to +its most basic form. The xml-types package neatly deconstructs the XML data +model (both a streaming and DOM-based approach) into some simple ADTs. +Haskell’s standard immutable data structures make it easier to apply transforms +to documents, and a simple set of functions makes parsing and rendering a +breeze.

+

We’re going to be covering the xml-conduit package. Under the surface, this +package uses a lot of the approaches Yesod in general does for high +performance: blaze-builder, text, conduit and attoparsec. But from a user +perspective, it provides everything from the simplest APIs +(readFile/writeFile) through full control of XML event streams.

+

In addition to xml-conduit, there are a few related packages that come into +play, like xml-hamlet and xml2html. We’ll cover both how to use all these +packages, and when they should be used.

+
+

Synopsis

+
<!-- Input XML file -->
+<document title="My Title">
+    <para>This is a paragraph. It has <em>emphasized</em> and <strong>strong</strong> words.</para>
+    <image href="myimage.png"/>
+</document>
+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import qualified Data.Map        as M
+import           Prelude         hiding (readFile, writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = do
+    -- readFile will throw any parse errors as runtime exceptions
+    -- def uses the default settings
+    Document prologue root epilogue <- readFile def "input.xml"
+
+    -- root is the root element of the document, let's modify it
+    let root' = transform root
+
+    -- And now we write out. Let's indent our output
+    writeFile def
+        { rsPretty = True
+        } "output.html" $ Document prologue root' epilogue
+
+-- We'll turn out <document> into an XHTML document
+transform :: Element -> Element
+transform (Element _name attrs children) = Element "html" M.empty
+    [xml|
+        <head>
+            <title>
+                $maybe title <- M.lookup "title" attrs
+                    \#{title}
+                $nothing
+                    Untitled Document
+        <body>
+            $forall child <- children
+                ^{goNode child}
+    |]
+
+goNode :: Node -> [Node]
+goNode (NodeElement e) = [NodeElement $ goElem e]
+goNode (NodeContent t) = [NodeContent t]
+goNode (NodeComment _) = [] -- hide comments
+goNode (NodeInstruction _) = [] -- and hide processing instructions too
+
+-- convert each source element to its XHTML equivalent
+goElem :: Element -> Element
+goElem (Element "para" attrs children) =
+    Element "p" attrs $ concatMap goNode children
+goElem (Element "em" attrs children) =
+    Element "i" attrs $ concatMap goNode children
+goElem (Element "strong" attrs children) =
+    Element "b" attrs $ concatMap goNode children
+goElem (Element "image" attrs _children) =
+    Element "img" (fixAttr attrs) [] -- images can't have children
+  where
+    fixAttr mattrs
+        | "href" `M.member` mattrs  = M.delete "href" $ M.insert "src" (mattrs M.! "href") mattrs
+        | otherwise                 = mattrs
+goElem (Element name attrs children) =
+    -- don't know what to do, just pass it through...
+    Element name attrs $ concatMap goNode children
+
<?xml version="1.0" encoding="UTF-8"?>
+<!-- Output XHTML -->
+<html>
+    <head>
+        <title>
+            My Title
+        </title>
+    </head>
+    <body>
+        <p>
+            This is a paragraph. It has
+            <i>
+                emphasized
+            </i>
+            and
+            <b>
+                strong
+            </b>
+            words.
+        </p>
+        <img src="myimage.png"/>
+    </body>
+</html>
+
+
+

Types

+

Let’s take a bottom-up approach to analyzing types. This section will also +serve as a primer on the XML data model itself, so don’t worry if you’re not +completely familiar with it.

+

I think the first place where Haskell really shows its strength is with the +Name datatype. Many languages (like Java) struggle with properly expressing +names. The issue is that there are in fact three components to a name: its +local name, its namespace (optional), and its prefix (also optional). Let’s +look at some XML to explain:

+
<no-namespace/>
+<no-prefix xmlns="first-namespace" first-attr="value1"/>
+<foo:with-prefix xmlns:foo="second-namespace" foo:second-attr="value2"/>
+

The first tag has a local name of no-namespace, and no namespace or prefix. +The second tag (local name: no-prefix) also has no prefix, but it does have +a namespace (first-namespace). first-attr, however, does not inherit that +namespace: attribute namespaces must always be explicitly set with a prefix.

+ +

The third tag has a local name of with-prefix, a prefix of foo and a +namespace of second-namespace. Its attribute has a second-attr local name +and the same prefix and namespace. The xmlns and xmlns:foo attributes are +part of the namespace specification, and are not considered attributes of their +respective elements.

+

So let’s review what we need from a name: every name has a local name, and it +can optionally have a prefix and namespace. Seems like a simple fit for a +record type:

+
data Name = Name
+    { nameLocalName :: Text
+    , nameNamespace :: Maybe Text
+    , namePrefix    :: Maybe Text
+    }
+

According the XML namespace standard, two names are considered equivalent +if they have the same localname and namespace. In other words, the prefix is +not important. Therefore, xml-types defines Eq and Ord instances that +ignore the prefix.

+

The last class instance worth mentioning is IsString. It would be very +tedious to have to manually type out Name "p" Nothing Nothing every time we +want a paragraph. If you turn on OverloadedStrings, "p" will resolve to +that all by itself! In addition, the IsString instance recognizes something +called Clark notation, which allows you to prefix the namespace surrounded in +curly brackets. In other words:

+
"{namespace}element" == Name "element" (Just "namespace") Nothing
+"element" == Name "element" Nothing Nothing
+
+

The Four Types of Nodes

+

XML documents are a tree of nested nodes. There are in fact four different +types of nodes allowed: elements, content (i.e., text), comments, and +processing instructions.

+ +

Since processing instructions have two pieces of text associated with them (the +target and the data), we have a simple data type:

+
data Instruction = Instruction
+    { instructionTarget :: Text
+    , instructionData :: Text
+    }
+

Comments have no special datatype, since they are just text. But content is an +interesting one: it could contain either plain text or unresolved entities +(e.g., &copyright-statement;). xml-types keeps those unresolved entities +in all the data types in order to completely match the spec. However, in +practice, it can be very tedious to program against those data types. And in +most use cases, an unresolved entity is going to end up as an error anyway.

+

Therefore, the Text.XML module defines its own set of datatypes for nodes, +elements and documents that removes all unresolved entities. If you need to +deal with unresolved entities instead, you should use the Text.XML.Unresolved +module. From now on, we’ll be focusing only on the Text.XML data types, +though they are almost identical to the xml-types versions.

+

Anyway, after that detour: content is just a piece of text, and therefore it +too does not have a special datatype. The last node type is an element, which +contains three pieces of information: a name, a map of attribute name/value +pairs, and a list of children nodes. (In xml-types, this value could contain +unresolved entities as well.) So our Element is defined as:

+
data Element = Element
+    { elementName :: Name
+    , elementAttributes :: Map Name Text
+    , elementNodes :: [Node]
+    }
+

Which of course begs the question: what does a Node look like? This is where +Haskell really shines: its sum types model the XML data model perfectly.

+
data Node
+    = NodeElement Element
+    | NodeInstruction Instruction
+    | NodeContent Text
+    | NodeComment Text
+
+
+

Documents

+

So now we have elements and nodes, but what about an entire document? Let’s +just lay out the datatypes:

+
data Document = Document
+    { documentPrologue :: Prologue
+    , documentRoot :: Element
+    , documentEpilogue :: [Miscellaneous]
+    }
+
+data Prologue = Prologue
+    { prologueBefore :: [Miscellaneous]
+    , prologueDoctype :: Maybe Doctype
+    , prologueAfter :: [Miscellaneous]
+    }
+
+data Miscellaneous
+    = MiscInstruction Instruction
+    | MiscComment Text
+
+data Doctype = Doctype
+    { doctypeName :: Text
+    , doctypeID :: Maybe ExternalID
+    }
+
+data ExternalID
+    = SystemID Text
+    | PublicID Text Text
+

The XML spec says that a document has a single root element (documentRoot). +It also has an optional doctype statement. Before and after both the doctype +and the root element, you are allowed to have comments and processing +instructions. (You can also have whitespace, but that is ignored in the +parsing.)

+

So what’s up with the doctype? Well, it specifies the root element of the +document, and then optional public and system identifiers. These are used to +refer to DTD files, which give more information about the file (e.g., +validation rules, default attributes, entity resolution). Let’s see some +examples:

+
<!DOCTYPE root> <!-- no external identifier -->
+<!DOCTYPE root SYSTEM "root.dtd"> <!-- a system identifier -->
+<!DOCTYPE root PUBLIC "My Root Public Identifier" "root.dtd"> <!-- public identifiers have a system ID as well -->
+

And that, my friends, is the entire XML data model. For many parsing purposes, +you’ll be able to simply ignore the entire Document datatype and go +immediately to the documentRoot.

+
+
+

Events

+

In addition to the document API, xml-types defines an Event datatype. This +can be used for constructing streaming tools, which can be much more memory +efficient for certain kinds of processing (eg, adding an extra attribute to all +elements). We will not be covering the streaming API currently, though it +should look very familiar after analyzing the document API.

+ +
+
+
+

Text.XML

+

The recommended entry point to xml-conduit is the Text.XML module. This module +exports all of the datatypes you’ll need to manipulate XML in a DOM fashion, as +well as a number of different approaches for parsing and rendering XML content. +Let’s start with the simple ones:

+
readFile  :: ParseSettings  -> FilePath -> IO Document
+writeFile :: RenderSettings -> FilePath -> Document -> IO ()
+

This introduces the ParseSettings and RenderSettings datatypes. You can use +these to modify the behavior of the parser and renderer, such as adding +character entities and turning on pretty (i.e., indented) output. Both these +types are instances of the Default typeclass, so you can simply use def when +these need to be supplied. That is how we will supply these values through the +rest of the chapter; please see the API docs for more information.

+

It’s worth pointing out that in addition to the file-based API, there is also a +text- and bytestring-based API. The bytestring-powered functions all perform +intelligent encoding detections, and support UTF-8, UTF-16 and UTF-32, in +either big or little endian, with and without a Byte-Order Marker (BOM). All +output is generated in UTF-8.

+

For complex data lookups, we recommend using the higher-level cursors API. The +standard Text.XML API not only forms the basis for that higher level, but is +also a great API for simple XML transformations and for XML generation. See the +synopsis for an example.

+
+

A note about file paths

+

In the type signature above, we have a type FilePath. However, this isn’t +Prelude.FilePath. The standard Prelude defines a type synonym type FilePath += [Char]. Unfortunately, there are many limitations to using such an +approach, including confusion of filename character encodings and differences +in path separators.

+

Instead, xml-conduit uses the system-filepath package, which defines an +abstract FilePath type. I’ve personally found this to be a much nicer +approach to work with. The package is fairly easy to follow, so I won’t go into +details here. But I do want to give a few quick explanations of how to use it:

+
    +
  • +

    +Since a FilePath is an instance of IsString, you can type in regular + strings and they will be treated properly, as long as the OverloadedStrings + extension is enabled. (I highly recommend enabling it anyway, as it makes + dealing with Text values much more pleasant.) +

    +
  • +
  • +

    +If you need to explicitly convert to or from Prelude's FilePath, you + should use the encodeString and decodeString, respectively. This takes into + account file path encodings. +

    +
  • +
  • +

    +Instead of manually splicing together directory names and file names with + extensions, use the operators in the Filesystem.Path.CurrentOS module, e.g. + myfolder </> filename <.> extension. +

    +
  • +
+
+
+
+

Cursor

+

Suppose you want to pull the title out of an XHTML document. You could do so +with the Text.XML interface we just described, using standard pattern +matching on the children of elements. But that would get very tedious, very +quickly. Probably the gold standard for these kinds of lookups is XPath, where +you would be able to write /html/head/title. And that’s exactly what inspired +the design of the Text.XML.Cursor combinators.

+

A cursor is an XML node that knows its location in the tree; it’s able to +traverse upwards, sideways, and downwards. (Under the surface, this is achieved +by tying the knot.) +There are two functions available for creating cursors from Text.XML types: +fromDocument and fromNode.

+

We also have the concept of an Axis, defined as type Axis = Cursor -> +[Cursor]. It’s easiest to get started by looking at example axes: child +returns zero or more cursors that are the child of the current one, parent +returns the single parent cursor of the input, or an empty list if the input is +the root element, and so on.

+

In addition, there are some axes that take predicates. element is a commonly +used function that filters down to only elements which match the given name. +For example, element "title" will return the input element if its name is +"title", or an empty list otherwise.

+

Another common function which isn’t quite an axis is content :: Cursor +-> [Text]. For all content nodes, it returns the contained text; +otherwise, it returns an empty list.

+

And thanks to the monad instance for lists, it’s easy to string all of these +together. For example, to do our title lookup, we would write the following +program:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+            child cursor >>= element "head" >>= child
+                         >>= element "title" >>= descendant >>= content
+

What this says is:

+
    +
  1. +

    +Get me all the child nodes of the root element +

    +
  2. +
  3. +

    +Filter down to only the elements named "head" +

    +
  4. +
  5. +

    +Get all the children of all those head elements +

    +
  6. +
  7. +

    +Filter down to only the elements named "title" +

    +
  8. +
  9. +

    +Get all the descendants of all those title elements. (A descendant is a + child, or a descendant of a child. Yes, that was a recursive definition.) +

    +
  10. +
  11. +

    +Get only the text nodes. +

    +
  12. +
+

So for the input document:

+
<html>
+    <head>
+        <title>My <b>Title</b></title>
+    </head>
+    <body>
+        <p>Foo bar baz</p>
+    </body>
+</html>
+

We end up with the output My Title. This is all well and good, but it’s much +more verbose than the XPath solution. To combat this verbosity, Aristid +Breitkreuz added a set of operators to the Cursor module to handle many common +cases. So we can rewrite our example as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $/ element "head" &/ element "title" &// content
+

$/ says to apply the axis on the right to the children of the cursor on the +left. &/ is almost identical, but is instead used to combine two axes +together. This is a general rule in Text.XML.Cursor: operators beginning with +$ directly apply an axis, while & will combine two together. &// is +used for applying an axis to all descendants.

+

Let’s go for a more complex, if more contrived, example. We have a document +that looks like:

+
<html>
+    <head>
+        <title>Headings</title>
+    </head>
+    <body>
+        <hgroup>
+            <h1>Heading 1 foo</h1>
+            <h2 class="foo">Heading 2 foo</h2>
+        </hgroup>
+        <hgroup>
+            <h1>Heading 1 bar</h1>
+            <h2 class="bar">Heading 2 bar</h2>
+        </hgroup>
+    </body>
+</html>
+

We want to get the content of all the h1 tags which precede an h2 tag with +a class attribute of "bar". To perform this convoluted lookup, we can write:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test2.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $// element "h2"
+               >=> attributeIs "class" "bar"
+               >=> precedingSibling
+               >=> element "h1"
+               &// content
+

Let’s step through that. First we get all h2 elements in the document. ($// +gets all descendants of the root element.) Then we filter out only those with +class=bar. That >=> operator is actually the standard operator from +Control.Monad; yet another advantage of the monad instance of lists. +precedingSibling finds all nodes that come before our node and share the +same parent. (There is also a preceding axis which takes all elements earlier +in the tree.) We then take just the h1 elements, and then grab their content.

+ +

While the cursor API isn’t quite as succinct as XPath, it has the advantages of +being standard Haskell code, and of type safety.

+
+
+

xml-hamlet

+

Thanks to the simplicity of Haskell’s data type system, creating XML content +with the Text.XML API is easy, if a bit verbose. The following code:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Data.Map (empty)
+import           Prelude  hiding (writeFile)
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty
+        [ NodeElement $ Element "head" empty
+            [ NodeElement $ Element "title" empty
+                [ NodeContent "My "
+                , NodeElement $ Element "b" empty
+                    [ NodeContent "Title"
+                    ]
+                ]
+            ]
+        , NodeElement $ Element "body" empty
+            [ NodeElement $ Element "p" empty
+                [ NodeContent "foo bar baz"
+                ]
+            ]
+        ]
+

produces

+
<?xml version="1.0" encoding="UTF-8"?>
+<html><head><title>My <b>Title</b></title></head><body><p>foo bar baz</p></body></html>
+

This is leaps and bounds easier than having to deal with an imperative, +mutable-value-based API (cough, Java, cough), but it’s far from pleasant, and +obscures what we’re really trying to achieve. To simplify things, we have the +xml-hamlet package, which using Quasi-Quotation to allow you to type in your +XML in a natural syntax. For example, the above could be rewritten as:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map        (empty)
+import           Prelude         hiding (writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>
+        My #
+        <b>Title
+<body>
+    <p>foo bar baz
+|]
+

Let’s make a few points:

+
    +
  • +

    +The syntax is almost identical to normal Hamlet, except URL-interpolation + (@{…}) has been removed. As such: +

    +
      +
    • +

      +No close tags. +

      +
    • +
    • +

      +Whitespace-sensitive. +

      +
    • +
    • +

      +If you want to have whitespace at the end of a line, use a # at the end. At + the beginning, use a backslash. +

      +
    • +
    +
  • +
  • +

    +An xml interpolation will return a list of Nodes. So you still need to + wrap up the output in all the normal Document and root Element + constructs. +

    +
  • +
  • +

    +There is no support for the special .class and #id attribute forms. +

    +
  • +
+

And like normal Hamlet, you can use variable interpolation and control +structures. So a slightly more complex example would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+import Data.Text (Text, pack)
+import Data.Map (empty)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Michael" 26
+    , Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Gavriella" 1
+    ]
+
+main :: IO ()
+main =
+    writeFile def "people.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>Some People
+<body>
+    <h1>Some People
+    $if null people
+        <p>There are no people.
+    $else
+        <dl>
+            $forall person <- people
+                ^{personNodes person}
+|]
+
+personNodes :: Person -> [Node]
+personNodes person = [xml|
+<dt>#{personName person}
+<dd>#{pack $ show $ personAge person}
+|]
+

A few more notes:

+
    +
  • +

    +The caret-interpolation (^{…}) takes a list of nodes, and so can easily + embed other xml-quotations. +

    +
  • +
  • +

    +Unlike Hamlet, hash-interpolations (#{…}) are not polymorphic, and can + only accept Text values. +

    +
  • +
+
+
+

xml2html

+

So far in this chapter, our examples have revolved around XHTML. I’ve done that +so far simply because it is likely to be the most familiar form of XML for most +of our readers. But there’s an ugly side to all this that we must acknowledge: +not all XHTML will be correct HTML. The following discrepancies exist:

+
    +
  • +

    +There are some void tags (e.g., img, br) in HTML which do not need to + have close tags, and in fact are not allowed to. +

    +
  • +
  • +

    +HTML does not understand self-closing tags, so + <script></script> and <script/> mean very different + things. +

    +
  • +
  • +

    +Combining the previous two points: you are free to self-close void tags, + though to a browser it won’t mean anything. +

    +
  • +
  • +

    +In order to avoid quirks mode, you should start your HTML documents with a + DOCTYPE statement. +

    +
  • +
  • +

    +We do not want the XML declaration <?xml …?> at the top of an HTML + page. +

    +
  • +
  • +

    +We do not want any namespaces used in HTML, while XHTML is fully namespaced. +

    +
  • +
  • +

    +The contents of <style> and <script> tags should not be + escaped. +

    +
  • +
+

Fortunately, xml-conduit provides ToHtml instances for Nodes, +Documents, and Elements which respect these discrepancies. So by just +using toHtml, we can get the correct output.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map                        (empty)
+import           Text.Blaze.Html                 (toHtml)
+import           Text.Blaze.Html.Renderer.String (renderHtml)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = putStr $ renderHtml $ toHtml $ Document (Prologue [] Nothing []) root []
+
+root :: Element
+root = Element "html" empty [xml|
+<head>
+    <title>Test
+    <script>if (5 < 6 || 8 > 9) alert("Hello World!");
+    <style>body > h1 { color: red }
+<body>
+    <h1>Hello World!
+|]
+

Outputs: (whitespace added)

+
<!DOCTYPE HTML>
+<html>
+    <head>
+        <title>Test</title>
+        <script>if (5 < 6 || 8 > 9) alert("Hello World!");</script>
+        <style>body > h1 { color: red }</style>
+    </head>
+    <body>
+        <h1>Hello World!</h1>
+    </body>
+</html>
+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/yesod-for-haskellers.html b/public/book-1.2/yesod-for-haskellers.html new file mode 100644 index 00000000..7d7d1aa5 --- /dev/null +++ b/public/book-1.2/yesod-for-haskellers.html @@ -0,0 +1,1225 @@ + Yesod for Haskellers :: Yesod Web Framework Book- Version 1.2 +
+

Yesod for Haskellers

+ + +

The majority of this book is built around giving practical information on how +to get common tasks done, without drilling too much into the details of what’s +going on under the surface. While the book presumes knowledge of Haskell, it +does not follow the typical style of many Haskell libraries introductions. Many +seasoned Haskellers are put off by this hiding of implementation details. The +purpose of this appendix is to address those concerns.

+

In this appendix, we’ll start off from a bare minimum web application, and +build up to more complicated examples, explaining the components and their +types along the way.

+
+

Hello Warp

+

Let’s start off with the most bare minimum application we can think of:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req = return $ responseLBS
+    status200
+    [("Content-Type", "text/plain")]
+    "Hello Warp!"
+

Wait a minute, there’s no Yesod in there! Don’t worry, we’ll get there. +Remember, we’re building from the ground up, and in Yesod, the ground floor in +WAI, the Web Application Interface. WAI sits between a web handler, such as a +web server or a test framework, and a web application. In our case, the +handler is Warp, a high performance web server, and our application is the +app function.

+

What’s this mysterious Application type? It’s a simple synonym for type +Application = Request → IO Response. The Request value contains information +such as the requested path, query string, request headers, request body, and +the IP address of the client. We can use this to do some simple dispatching:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, pathInfo, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req =
+    case pathInfo req of
+        ["foo", "bar"] -> return $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested /foo/bar"
+        _ -> return $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested something else"
+

WAI mandates that the path be split into individual fragments (the stuff +between forward slashes) and converted into text. This allows for easy pattern +matching. If you need the original, unmodified ByteString, you can use +rawPathInfo. For more information on the available fields, please see the WAI +Haddocks.

+

That addresses the request side; what about responses? We’ve already seen +responseLBS, which is a convenient way of creating a response from a lazy +ByteString. That function takes three arguments: the status code, a list of +response headers (as key/value pairs), and the body itself. But responseLBS +is just a convenience wrapper. Under the surface, WAI uses blaze-builder’s +Builder data type to represent the raw bytes. Let’s dig down another level +and use that directly:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (Builder, fromByteString)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseBuilder)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req = return $ responseBuilder
+    status200
+    [("Content-Type", "text/plain")]
+    (fromByteString "Hello from blaze-builder!" :: Builder)
+

This opens up some nice opportunities for efficiently building up response +bodies, since Builder allows for O(1) append operations. We’re also able to +take advantage of blaze-html, which sits on top of blaze-builder. Let’s see our +first HTML application.

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (Application, responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import           Text.Blaze.Html5              (Html, docTypeHtml)
+import qualified Text.Blaze.Html5              as H
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req = return $ responseBuilder
+    status200
+    [("Content-Type", "text/html")] -- yay!
+    (renderHtmlBuilder myPage)
+
+myPage :: Html
+myPage = docTypeHtml $ do
+    H.head $ do
+        H.title "Hello from blaze-html and Warp"
+    H.body $ do
+        H.h1 "Hello from blaze-html and Warp"
+

But there’s a limitation with using a pure Builder value: we need to create +the entire response body before returning the Response value. With lazy +evaluation, that’s not as bad as it sounds, since not all of the body will live in +memory at once. However, if we need to perform some I/O to generate our +response body (such as reading data from a database), we’ll be in trouble.

+

To deal with that situation, WAI uses conduit to represent a streaming response +body. It also allows explicit control of flushing the stream by wrapping values +in the Flush data type. Let’s see how this works.

+
{-# LANGUAGE OverloadedStrings #-}
+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.Conduit                       (Flush (Chunk, Flush),
+                                                     Source, yield)
+import           Data.Monoid                        ((<>))
+import           Network.HTTP.Types                 (status200)
+import           Network.Wai                        (Application,
+                                                     responseSource)
+import           Network.Wai.Handler.Warp           (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req = return $ responseSource
+    status200
+    [("Content-Type", "text/plain")]
+    mySrc
+
+mySrc :: Source IO (Flush Builder)
+mySrc = do
+    yield $ Chunk $ fromByteString "Starting streaming response.\n"
+    yield $ Chunk $ fromByteString "Performing some I/O.\n"
+    yield Flush
+    -- pretend we're performing some I/O
+    lift $ threadDelay 1000000
+    yield $ Chunk $ fromByteString "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        yield $ Chunk $ fromByteString "Got the value: " <>
+                        fromShow i <>
+                        fromByteString "\n"
+

Another common requirement when dealing with a streaming response is safely +allocating a scarce resource- such as a file handle. By safely, I mean +ensuring that the response will be released, even in the case of some +exception. To deal with that, you can use responseSourceBracket:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (fromByteString)
+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, responseSourceBracket)
+import           Network.Wai.Handler.Warp (run)
+import           System.IO                (IOMode (ReadMode), hClose, openFile)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req = responseSourceBracket
+    (openFile "index.html" ReadMode)
+    hClose
+    $ \handle -> return
+        ( status200
+        , [("Content-Type", "text/html")]
+        , sourceHandle handle $= CL.map (Chunk . fromByteString)
+        )
+

But in the case of serving files, it’s more efficient to use responseFile, +which can use the sendfile system call to avoid unnecessary buffer copies:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseFile)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req = return $ responseFile
+    status200
+    [("Content-Type", "text/html")]
+    "index.html"
+    Nothing -- means "serve whole file"
+            -- you can also serve specific ranges in the file
+

There are many aspects of WAI we haven’t covered here. One important topic is WAI middlewares, which we’ll cover towards the end of this chapter. We also haven’t inspected request bodies at all. But for the purposes of understanding Yesod, we’ve covered enough for the moment.

+
+
+

What about Yesod?

+

In all our excitement about WAI and Warp, we still haven’t seen anything about Yesod! Since we just learnt all about WAI, our first question should be: how does Yesod interact with WAI. The answer to that is with one very important function:

+
toWaiApp :: YesodDispatch site => site -> IO Application
+ +

This function takes some site value, which must be an instance of +YesodDispatch, and creates an Application. This function lives in the IO +monad, since it will likely perform actions like allocating a shared logging +buffer. The more interesting question is what this site value is all about.

+

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. +

    +
  • +
  • +

    +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 appendix, we’re going to create our first +Yesod application the hard way, by writing everything manually. We’ll +progressively add more convenience helpers on top as we go along.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import qualified Text.Blaze.Html5              as H
+import           Yesod.Core                    (Html, RenderRoute (..), Yesod,
+                                                YesodDispatch (..), toWaiApp)
+import           Yesod.Core.Types              (YesodRunnerEnv (..))
+
+-- | Our foundatation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+instance YesodDispatch App where
+    yesodDispatch (YesodRunnerEnv _logger site _sessionBackend) _req =
+        return $ responseBuilder
+            status200
+            [("Content-Type", "text/html")]
+            (renderHtmlBuilder $ welcomeMessage site)
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

OK, we’ve added quite a few new pieces here, let’s attack them one at a time. +The first thing we’ve done is created a new datatype, App. This is commonly +used as the foundation data type name for each application, though you’re free +to use whatever name you want. We’ve added one field to this datatype, +welcomeMessage, which will hold the content for our homepage.

+

Next we declare our Yesod instance. We just use the default values for all of +the methods for this example. More interesting is the RenderRoute typeclass. +This is the heart of type-safe URLs. We create an associated data type for +App which lists all of our app’s accepted routes. In this case, we have just +one: the homepage, which we call HomeR. It’s yet another Yesod naming +convention to append R to all of the route data constructors.

+

We also need to create a renderRoute method, which converts each type-safe +route value into a tuple of path pieces and query string parameters. We’ll get +to more interesting examples later, but for now, our homepage has an empty list +for both of those.

+

YesodDispatch determines how our application behaves. It has one method, +yesodDispatch, of type:

+
yesodDispatch :: YesodRunnerEnv site -> Application
+

YesodRunnerEnv provides three 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.

+

The return type of yesodDispatch is Application from WAI. But as we saw +earlier, Application is simply a function from Request to IO Response. So +our implementation of yesodDispatch is able to use everything we learned +about WAI above. Notice also how we accessed the welcomeMessage from our +foundation data type.

+

Finally, we have the main function. The App value is easy to create and, as +you can see, you could just as easily have performed some I/O to acquire the +welcome message. We use toWaiApp to obtain a WAI application, and then pass +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 appendix.)

+
+
+

The HandlerT monad transformer

+

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.

+

There are many common things you’d want to do when handling a single request, +e.g.:

+
    +
  • +

    +Return some HTML. +

    +
  • +
  • +

    +Redirect to a different URL. +

    +
  • +
  • +

    +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:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.Wai              (pathInfo)
+import           Network.Wai.Handler.Warp (run)
+import qualified Text.Blaze.Html5         as H
+import           Yesod.Core               (HandlerT, Html, RenderRoute (..),
+                                           Yesod, YesodDispatch (..), getYesod,
+                                           notFound, toWaiApp, yesodRunner)
+
+-- | Our foundatation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO Html
+getHomeR = do
+    site <- getYesod
+    return $ welcomeMessage site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

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 +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.

+

Our function body is short. We use the getYesod function to get the +foundation data type value, and then return the welcomeMessage field. We’ll +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 Applications. Let’s look at its type signature:

+
yesodRunner :: (ToTypedContent res, Yesod site)
+            => HandlerT site IO res
+            -> YesodRunnerEnv site
+            -> Maybe (Route site)
+            -> Application
+

We’re already familiar with YesodRunnerEnv from our previous example. As you +can see in our call to yesodRunner above, we pass that value in unchanged. +The Maybe (Route site) is a bit interesting, and gives us more insight into +how type-safe URLs work. Until now, we only saw the rendering side of these +URLs. But just as important is the parsing side: converting a requested path +into a route value. In our example, this code is just a few lines, and we store +the result in +maybeRoute.

+ +

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 +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 +template. That template can be overridden in the Yesod typeclass; out of the +box, it’s just a boring HTML page.

+

This almost all makes sense, except for one issue: what’s that ToTypedContent +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 will examine in a moment) +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, +and sending the response body.

+
+

(To)Content, (To)TypedContent

+

At the very core of Yesod’s content system are the following types:

+
data Content = ContentBuilder !Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+
+type ContentType = ByteString
+data TypedContent = TypedContent !ContentType !Content
+

Content should remind you a bit of the WAI response types. ContentBuilder +is similar to responseBuilder, ContentSource is like responseSource, and +ContentFile is like responseFile. Unlike their WAI counterparts, none of +these constructors contain information on the status code or response headers; +that’s handled orthogonally in Yesod.

+

The one completely new constructor is ContentDontEvaluate. By default, when +you create a response body in Yesod, Yesod fully evaluates the body before +generating the response. The reason for this is to ensure that there are no +impure exceptions in your value. Yesod wants to make sure to catch any such +exceptions before starting to send your response so that, if there is an +exception, Yesod can generate a proper 500 internal server error response +instead of simply dying in the middle of sending a non-error response. However, +performing this evaluation can cause more memory usage. Therefore, Yesod +provides a means of opting out of this protection.

+

TypedContent is then a minor addition to Content: it includes the +ContentType as well. Together with a convention that an application returns a +200 OK status unless otherwise specified, we have everything we need from the +TypedContent type to create a response.

+

Yesod could have taken the approach of requiring users to always return +TypedContent from a handler function, but that would have required manually +converting to that type. Instead, Yesod uses a pair of typeclasses for this, +appropriately named ToContent and ToTypedContent. They have exactly the +definitions you’d expect:

+
class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

And Yesod provides instances for many common data types, including Text, +Html, and aeson’s Value type (containing JSON data). That’s how the +getHomeR function was able to return Html: Yesod knows how to convert it to +TypedContent, and from there it can be converted into a WAI response.

+
+
+

HasContentType and representations

+

This typeclass approach allows for one other nice abstraction. For many types, the type system itself lets us know what the content-type for the content should be. For example, Html will always be served with a text/html content-type.

+ +

Some requests to a web application can be displayed with various representation. For example, a request for tabular data could be served with:

+
    +
  • +

    +An HTML table. +

    +
  • +
  • +

    +A CSV file. +

    +
  • +
  • +

    +XML. +

    +
  • +
  • +

    +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 +selectRep/provideRep function combo to provide multiple representations, +and have the framework automatically choose the appropriate one based on the +client headers.

+

The last missing piece to make this all work is the HasContentType typeclass:

+
class ToTypedContent a => HasContentType a where
+    getContentType :: Monad m => m a -> ContentType
+

The parameter m a is just a poor man’s Proxy type. There are instances for +this typeclass for most data types supported by ToTypedContent. Below is our +example from above, tweaked just a bit to provide multiple representations of +the data:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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 (..),
+                                           TypedContent, Value, Yesod,
+                                           YesodDispatch (..), getYesod,
+                                           notFound, object, provideRep,
+                                           selectRep, toWaiApp, yesodRunner,
+                                           (.=))
+
+-- | Our foundatation datatype.
+data App = App
+    { welcomeMessageHtml :: !Html
+    , welcomeMessageText :: !Text
+    , welcomeMessageJson :: !Value
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = do
+    site <- getYesod
+    selectRep $ do
+        provideRep $ return $ welcomeMessageHtml site
+        provideRep $ return $ welcomeMessageText site
+        provideRep $ return $ welcomeMessageJson site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req
+
+main :: IO ()
+main = do
+    waiApp <- toWaiApp App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+    run 3000 waiApp
+
+
+

Convenience warp function

+

And one minor convenience you’ll see quite a bit in the Yesod world. It’s very +common to call toWaiApp to create a WAI Application, and then pass that to +Warp’s run function. So Yesod provides a convenience warp wrapper function. +We can replace our previous main function with the following:

+
main :: IO ()
+main =
+    warp 3000 App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+

There’s also a warpEnv function which reads the port number from the PORT +environment variable, which is useful for working with platforms such as FP +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.

+

In addition to that standard functionality, the following are some common +categories of functions. The only requirement Yesod places on your handler +functions is that, ultimately, they return a type which is an instance of +ToTypedContent.

+

This section is just a short overview of functionality. For more information, +you should either look through the Haddocks, or read the rest of this book.

+
+

Getting request parameters

+

There are a few pieces of information provided by the client in a request:

+
    +
  • +

    +The requested path. This is usually handled by Yesod’s routing framework, and is not directly queried in a handler function. +

    +
  • +
  • +

    +Query string parameters. This can be queried using lookupGetParam. +

    +
  • +
  • +

    +Request bodies. In the case of URL encoded and multipart bodies, you can use lookupPostParam to get the request parameter. For multipart bodies, there’s also lookupFile for file parameters. +

    +
  • +
  • +

    +Request headers can be queried via lookupHeader. (And response headers can be set with addHeader.) +

    +
  • +
  • +

    +Yesod parses cookies for you automatically, and they can be queried using lookupCookie. (Cookies can be set via the setCookie function.) +

    +
  • +
  • +

    +Finally, Yesod provides a user session framework, where data can be set in a cryptographically secure session and associated with each user. This can be queried and set using the functions lookupSession, setSession and deleteSession. +

    +
  • +
+

While you can use these functions directly for such purposes as processing +forms, you usually will want to use the yesod-form library, which provides a +higher level form abstraction based on applicative functors.

+
+
+

Short circuiting

+

In some cases, you’ll want to short circuit the handling of a request. Reasons +for doing this would be:

+
    +
  • +

    +Send an HTTP redirect, via the redirect function. This will default to using the 303 status code. You can use redirectWith to get more control over this. +

    +
  • +
  • +

    +Return a 404 not found with notFound, or a 405 bad method via badMethod. +

    +
  • +
  • +

    +Indicate some error in the request via notAuthenticated, permissionDenied, or invalidArgs. +

    +
  • +
  • +

    +Send a special response, such as with sendFile or sendResponseStatus (to override the status 200 response code) +

    +
  • +
  • +

    +sendWaiResponse to drop down a level of abstraction, bypass some Yesod abstractions, and use WAI itself. +

    +
  • +
+
+
+

Streaming

+

So far, the examples of ToTypedContent instances I gave all involved +non-streaming responses. Html, Text, and Value all get converted into a +ContentBuilder constructor. As such, they cannot interleave I/O with sending +data to the user. What happens if we want to perform such interleaving?

+

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 +Builder. Yesod also provides a number of convenience functions for creating +that Source, such as sendChunk, sendChunkBS, and sendChunkText.

+

Here’s an example, which just converts our initial responseSource example +from WAI to Yesod.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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,
+                                                     YesodDispatch (..), liftIO,
+                                                     notFound, respondSource,
+                                                     sendChunk, sendChunkBS,
+                                                     sendChunkText, sendFlush,
+                                                     warp, yesodRunner)
+
+-- | Our foundatation datatype.
+data App = App
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = respondSource "text/plain" $ do
+    sendChunkBS "Starting streaming response.\n"
+    sendChunkText "Performing some I/O.\n"
+    sendFlush
+    -- pretend we're performing some I/O
+    liftIO $ threadDelay 1000000
+    sendChunkBS "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        sendChunk $ fromByteString "Got the value: " <>
+                    fromShow i <>
+                    fromByteString "\n"
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req
+
+main :: IO ()
+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, +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 +Fibonacci number. And if you visit /, it will automatically redirect you to +/fib/1.

+

In the Yesod world, the first question to ask is: how do we model our route +data type? This is pretty straight-forward: data Route App = HomeR | FibR +Int. The question is: how do we want to define our RenderRoute instance? We +need to convert the Int to a Text. What function should we use?

+

Before you answer that, realize that we’ll also need to be able to parse back a Text into an Int for dispatch purposes. So we need to make sure that we have a pair of functions with the property fromText . toText == Just. Show/Read could be a candidate for this, except that:

+
    +
  1. +

    +We’d be required to convert through String. +

    +
  2. +
  3. +

    +The Show/Read instances for Text and String both involve extra escaping, which we don’t want to incur. +

    +
  4. +
+

Instead, the approach taken by Yesod is the path-pieces package, and in +particular the PathPiece typeclass, defined as:

+
class PathPiece s where
+    fromPathPiece :: Text -> Maybe s
+    toPathPiece   :: s    -> Text
+

Using this typeclass, we can write parse and render functions for our route datatype:

+
instance RenderRoute App where
+    data Route App = HomeR | FibR Int
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ([], [])
+    renderRoute (FibR i) = (["fib", toPathPiece i], [])
+
+parseRoute' [] = Just HomeR
+parseRoute' ["fib", i] = FibR <$> fromPathPiece i
+parseRoute' _ = Nothing
+

And then we can write our YesodDispatch typeclass instance:

+
instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req =
+        let maybeRoute = parseRoute' (pathInfo req)
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+                    Just (FibR i) -> getFibR i
+         in yesodRunner handler yesodRunnerEnv maybeRoute req
+
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR i = return $ show $ fibs !! i
+

Notice our call to redirect in getHomeR. We’re able to use the route +datatype as the parameter to redirect, and Yesod takes advantage of our +renderRoute function to create a textual link.

+
+
+

Routing with Template Haskell

+

Now let’s suppose we want to add a new route to our previous application. We’d +have to make the following changes:

+
    +
  1. +

    +Modify the Route datatype itself. +

    +
  2. +
  3. +

    +Add a clause to renderRoute. +

    +
  4. +
  5. +

    +Add a clause to parseRoute', and make sure it corresponds correctly to renderRoute. +

    +
  6. +
  7. +

    +Add a clause to the case statement in yesodDispatch to call our handler function. +

    +
  8. +
  9. +

    +Write our handler function. +

    +
  10. +
+

That’s a lot of changes! And lots of manual, boilerplate changes means lots of +potential for mistakes. Some of the mistakes can be caught by the compiler if +you turn on warnings (forgetting to add a clause in renderRoute or a match in +yesodDispatch's case statement), but others cannot (ensuring that +renderRoute and parseRoute have the same logic, or adding the parseRoute +clause).

+

This is where Template Haskell comes into the Yesod world. Instead of dealing +with all of these changes manually, Yesod declares a high level routing syntax. +This syntax lets you specify your route syntax, dynamic parameters, constructor +names, and accepted request methods, and automatically generates parse, render, +and dispatch functions.

+

To get an idea of how much manual coding this saves, have a look at our +previous example converted to the Template Haskell version:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod.Core (RenderRoute (..), Yesod, mkYesod, parseRoutes,
+                             redirect, warp)
+
+-- | Our foundatation datatype.
+data App = App
+
+instance Yesod App
+
+mkYesod "App" [parseRoutes|
+/         HomeR GET
+/fib/#Int FibR  GET
+|]
+
+getHomeR :: Handler ()
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> Handler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 App
+

What’s wonderful about this is, as the developer, you can now focus on the +important part of your application, and not get involved in the details of +writing parsers and renderers. There are of course some downsides to the usage +of Template Haskell:

+
    +
  • +

    +Compile times are a bit slower. +

    +
  • +
  • +

    +The details of what’s going on behind the scenes aren’t easily apparent. (Though you can use cabal haddock to see what identifiers have been generated for you.) +

    +
  • +
  • +

    +You don’t have as much fine-grained control. For example, in the Yesod route syntax, each dynamic parameter has to be a separate field in the route constructor, as opposed to bundling fields together. This is a conscious trade-off in Yesod between flexibility and complexity. +

    +
  • +
+

This usage of Template Haskell is likely the most controversial decision in +Yesod. I personally think the benefits definitely justify its usage. But if +you’d rather avoid Template Haskell, you’re free to do so. Every example so far +in this appendix has done so, and you can follow those techniques. We also have +another, simpler approach in the Yesod world: LiteApp.

+
+

LiteApp

+

LiteApp allows you to throw away type safe URLs and Template Haskell. It uses +a simple routing DSL in pure Haskell. Once again, as a simple comparison, let’s +rewrite our Fibonacci example to use it.

+
import           Data.Text  (pack)
+import           Yesod.Core (LiteHandler, dispatchTo, dispatchTo, liteApp,
+                             onStatic, redirect, warp, withDynamic)
+
+getHomeR :: LiteHandler ()
+getHomeR = redirect "/fib/1"
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> LiteHandler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "fib") $ withDynamic $ \i -> dispatchTo (getFibR i)
+

There you go, a simple Yesod app without any language extensions at all! +However, even this application still demonstrates some type safety. Yesod will +use fromPathPiece to convert the parameter for getFibR from Text to an +Int, so any invalid parameter will be got by Yesod itself. It’s just one less +piece of checking that you have to perform.

+
+
+
+

Shakespeare

+

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.

+

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.

+
import           Data.Text  (pack)
+import           Yesod.Core
+
+getHomeR :: LiteHandler TypedContent
+getHomeR = return $ TypedContent typeHtml $ toContent
+    "<html><head><title>Hi There!</title>\
+    \<link rel='stylesheet' href='/style.css'>\
+    \<script src='/script.js'></script></head>\
+    \<body><h1>Hello World!</h1></body></html>"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

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 +content as Strings, convert them to Content using toContent, then wrap +them with a TypedContent constructor to give them the appropriate +content-type headers.

+

But as usual, we can do better. Dealing with Strings is not very efficient, +and it’s tedious to have to manually put in the content type all the time. But +we already know the solution to those problems: use the Html datatype from +blaze-html. Let’s convert our getHomeR function to use it:

+
import           Data.Text                   (pack)
+import           Text.Blaze.Html5            (toValue, (!))
+import qualified Text.Blaze.Html5            as H
+import qualified Text.Blaze.Html5.Attributes as A
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = return $ H.docTypeHtml $ do
+    H.head $ do
+        H.title $ toHtml "Hi There!"
+        H.link ! A.rel (toValue "stylesheet") ! A.href (toValue "/style.css")
+        H.script ! A.src (toValue "/script.js") $ return ()
+    H.body $ do
+        H.h1 $ toHtml "Hello World!"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

Ahh, far nicer. blaze-html provides a convenient combinator library, and will +execute far faster in most cases than whatever String concatenation you might +attempt.

+

If you’re happy with blaze-html combinators, by all means use them. However, +many people like to use a more specialized templating language. Yesod’s +standard provider for this is the Shakespearean languages: Hamlet, Lucius, and +Julius. You are by all means welcome to use a different system if so desired, +the only requirement is that you can a Content value from the template.

+

Since Shakespearean templates on compile-time checked, their usage requires +either quasiquotation or Template Haskell. We’ll go for the former approach +here. Please see the Shakespeare chapter in the book for more information.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Data.Text   (Text, pack)
+import           Text.Julius (Javascript)
+import           Text.Lucius (Css)
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = giveUrlRenderer $
+    [hamlet|
+        $doctype 5
+        <html>
+            <head>
+                <title>Hi There!
+                <link rel=stylesheet href=/style.css>
+                <script src=/script.js>
+            <body>
+                <h1>Hello World!
+    |]
+
+getStyleR :: LiteHandler Css
+getStyleR = giveUrlRenderer [lucius|h1 { color: red }|]
+
+getScriptR :: LiteHandler Javascript
+getScriptR = giveUrlRenderer [julius|alert('Yay, Javascript works too!');|]
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+
+

URL rendering function

+

Likely the most confusing part of this is the giveUrlRenderer 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 +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.

+

The recommended approach instead would be to use our type-safe URL datatype in +our template instead of including explicit strings. As mentioned above, +LiteApp doesn’t provide any meaningful type-safe URLs, so we don’t have that +option here. But if you use the Template Haskell generators, you get type-safe +URLs for free.

+

In any event, the Shakespearean templates all expect to receive a function to +handle the rendering of a type-safe URL. Since we don’t actually use any +type-safe URLs, just about any function would work here (the function will be +ignored entirely), but giveUrlRenderer is a convenient way of doing this.

+

As we’ll see next, giveUrlRenderer isn’t really needed most of the time, +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 +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 +using them.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = defaultLayout $ do
+    setTitle $ toHtml "Hi There!"
+    [whamlet|<h1>Hello World!|]
+    toWidget [lucius|h1 { color: red }|]
+    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 +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 +temporary static files and linking to them).

+

Widgets also have another advantage. The defaultLayout function is a member +of the Yesod typeclass, and can be modified to provide a customized +look-and-feel for your website. Many built-in pieces of Yesod, such as error +messages, take advantage of the widget system, so by using widgets, you get a +consistent feel throughout your site.

+
+
+

Forms

+

FIXME

+
+
+

Persistent

+

FIXME

+
+
+

WAI middlewares

+

FIXME

+

These are functions of type type Middleware = Application → Application, and they do some kind of arbitrary transformation to an application, such as enabling GZIP compression or logging requests.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/yesod-typeclass.html b/public/book-1.2/yesod-typeclass.html new file mode 100644 index 00000000..080ce4ca --- /dev/null +++ b/public/book-1.2/yesod-typeclass.html @@ -0,0 +1,670 @@ + Yesod Typeclass :: Yesod Web Framework Book- Version 1.2 +
+

Yesod Typeclass

+ + +

Every one of our Yesod applications requires an instance of the Yesod +typeclass. So far, we’ve only seen defaultLayout. In this chapter, we’ll +explore the meaning of many of the methods of the Yesod typeclass.

+

The Yesod typeclass gives us a central place for defining settings for our +application. Everything has a default definition which is often the +right thing. But in order to build a powerful, customized application, you’ll +usually end up wanting to override at least a few of these methods.

+ +
+

Rendering and Parsing URLs

+

We’ve already mentioned how Yesod is able to automatically render type-safe +URLs into a textual URL that can be inserted into an HTML page. Let’s say we +have a route definition that looks like:

+
mkYesod "MyApp" [parseRoutes|
+/some/path SomePathR GET
+]
+

If we place SomePathR into a hamlet template, how does Yesod render it? Yesod +always tries to construct absolute URLs. This is especially useful once we +start creating XML sitemaps and Atom feeds, or sending emails. But in order to +construct an absolute URL, we need to know the domain name of the application.

+

You might think we could get that information from the user’s request, but we +still need to deal with ports. And even if we get the port number from the +request, are we using HTTP or HTTPS? And even if you know that, such an +approach would mean that, depending on how the user submitted a request would +generate different URLs. For example, we would generate different URLs +depending if the user connected to "example.com" or "www.example.com". For +Search Engine Optimization, we want to be able to consolidate on a single +canonical URL.

+

And finally, Yesod doesn’t make any assumption about where you host your +application. For example, I may have a mostly static site +(http://static.example.com/), but I’d like to stick a Yesod-powered Wiki at +/wiki/. There is no reliable way for an application to determine what subpath +it is being hosted from. So instead of doing all of this guesswork, Yesod needs +you to tell it the application root.

+

Using the wiki example, you would write your Yesod instance as:

+
instance Yesod MyWiki where
+    approot = ApprootStatic "http://static.example.com/wiki"
+

Notice that there is no trailing slash there. Next, when Yesod wants to +construct a URL for SomePathR, it determines that the relative path for +SomePathR is /some/path, appends that to your approot and creates +http://static.example.com/wiki/some/path.

+

The default value of approot is ApprootRelative, which essentially means +“don’t add any prefix.” In that case, the generated URL would be +/some/path. This works fine for the common case of a link within your +application, and your application being hosted at the root of your domain. But +if you have any use cases which demand absolute URLs (such as sending an +email), it’s best to use ApprootStatic.

+

In addition to the ApprootStatic constructor demonstrated above, you can also +use the ApprootMaster and ApprootRequest constructors. The former allows +you to determine the approot from the foundation value, which would let you +load up the approot from a config file, for instance. The latter allows you to +additionally use the request value to determine the approot; using this, you +could for example provide a different domain name depending on how the user +requested the site in the first place.

+

The scaffolded site uses ApprootMaster by default, and pulls your approot +from a config file on launch. Additionally, it load different settings for +developing, testing, staging, and production builds, so you can easily test on +one domain- like localhost- and serve from a different domain. You can modify +these values from the config file.

+
+

joinPath

+

In order to convert a type-safe URL into a text value, Yesod uses two helper +functions. The first is the renderRoute method of the RenderRoute +typeclass. Every type-safe URL is an instance of this typeclass. renderRoute +converts a value into a list of path pieces. For example, our SomePathR from +above would be converted into ["some", "path"].

+ +

The other function is the joinPath method of the Yesod typeclass. This function takes four arguments:

+
    +
  • +

    +The foundation value +

    +
  • +
  • +

    +The application root +

    +
  • +
  • +

    +A list of path segments +

    +
  • +
  • +

    +A list of query string parameters +

    +
  • +
+

It returns a textual URL. The default implementation does the “right thing”: +it separates the path pieces by forward slashes, prepends the application root, +and appends the query string.

+

If you are happy with default URL rendering, you should not need to modify it. +However, if you want to modify URL rendering to do things like append a +trailing slash, this would be the place to do it.

+
+
+

cleanPath

+

The flip side of joinPath is cleanPath. Let’s look at how it gets used in +the dispatch process:

+
    +
  1. +

    +The path info requested by the user is split into a series of path pieces. +

    +
  2. +
  3. +

    +We pass the path pieces to the cleanPath function. +

    +
  4. +
  5. +

    +If cleanPath indicates a redirect (a Left response), then a 301 response +is sent to the client. This is used to force canonical URLs (eg, remove extra +slashes). +

    +
  6. +
  7. +

    +Otherwise, we try to dispatch using the response from cleanPath (a +Right). If this works, we return a response. Otherwise, we return a 404. +

    +
  8. +
+

This combination allows subsites to retain full control of how their URLs +appear, yet allows master sites to have modified URLs. As a simple example, +let’s see how we could modify Yesod to always produce trailing slashes on URLs:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# 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)
+import           Yesod
+
+data Slash = Slash
+
+mkYesod "Slash" [parseRoutes|
+/ RootR GET
+/foo FooR GET
+|]
+
+instance Yesod Slash where
+    joinPath _ ar pieces' qs' =
+        fromText ar `mappend` encodePath pieces qs
+      where
+        qs = map (TE.encodeUtf8 *** go) qs'
+        go "" = Nothing
+        go x = Just $ TE.encodeUtf8 x
+        pieces = pieces' ++ [""]
+
+    -- We want to keep canonical URLs. Therefore, if the URL is missing a
+    -- trailing slash, redirect. But the empty set of pieces always stays the
+    -- same.
+    cleanPath _ [] = Right []
+    cleanPath _ s
+        | dropWhile (not . T.null) s == [""] = -- the only empty string is the last one
+            Right $ init s
+        -- Since joinPath will append the missing trailing slash, we simply
+        -- remove empty pieces.
+        | otherwise = Left $ filter (not . T.null) s
+
+getRootR :: Handler Html
+getRootR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{RootR}>RootR
+        <p>
+            <a href=@{FooR}>FooR
+    |]
+
+getFooR :: Handler Html
+getFooR = getRootR
+
+main :: IO ()
+main = warp 3000 Slash
+

First, let’s look at our joinPath implementation. This is copied almost +verbatim from the default Yesod implementation, with one difference: we append +an extra empty string to the end. When dealing with path pieces, an empty +string will append another slash. So adding an extra empty string will force a +trailing slash.

+

cleanPath is a little bit trickier. First, we check for the empty path like +before, and if so pass it through as-is. We use Right to indicate that a +redirect is not necessary. The next clause is actually checking for two +different possible URL issues:

+
    +
  • +

    +There is a double slash, which would show up as an empty string in the middle + of our paths. +

    +
  • +
  • +

    +There is a missing trailing slash, which would show up as the last piece not + being an empty string. +

    +
  • +
+

Assuming neither of those conditions hold, then only the last piece is empty, +and we should dispatch based on all but the last piece. However, if this is not +the case, we want to redirect to a canonical URL. In this case, we strip out +all empty pieces and do not bother appending a trailing slash, since joinPath +will do that for us.

+
+
+
+

defaultLayout

+

Most websites like to apply some general template to all of their pages. +defaultLayout is the recommended approach for this. While you could just as +easily define your own function and call that instead, when you override +defaultLayout all of the Yesod-generated pages (error pages, authentication +pages) automatically get this style.

+

Overriding is very straight-forward: we use widgetToPageContent to convert a +Widget to a title, head tags and body tags, and then use giveUrlRenderer to +convert a Hamlet template into an Html value. We can even add extra widget +components, like a Lucius template, from within defaultLayout. For more +information, see the previous chapter on widgets.

+

If you are using the scaffolded site, you can modify the files +templates/default-layout.hamlet and +templates/default-layout-wrapper.hamlet.

+
+

getMessage

+

Even though we haven’t covered sessions yet, I’d like to mention getMessage +here. A common pattern in web development is setting a message in one handler +and displaying it in another. For example, if a user POSTs a form, you may +want to redirect him/her to another page along with a “Form submission +complete” message. This is commonly known as +Post/Redirect/Get.

+

To facilitate this, Yesod comes built in with a pair of functions: setMessage +sets a message in the user session, and getMessage retrieves the message (and +clears it, so it doesn’t appear a second time). It’s recommended that you put +the result of getMessage into your defaultLayout. For example:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+import Data.Time (getCurrentTime)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    defaultLayout contents = do
+        PageContent title headTags bodyTags <- widgetToPageContent contents
+        mmsg <- getMessage
+        giveUrlRenderer [hamlet|
+            $doctype 5
+
+            <html>
+                <head>
+                    <title>#{title}
+                    ^{headTags}
+                <body>
+                    $maybe msg <- mmsg
+                        <div #message>#{msg}
+                    ^{bodyTags}
+        |]
+
+getHomeR :: Handler Html
+getHomeR = do
+    now <- liftIO getCurrentTime
+    setMessage $ toHtml $ "You previously visited at: " ++ show now
+    defaultLayout [whamlet|<p>Try refreshing|]
+
+main :: IO ()
+main = warp 3000 App
+

We’ll cover getMessage/setMessage in more detail when we discuss sessions.

+
+
+
+

Custom error pages

+

One of the marks of a professional web site is a properly designed error page. +Yesod gets you a long way there by automatically using your defaultLayout for +displaying error pages. But sometimes, you’ll want to go even further. For +this, you’ll want to override the errorHandler method:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/error ErrorR GET
+/not-found NotFoundR GET
+|]
+
+instance Yesod App where
+    errorHandler NotFound = fmap toTypedContent $ defaultLayout $ do
+        setTitle "Request page not located"
+        toWidget [hamlet|
+<h1>Not Found
+<p>We apologize for the inconvenience, but the requested page could not be located.
+|]
+    errorHandler other = defaultErrorHandler other
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{ErrorR}>Internal server error
+            <a href=@{NotFoundR}>Not found
+    |]
+
+getErrorR :: Handler ()
+getErrorR = error "This is an error"
+
+getNotFoundR :: Handler ()
+getNotFoundR = notFound
+
+main :: IO ()
+main = warp 3000 App
+

Here we specify a custom 404 error page. We can also use the +defaultErrorHandler when we don’t want to write a custom handler for each +error type. Due to type constraints, we need to start off our methods with +fmap toTypedContent, but otherwise you can write a typical handler function. +(We’ll learn more about TypedContent in the next chapter.)

+

In fact, you could even use special responses like redirects:

+
    errorHandler NotFound = redirect HomeR
+    errorHandler other = defaultErrorHandler other
+ +
+
+

External CSS and Javascript

+ +

One of the most powerful, and most intimidating, methods in the Yesod typeclass +is addStaticContent. Remember that a Widget consists of multiple components, +including CSS and Javascript. How exactly does that CSS/JS arrive in the user’s +browser? By default, they are served in the <head> of the page, inside +<style> and <script> tags, respectively.

+

That might be simple, but it’s far from efficient. Every page load will now +require loading up the CSS/JS from scratch, even if nothing changed! What we +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 +(text/css or text/javascript) and the content itself. It will then return +one of three possible results:

+
+
+Nothing +
+

+No static file saving occurred; embed this content directly in the +HTML. This is the default behavior. +

+
+
+Just (Left Text) +
+

+This content was saved in an external file, and use the +given textual link to refer to it. +

+
+
+Just (Right (Route a, Query)) +
+

+Same, but now use a type-safe URL along with +some query string parameters. +

+
+
+

The Left result is useful if you want to store your static files on an +external server, such as a CDN or memory-backed server. The Right result is +more commonly used, and ties in very well with the static subsite. This is the +recommended approach for most applications, and is provided by the scaffolded +site by default.

+ +

The scaffolded addStaticContent does a number of intelligent things to help +you out:

+
    +
  • +

    +It automatically minifies your Javascript using the hjsmin package. +

    +
  • +
  • +

    +It names the output files based on a hash of the file contents. This means + you can set your cache headers to far in the future without fears of stale + content. +

    +
  • +
  • +

    +Also, since filenames are based on hashes, you can be guaranteed that a file + doesn’t need to be written if a file with the same name already exists. The + scaffold code automatically checks for the existence of that file, and avoids + the costly disk I/O of a write if it’s not necessary. +

    +
  • +
+
+
+

Smarter Static Files

+

Google recommends an important optimization: +serve +static files from a separate domain. The advantage to this approach is that +cookies set on your main domain are not sent when retrieving static files, thus +saving on a bit of bandwidth.

+

To facilitate this, we have the urlRenderOverride method. This method +intercepts the normal URL rendering and sets a special value for some routes. +For example, the scaffolding defines this method as:

+
urlRenderOverride y (StaticR s) =
+    Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s
+
+urlRenderOverride _ _ = Nothing
+

This means that static routes are served from a special static root, which you +can configure to be a different domain. This is a great example of the power +and flexibility of type-safe URLs: with a single line of code you’re able to +change the rendering of static routes throughout all of your handlers.

+
+
+

Authentication/Authorization

+

For simple applications, checking permissions inside each handler function can +be a simple, convenient approach. However, it doesn’t scale well. Eventually, +you’re going to want to have a more declarative approach. Many systems out +there define ACLs, special config files, and a lot of other hocus-pocus. In +Yesod, it’s just plain old Haskell. There are three methods involved:

+
+
+isWriteRequest +
+

+Determine if the current request is a "read" or "write" operations. By default, Yesod follows RESTful principles, and assumes GET, HEAD, OPTIONS, and TRACE requests are read-only, while all others are can write. +

+
+
+isAuthorized +
+

+Takes a route (i.e., type-safe URL) and a boolean indicating whether or not the request is a write request. It returns an AuthResult, which can have one of three values: +

+
    +
  • +

    +Authorized +

    +
  • +
  • +

    +AuthenticationRequired +

    +
  • +
  • +

    +Unauthorized +

    +
  • +
+
+
+

By default, it returns Authorized for all requests.

+
+
+authRoute +
+

+If isAuthorized returns AuthenticationRequired, then redirect +to the given route. If no route is provided (the default), return a 401 +“authentication required” message. +

+
+
+

These methods tie in nicely with the yesod-auth package, which is used by the +scaffolded site to provide a number of authentication options, such as OpenID, +Mozilla Persona, email, username and Twitter. We’ll cover more concrete +examples in the auth chapter.

+
+
+

Some Simple Settings

+

Not everything in the Yesod typeclass is complicated. Some methods are simple +functions. Let’s just go through the list:

+
+
+maximumContentLength +
+

+To prevent Denial of Service (DoS) attacks, Yesod will +limit the size of request bodies. Some of the time, you’ll want to bump that +limit for some routes (e.g., a file upload page). This is where you’d do that. +

+
+
+fileUpload +
+

+Determines how uploaded files and treated, based on the size of +the request. The two most common approaches are saving the files in memory, or +streaming to temporary files. By default, small requests are kept in memory and +large ones are stored to disk. +

+
+
+shouldLog +
+

+Determines if a given log message (with associated source and +level) should be sent to the log. This allows you to put lots of debugging +information into your app, but only turn it on as necessary. +

+
+
+

For the most up-to-date information, please see the Haddock API documentation +for the Yesod typeclass.

+
+
+

Summary

+

The Yesod typeclass has a number of overrideable methods that allow you to +configure your application. They are all optional, and provide sensible +defaults. By using built-in Yesod constructs like defaultLayout and +getMessage, you’ll get a consistent look-and-feel throughout your site, +including pages automatically generated by Yesod such as error pages and +authentication.

+

We haven’t covered all the methods in the Yesod typeclass in this chapter. For +a full listing of methods available, you should consult the Haddock +documentation.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.2/yesods-monads.html b/public/book-1.2/yesods-monads.html new file mode 100644 index 00000000..5d93c504 --- /dev/null +++ b/public/book-1.2/yesods-monads.html @@ -0,0 +1,643 @@ + Yesod’s Monads :: Yesod Web Framework Book- Version 1.2 +
+

Yesod’s Monads

+ + +

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 +Model-View-Controller (MVC) terms, we could consider YesodDB to be the model, +Widget to be the view, and Handler to be the controller.

+

So far, we’ve presented some very straight-forward ways to use these monads: +your main handler will run in Handler, using runDB to execute a YesodDB +query, and defaultLayout to return a Widget, which in turn was created by +calls to toWidget.

+

However, if we have a deeper understanding of these types, we can achieve some +fancier results.

+
+

Monad Transformers

+
+ +Shrek- more or less + +

Monads are like onions. Monads are not like cakes.

+
+

Before we get into the heart of Yesod’s monads, we need to understand a bit +about monad transformers. (If you already know all about monad transformers, +you can likely skip this section.) Different monads provide different +functionality: Reader allows read-only access to some piece of data +throughout a computation, Error allows you to short-circuit computations, and +so on.

+

Often times, however, you would like to be able to combine a few of these +features together. After all, why not have a computation with read-only access +to some settings variable, that could error out at any time? One approach to +this would be to write a new monad like ReaderError, but this has the obvious +downside of exponential complexity: you’ll need to write a new monad for every +single possible combination.

+

Instead, we have monad transformers. In addition to Reader, we have +ReaderT, which adds reader functionality to any other monad. So we could +represent our ReaderError as (conceptually):

+
type ReaderError = ReaderT Error
+

In order to access our settings variable, we can use the ask function. But +what about short-circuiting a computation? We’d like to use throwError, but +that won’t exactly work. Instead, we need to lift our call into the next +monad up. In other words:

+
throwError :: errValue -> Error
+lift . throwError :: errValue -> ReaderT Error
+

There are a few things you should pick up here:

+
    +
  • +

    +A transformer can be used to add functionality to an existing monad. +

    +
  • +
  • +

    +A transformer must always wrap around an existing monad. +

    +
  • +
  • +

    +The functionality available in a wrapped monad will be dependent not only on + the monad transformer, but also on the inner monad that is being wrapped. +

    +
  • +
+

A great example of that last point is the IO monad. No matter how many layers +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

+ +

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.

+

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 monad +transformer SqlPersistT. This means that you can run a SQL database with any +underlying monad. The takeaway here is that we can layer our Persistent +transformer on top of Handler or Widget.

+ +

In order to make it simpler to refer to the relevant Persistent transformer, +the yesod-persistent package defines the YesodPersistBackend associated type. +For example, if I have a site called MyApp and it uses SQL, I would define +something like type instance YesodPersistBackend MyApp = SqlPersistT. And for +more convenience, we have a type synonym called YesodDB which is defined as:

+
type YesodDB site = YesodPersistBackend site (HandlerT site IO)
+

Our database actions will then have types that look like YesodDB MyApp +SomeResult. In order to run these, we can use the standard Persistent unwrap +functions (like runSqlPool) to run the action and get back a normal +Handler. To automate this, we provide the runDB function. Putting it all +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 +can do, by using the handlerToWidget function. So for example, you can run +database queries inside a Widget by using something like handlerToWidget . +runDB.

+
+
+

Example: Database-driven navbar

+

Let’s put some of this new knowledge into action. We want to create a Widget +that generates its output based on the contents of the database. Previously, +our approach would have been to load up the data in a Handler, and then pass +that data into a Widget. Now, we’ll do the loading of data in the Widget +itself. This is a boon for modularity, as this Widget can be used in any +Handler we want, without any need to pass in the database contents.

+
{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE GADTs                 #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Text               (Text)
+import           Data.Time
+import           Database.Persist.Sqlite
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Link
+    title Text
+    url Text
+    added UTCTime
+|]
+
+data App = App ConnectionPool
+
+mkYesod "App" [parseRoutes|
+/         HomeR    GET
+/add-link AddLinkR POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlPersistT
+    runDB db = do
+        App pool <- getYesod
+        runSqlPool db pool
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{AddLinkR}>
+            <p>
+                Add a new link to
+                <input type=url name=url value=http://>
+                titled
+                <input type=text name=title>
+                <input type=submit value="Add link">
+        <h2>Existing links
+        ^{existingLinks}
+    |]
+
+existingLinks :: Widget
+existingLinks = do
+    links <- handlerToWidget $ runDB $ selectList [] [LimitTo 5, Desc LinkAdded]
+    [whamlet|
+        <ul>
+            $forall Entity _ link <- links
+                <li>
+                    <a href=#{linkUrl link}>#{linkTitle link}
+    |]
+
+postAddLinkR :: Handler ()
+postAddLinkR = do
+    url <- runInputPost $ ireq urlField "url"
+    title <- runInputPost $ ireq textField "title"
+    now <- liftIO getCurrentTime
+    runDB $ insert $ Link title url now
+    setMessage "Link added"
+    redirect HomeR
+
+main :: IO ()
+main = withSqlitePool "links.db3" 10 $ \pool -> do
+    runSqlPersistMPool (runMigration migrateAll) pool
+    warp 3000 $ App pool
+

Pay attention in particular to the existingLinks function. Notice how all we +needed to do was apply handlerToWidget . runDB to a normal database action. +And from within getHomeR, we treated existingLinks like any ordinary +Widget, no special parameters at all. See the figure for the output of this +app.

+ +
+
+

Example: Request information

+

Likewise, you can get request information inside a Widget. Here we can determine the sort order of a list based on a GET parameter.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.List (sortBy)
+import           Data.Ord  (comparing)
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Michael" 26
+    , Person "Gavriella" 1
+    ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href="?sort=name">Sort by name
+            |
+            <a href="?sort=age">Sort by age
+            |
+            <a href="?">No sort
+        ^{showPeople}
+    |]
+
+showPeople :: Widget
+showPeople = do
+    msort <- runInputGet $ iopt textField "sort"
+    let people' =
+            case msort of
+                Just "name" -> sortBy (comparing personName) people
+                Just "age"  -> sortBy (comparing personAge)  people
+                _           -> people
+    [whamlet|
+        <dl>
+            $forall person <- people'
+                <dt>#{personName person}
+                <dd>#{show $ personAge person}
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Notice that in this case, we didn’t even have to call handlerToWidget. The +reason is that a number of the functions included in Yesod automatically work +for both Handler and Widget, by means of the MonadHandler typeclass. In +fact, MonadHandler will allow these functions to be "autolifted" through +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

+ +

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:

+
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 +application. As a motivating example, let’s consider the +monadcryptorandom +package from Hackage, which defines both a MonadCRandom typeclass for monads +which allow generating cryptographically-secure random values, and CRandT as +a concrete instance of that typeclass. You would like to write some code that +generates a random bytestring, e.g.:

+
import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+getHomeR = do
+    randomBS <- getBytes 128
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+

However, this results in an error message along the lines of:

+
    No instance for (MonadCRandom e0 (HandlerT App IO))
+      arising from a use of ‘getBytes’
+    In a stmt of a 'do' block: randomBS <- getBytes 128
+

How do we get such an instance? One approach is to simply use the CRandT monad transformer when we call getBytes. A complete example of doing so would be:

+
{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell, TypeFamilies #-}
+import Yesod
+import Crypto.Random (SystemRandom, newGenIO)
+import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    gen <- liftIO newGenIO
+    eres <- evalCRandT (getBytes 16) (gen :: SystemRandom)
+    randomBS <-
+        case eres of
+            Left e -> error $ show (e :: GenError)
+            Right gen -> return gen
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+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.

+

But there are two downsides to this approach:

+
    +
  1. +

    +It requires you to jump into this alternate monad each time you want to work with random values. +

    +
  2. +
  3. +

    +It’s inefficient: you need to create a new random seed each time you enter this other monad. +

    +
  4. +
+

The second point could be worked around by storing the random seed in the +foundation datatype, in a mutable reference like an IORef, and then +atomically sampling it each time we enter the CRandT transformer. But we can +even go a step further, and use this trick to make our Handler monad itself +an instance of MonadCRandom! Let’s look at the code, which is in fact a bit +involved:

+
{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE TypeSynonymInstances  #-}
+import           Control.Monad              (join)
+import           Control.Monad.Catch        (catch, throwM)
+import           Control.Monad.CryptoRandom
+import           Control.Monad.Error.Class  (MonadError (..))
+import           Crypto.Random              (SystemRandom, newGenIO)
+import           Data.ByteString.Base16     (encode)
+import           Data.IORef
+import           Data.Text.Encoding         (decodeUtf8)
+import           Yesod
+
+data App = App
+    { randGen :: IORef SystemRandom
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    randomBS <- getBytes 16
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+instance MonadError GenError Handler where
+    throwError = throwM
+    catchError = catch
+instance MonadCRandom GenError Handler where
+    getCRandom  = wrap crandom
+    {-# INLINE getCRandom #-}
+    getBytes i = wrap (genBytes i)
+    {-# INLINE getBytes #-}
+    getBytesWithEntropy i e = wrap (genBytesWithEntropy i e)
+    {-# INLINE getBytesWithEntropy #-}
+    doReseed bs = do
+        genRef <- fmap randGen getYesod
+        join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+            case reseed bs gen of
+                Left e -> (gen, throwM e)
+                Right gen' -> (gen', return ())
+    {-# INLINE doReseed #-}
+
+wrap :: (SystemRandom -> Either GenError (a, SystemRandom)) -> Handler a
+wrap f = do
+    genRef <- fmap randGen getYesod
+    join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+        case f gen of
+            Left e -> (gen, throwM e)
+            Right (x, gen') -> (gen', return x)
+
+main :: IO ()
+main = do
+    gen <- newGenIO
+    genRef <- newIORef gen
+    warp 3000 App
+        { randGen = genRef
+        }
+

This really comes down to a few different concepts:

+
    +
  1. +

    +We modify the App datatype to have a field for an IORef SystemRandom. +

    +
  2. +
  3. +

    +Similarly, we modify the main function to generate an IORef SystemRandom. +

    +
  4. +
  5. +

    +Our getHomeR function became a lot simpler: we can now simply call getBytes without playing with transformers. +

    +
  6. +
  7. +

    +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. +

    +
  8. +
+

Hopefully, this helps get across an important point: the power of the +HandlerT transformer. 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 +ErrorT with this abstraction.

+
+
+

Summary

+

If you completely ignore this chapter, you’ll still be able to use Yesod to +great benefit. The advantage of understanding how Yesod’s monads interact is to +be able to produce cleaner, more modular code. Being able to perform arbitrary +actions in a Widget can be a powerful tool, and understanding how Persistent +and your Handler code interact can help you make more informed design +decisions in your app.

+
+
+
+

Note: You are looking at version 1.2 of the book, which is two versions behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4.html b/public/book-1.4.html new file mode 100644 index 00000000..09cf5843 --- /dev/null +++ b/public/book-1.4.html @@ -0,0 +1,188 @@ + Yesod Web Framework Book- Version 1.4 +

Available from O'Reilly:

+
Developing Web Applications with Haskell and Yesod + +
+
+

The current version of the book +covers Yesod 1.6. We also maintain older copies for +version 1.4, version 1.2 +, and +version 1.1.

+ +
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/authentication-and-authorization.html b/public/book-1.4/authentication-and-authorization.html new file mode 100644 index 00000000..b8647fa8 --- /dev/null +++ b/public/book-1.4/authentication-and-authorization.html @@ -0,0 +1,717 @@ + Authentication and Authorization :: Yesod Web Framework Book- Version 1.4 +
+

Authentication and Authorization

+ + +

Authentication and authorization are two very related, and yet separate, +concepts. While the former deals with identifying a user, the latter determines +what a user is allowed to do. Unfortunately, since both terms are often +abbreviated as "auth," the concepts are often conflated.

+

Yesod provides built-in support for a number of third-party authentication +systems, such as OpenID, BrowserID and OAuth. These are systems where your +application trusts some external system for validating a user’s credentials. +Additionally, there is support for more commonly used username/password and +email/password systems. The former route ensures simplicity for users (no new +passwords to remember) and implementors (no need to deal with an entire +security architecture), while the latter gives the developer more control.

+

On the authorization side, we are able to take advantage of REST and type-safe +URLs to create simple, declarative systems. Additionally, since all +authorization code is written in Haskell, you have the full flexibility of the +language at your disposal.

+

This chapter will cover how to set up an "auth" solution in Yesod and discuss +some trade-offs in the different authentication options.

+
+

Overview

+

The yesod-auth package provides a unified interface for a number of different +authentication plugins. The only real requirement for these backends is that +they identify a user based on some unique string. In OpenID, for instance, this +would be the actual OpenID value. In BrowserID, it’s the email address. For +HashDB (which uses a database of hashed passwords), it’s the username.

+

Each authentication plugin provides its own system for logging in, whether it +be via passing tokens with an external site or an email/password form. After a +successful login, the plugin sets a value in the user’s session to indicate +his/her AuthId. This AuthId is usually a Persistent ID from a table used +for keeping track of users.

+

There are a few functions available for querying a user’s AuthId, most +commonly maybeAuthId, requireAuthId, maybeAuth and requireAuth. The +“require” versions will redirect to a login page if the user is not logged in, +while the second set of functions (the ones not ending in Id) give both the +table ID and entity value.

+

Since all of the storage of AuthId is built on top of sessions, all of the +rules from there apply. In particular, the data is stored in an encrypted, +HMACed client cookie, which automatically times out after a certain configurable +period of inactivity. Additionally, since there is no server-side component to sessions, +logging out simply deletes the data from the session cookie; if a user reuses an +older cookie value, the session will still be valid.

+ +

On the flip side, authorization is handled by a few methods inside the Yesod +typeclass. For every request, these methods are run to determine if access +should be allowed, denied, or if the user needs to be authenticated. By +default, these methods allow access for every request. Alternatively, you can +implement authorization in a more ad-hoc way by adding calls to requireAuth +and the like within individual handler functions, though this undermines many +of the benefits of a declarative authorization system.

+
+
+

Authenticate Me

+

Let’s jump right in with an example of authentication. For the Google oAuth authentication to work you should follow these steps:

+
    +
  1. +

    +Read on Google Developers Help how to obtain OAuth 2.0 credentials such as a client ID and client secret that are known to both Google and your application. +

    +
  2. +
  3. +

    +Set Authorized redirect URIs to http://localhost:3000/auth/page/googleemail2/complete. +

    +
  4. +
  5. +

    +Enable Google+ API and Contacts API. +

    +
  6. +
  7. +

    +Once you have the clientId and secretId, replace them in the code below. +

    +
  8. +
+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default                (def)
+import           Data.Text                   (Text)
+import           Network.HTTP.Client.Conduit (Manager, newManager)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.BrowserId
+import           Yesod.Auth.GoogleEmail2
+
+
+-- Replace with Google client ID.
+clientId :: Text
+clientId = ""
+
+-- Replace with Google secret ID.
+clientSecret :: Text
+clientSecret = ""
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Note: In order to log in with BrowserID, you must correctly
+    -- set your hostname here.
+    approot = ApprootStatic "http://localhost:3000"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    getAuthId = return . Just . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ =
+        [ authBrowserId def
+        , authGoogleEmail clientId clientSecret
+        ]
+
+    authHttpManager = httpManager
+
+    -- The default maybeAuthId assumes a Persistent database. We're going for a
+    -- simpler AuthId, so we'll just do a direct lookup in the session.
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = do
+    man <- newManager
+    warp 3000 $ App man
+

We’ll start with the route declarations. First, we declare our standard HomeR +route, and then we set up the authentication subsite. Remember that a subsite +needs four parameters: the path to the subsite, the route name, the subsite +name, and a function to get the subsite value. In other words, based on the +line:

+
/auth AuthR Auth getAuth
+

We need to have getAuth :: MyAuthSite → Auth. While we haven’t written +that function ourselves, yesod-auth provides it automatically. With other +subsites (like static files), we provide configuration settings in the subsite +value, and therefore need to specify the get function. In the auth subsite, we +specify these settings in a separate typeclass, YesodAuth.

+ +

So what exactly goes in this YesodAuth instance? There are six required declarations:

+
    +
  • +

    +AuthId is an associated type. This is the value yesod-auth will give you + when you ask if a user is logged in (via maybeAuthId or requireAuthId). + In our case, we’re simply using Text, to store the raw identifier- email + address in our case, as we’ll soon see. +

    +
  • +
  • +

    +getAuthId gets the actual AuthId from the Creds (credentials) data + type. This type has three pieces of information: the authentication backend + used (browserid or googleemail in our case), the actual identifier, and an + associated list of arbitrary extra information. Each backend provides + different extra information; see their docs for more information. +

    +
  • +
  • +

    +loginDest gives the route to redirect to after a successful login. +

    +
  • +
  • +

    +Likewise, logoutDest gives the route to redirect to after a logout. +

    +
  • +
  • +

    +authPlugins is a list of individual authentication backends to use. In our example, we’re using BrowserID, which logs in via Mozilla’s BrowserID system, and Google oAuth, which authenticates a user using their Google account. The somewhat advantage of BrowserID backends is: +

    +
      +
    • +

      +It requires no set up, as opposed to Facebook or OAuth, which require setting up credentials. +

      +
    • +
    • +

      +It uses email addresses as identifiers, which people are comfortable with, as opposed to OpenID, which uses a URL. +

      +
    • +
    +
  • +
  • +

    +authHttpManager gets an HTTP connection manager from the foundation type. + This allows authentication backends which use HTTP connections (i.e., almost + all third-party login systems) to share connections, avoiding the cost of + restarting a TCP connection for each request. +

    +
  • +
+

In addition to these six methods, there are other methods available to control +other behavior of the authentication system, such as what the login page looks +like. For more information, please +see the API documentation.

+

In our HomeR handler, we have some simple links to the login and logout +pages, depending on whether or not the user is logged in. Notice how we +construct these subsite links: first we give the subsite route name (AuthR), +followed by the route within the subsite (LoginR and LogoutR).

+

The figures below show what the login process looks like from a user perspective.

+

Initial page load

+ + + + + + +
+

BrowserID login screen

+ + + + + + +
+

Homepage after logging in

+ + + + + + +
+
+
+

Email

+

For many use cases, third-party authentication of email will be sufficient. +Occasionally, you’ll want users to create passwords on your site. The +scaffolded site does not include this setup, because:

+
    +
  • +

    +In order to securely accept passwords, you need to be running over SSL. Many + users are not serving their sites over SSL. +

    +
  • +
  • +

    +While the email backend properly salts and hashes passwords, a compromised + database could still be problematic. Again, we make no assumptions that Yesod + users are following secure deployment practices. +

    +
  • +
  • +

    +You need to have a working system for sending email. Many web servers these + days are not equipped to deal with all of the spam protection measures used + by mail servers. +

    +
  • +
+ +

But assuming you are able to meet these demands, and you want to have a +separate password login specifically for your site, Yesod offers a built-in +backend. It requires quite a bit of code to set up, since it needs to store +passwords securely in the database and send a number of different emails to +users (verify account, password retrieval, etc.).

+

Let’s have a look at a site that provides email authentication, storing +passwords in a Persistent SQLite database.

+ +
{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad            (join)
+import           Control.Monad.Logger (runNoLoggingT)
+import           Data.Maybe               (isJust)
+import           Data.Text                (Text, unpack)
+import qualified Data.Text.Lazy.Encoding
+import           Data.Typeable            (Typeable)
+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
+import           Yesod.Auth.Email
+
+share [mkPersist sqlSettings { mpsGeneric = False }, mkMigrate "migrateAll"] [persistLowerCase|
+User
+    email Text
+    password Text Maybe -- Password may not be set yet
+    verkey Text Maybe -- Used for resetting passwords
+    verified Bool
+    UniqueUser email
+    deriving Typeable
+|]
+
+data App = App SqlBackend
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Emails will include links, so be sure to include an approot so that
+    -- the links are valid!
+    approot = ApprootStatic "http://localhost:3000"
+    yesodMiddleware = defaultCsrfMiddleware . defaultYesodMiddleware
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Set up Persistent
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB f = do
+        App conn <- getYesod
+        runSqlConn f conn
+
+instance YesodAuth App where
+    type AuthId App = UserId
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    authPlugins _ = [authEmail]
+
+    -- Need to find the UserId for the given email address.
+    getAuthId creds = runDB $ do
+        x <- insertBy $ User (credsIdent creds) Nothing Nothing False
+        return $ Just $
+            case x of
+                Left (Entity userid _) -> userid -- newly added user
+                Right userid -> userid -- existing user
+
+    authHttpManager = error "Email doesn't need an HTTP manager"
+
+instance YesodAuthPersist App
+
+-- Here's all of the email-specific code
+instance YesodAuthEmail App where
+    type AuthEmailId App = UserId
+
+    afterPasswordRoute _ = HomeR
+
+    addUnverified email verkey =
+        runDB $ insert $ User email Nothing (Just verkey) False
+
+    sendVerifyEmail email _ verurl = do
+        -- Print out to the console the verification email, for easier
+        -- debugging.
+        liftIO $ putStrLn $ "Copy/ Paste this URL in your browser:" ++ unpack verurl
+
+        -- Send email.
+        liftIO $ renderSendMail (emptyMail $ Address Nothing "noreply")
+            { mailTo = [Address Nothing email]
+            , mailHeaders =
+                [ ("Subject", "Verify your email address")
+                ]
+            , mailParts = [[textPart, htmlPart]]
+            }
+      where
+        textPart = Part
+            { partType = "text/plain; charset=utf-8"
+            , partEncoding = None
+            , partFilename = Nothing
+            , partContent = Data.Text.Lazy.Encoding.encodeUtf8
+                [stext|
+                    Please confirm your email address by clicking on the link below.
+
+                    #{verurl}
+
+                    Thank you
+                |]
+            , partHeaders = []
+            }
+        htmlPart = Part
+            { partType = "text/html; charset=utf-8"
+            , partEncoding = None
+            , partFilename = Nothing
+            , partContent = renderHtml
+                [shamlet|
+                    <p>Please confirm your email address by clicking on the link below.
+                    <p>
+                        <a href=#{verurl}>#{verurl}
+                    <p>Thank you
+                |]
+            , partHeaders = []
+            }
+    getVerifyKey = runDB . fmap (join . fmap userVerkey) . get
+    setVerifyKey uid key = runDB $ update uid [UserVerkey =. Just key]
+    verifyAccount uid = runDB $ do
+        mu <- get uid
+        case mu of
+            Nothing -> return Nothing
+            Just u -> do
+                update uid [UserVerified =. True]
+                return $ Just uid
+    getPassword = runDB . fmap (join . fmap userPassword) . get
+    setPassword uid pass = runDB $ update uid [UserPassword =. Just pass]
+    getEmailCreds email = runDB $ do
+        mu <- getBy $ UniqueUser email
+        case mu of
+            Nothing -> return Nothing
+            Just (Entity uid u) -> return $ Just EmailCreds
+                { emailCredsId = uid
+                , emailCredsAuthId = Just uid
+                , emailCredsStatus = isJust $ userPassword u
+                , emailCredsVerkey = userVerkey u
+                , emailCredsEmail = email
+                }
+    getEmail = runDB . fmap (fmap userEmail) . get
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = runNoLoggingT $ withSqliteConn "email.db3" $ \conn -> liftIO $ do
+    runSqlConn (runMigration migrateAll) conn
+    warp 3000 $ App conn
+
+
+

Authorization

+

Once you can authenticate your users, you can use their credentials to +authorize requests. Authorization in Yesod is simple and declarative: most of +the time, you just need to add the authRoute and isAuthorized methods to +your Yesod typeclass instance. Let’s see an example.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default         (def)
+import           Data.Text            (Text)
+import           Network.HTTP.Conduit (Manager, newManager, tlsManagerSettings)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy -- just for testing, don't use in real life!!!
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET POST
+/admin AdminR GET
+/auth  AuthR  Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+
+    -- route name, then a boolean indicating if it's a write request
+    isAuthorized HomeR True = isAdmin
+    isAuthorized AdminR _ = isAdmin
+
+    -- anyone can access other pages
+    isAuthorized _ _ = return Authorized
+
+isAdmin = do
+    mu <- maybeAuthId
+    return $ case mu of
+        Nothing -> AuthenticationRequired
+        Just "admin" -> Authorized
+        Just _ -> Unauthorized "You must be an admin"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    getAuthId = return . Just . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ = [authDummy]
+
+    authHttpManager = httpManager
+
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Note: Log in as "admin" to be an administrator.
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            <p>
+                <a href=@{AdminR}>Go to admin page
+            <form method=post>
+                Make a change (admins only)
+                \ #
+                <input type=submit>
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    setMessage "You made some change to the page"
+    redirect HomeR
+
+getAdminR :: Handler Html
+getAdminR = defaultLayout
+    [whamlet|
+        <p>I guess you're an admin!
+        <p>
+            <a href=@{HomeR}>Return to homepage
+    |]
+
+main :: IO ()
+main = do
+    manager <- newManager tlsManagerSettings
+    warp 3000 $ App manager
+

authRoute should be your login page, almost always AuthR LoginR. +isAuthorized is a function that takes two parameters: the requested route, +and whether or not the request was a "write" request. You can actually change +the meaning of what a write request is using the isWriteRequest method, but +the out-of-the-box version follows RESTful principles: anything but a GET, +HEAD, OPTIONS or TRACE request is a write request.

+

What’s convenient about the body of isAuthorized is that you can run any +Handler code you want. This means you can:

+
    +
  • +

    +Access the filesystem (normal IO) +

    +
  • +
  • +

    +Lookup values in the database +

    +
  • +
  • +

    +Pull any session or request values you want +

    +
  • +
+

Using these techniques, you can develop as sophisticated an authorization +system as you like, or even tie into existing systems used by your +organization.

+
+
+

Conclusion

+

This chapter covered the basics of setting up user authentication, as well as +how the built-in authorization functions provide a simple, declarative approach +for users. While these are complicated concepts, with many approaches, Yesod +should provide you with the building blocks you need to create your own +customized auth solution.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/basics.html b/public/book-1.4/basics.html new file mode 100644 index 00000000..af8c2ece --- /dev/null +++ b/public/book-1.4/basics.html @@ -0,0 +1,468 @@ + Basics :: Yesod Web Framework Book- Version 1.4 +
+

Basics

+ + +

The first step with any new technology is getting it running. The goal of +this chapter is to get you started with a simple Yesod application, and cover +some of the basic concepts and terminology.

+
+

Hello World

+

Let’s get this book started properly: a simple web page that says Hello +World:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data HelloWorld = HelloWorld
+
+mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod HelloWorld
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|Hello World!|]
+
+main :: IO ()
+main = warp 3000 HelloWorld
+

If you save that code in helloworld.hs and run it with runhaskell +helloworld.hs, you’ll get a web server running on port 3000. Note, +if you followed the quick start guide and installed yesod with stack +then you will not have runhaskell and will need to run stack runghc +helloworld.hs instead. If you point your browser to http://localhost:3000, +you’ll get the following HTML:

+
<!DOCTYPE html>
+<html><head><title></title></head><body>Hello World!</body></html>
+

We’ll refer back to this example through the rest of the chapter.

+
+
+

Routing

+

Like most modern web frameworks, Yesod follows a +front controller +pattern. This means that every request to a Yesod application enters at the +same point and is routed from there. As a contrast, in systems like PHP and ASP +you usually create a number of different files, and the web server +automatically directs requests to the relevant file.

+

In addition, Yesod uses a declarative style for specifying routes. In our +example above, this looked like:

+
mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+ +

In English, all this means is: In the HelloWorld application, create one route. +I’d like to call it HomeR, it should listen for requests to / (the root of +the application), and should answer GET requests. We call HomeR a resource, +which is where the "R" suffix comes from.

+ +

The mkYesod TH function generates quite a bit of code here: a route data +type, parser/render functions, a dispatch function, and some helper types. +We’ll look at this in more detail in the routing chapter. But by using the +-ddump-splices GHC option, we can get an immediate look at the generated +code. A much cleaned up version of it is:

+
instance RenderRoute HelloWorld where
+    data Route HelloWorld = HomeR
+        deriving (Show, Eq, Read)
+    renderRoute HomeR = ([], [])
+
+instance ParseRoute HelloWorld where
+    parseRoute ([], _) = Just HomeR
+    parseRoute _       = Nothing
+
+instance YesodDispatch HelloWorld where
+    yesodDispatch env req =
+        yesodRunner handler env mroute req
+      where
+        mroute = parseRoute (pathInfo req, textQueryString req)
+        handler =
+            case mroute of
+                Nothing -> notFound
+                Just HomeR ->
+                    case requestMethod req of
+                        "GET" -> getHomeR
+                        _     -> badMethod
+
+type Handler = HandlerT HelloWorld IO
+ +

We can see that the RenderRoute class defines an associated data type +providing the routes for our application. In this simple example, we have just +one route: HomeR. In real life applications, we’ll have many more, and they +will be more complicated than our HomeR.

+

renderRoute takes a route and turns it into path segments and query string +parameters. Again, our example is simple, so the code is likewise simple: both +values are empty lists.

+

ParseRoute provides the inverse function, parseRoute. Here we see the first +strong motivation for our reliance on Template Haskell: it ensures that the +parsing and rendering of routes correspond correctly with each other. This kind +of code can easily become difficult to keep in sync when written by hand. By +relying on code generation, we’re letting the compiler (and Yesod) handle those +details for us.

+

YesodDispatch provides a means of taking an input request and passing it to +the appropriate handler function. The process is essentially:

+
    +
  1. +

    +Parse the request. +

    +
  2. +
  3. +

    +Choose a handler function. +

    +
  4. +
  5. +

    +Run the handler function. +

    +
  6. +
+

The code generation follows a simple format for matching routes to handler +function names, which we’ll describe in the next section.

+

Finally, we have a simple type synonym defining Handler to make our code a +little easier to write.

+

There’s a lot more going on here than we’ve described. The generated dispatch +code actually uses the view patterns language extension for efficiency, more +type class instances are created, and there are other cases to handle such as +subsites. We’ll get into the details as we go through the book, especially in +the “Understanding a Request” chapter.

+
+
+

Handler function

+

So we have a route named HomeR, and it responds to GET requests. How do you +define your response? You write a handler function. Yesod follows a standard +naming scheme for these functions: it’s the lower case method name (e.g., GET +becomes get) followed by the route name. In this case, the function name +would be getHomeR.

+

Most of the code you write in Yesod lives in handler functions. This is where +you process user input, perform database queries and create responses. In our +simple example, we create a response using the defaultLayout function. This +function wraps up the content it’s given in your site’s template. By default, +it produces an HTML file with a doctype and html, head and body tags. As +we’ll see in the Yesod typeclass chapter, this function can be overridden to do +much more.

+

In our example, we pass [whamlet|Hello World!|] to defaultLayout. +whamlet is another quasi-quoter. In this case, it converts Hamlet syntax into +a Widget. Hamlet is the default HTML templating engine in Yesod. Together with +its siblings Cassius, Lucius and Julius, you can create HTML, CSS and +Javascript in a fully type-safe and compile-time-checked manner. We’ll see much +more about this in the Shakespeare chapter.

+

Widgets are another cornerstone of Yesod. They allow you to create modular +components of a site consisting of HTML, CSS and Javascript and reuse them +throughout your site. We’ll get into more detail on them in the widgets +chapter.

+
+
+

The Foundation

+

The word ‘HelloWorld’ shows up a number of times in our example. Every Yesod +application has a foundation datatype. This datatype must be an instance of the +Yesod typeclass, which provides a central place for declaring a number of +different settings controlling the execution of our application.

+

In our case, this datatype is pretty boring: it doesn’t contain any +information. Nonetheless, the foundation is central to how our example runs: it +ties together the routes with the instance declaration and lets it all be run. +We’ll see throughout this book that the foundation pops up in a whole bunch of +places.

+

But foundations don’t have to be boring: they can be used to store lots of +useful information, usually stuff that needs to be initialized at program +launch and used throughout. Some very common examples are:

+
    +
  • +

    +A database connection pool. +

    +
  • +
  • +

    +Settings loaded from a config file. +

    +
  • +
  • +

    +An HTTP connection manager. +

    +
  • +
  • +

    +A random number generator. +

    +
  • +
+ +
+
+

Running

+

Once again we mention HelloWorld in our main function. Our foundation +contains all the information we need to route and respond to requests in our +application; now we just need to convert it into something that can run. A +useful function for this in Yesod is warp, which runs the Warp webserver with +a number of default settings enabled on the specified port (here, it’s 3000).

+

One of the features of Yesod is that you aren’t tied down to a single +deployment strategy. Yesod is built on top of the Web Application Interface +(WAI), allowing it to run on FastCGI, SCGI, Warp, or even as a desktop +application using the Webkit library. We’ll discuss some of these options in +the deployment chapter. And at the end of this chapter, we will explain the +development server.

+

Warp is the premiere deployment option for Yesod. It is a lightweight, highly +efficient web server developed specifically for hosting Yesod. It is also used +outside of Yesod for other Haskell development (both framework and +non-framework applications), as well as a standard file server in a number of +production environments.

+
+
+

Resources and type-safe URLs

+

In our hello world, we defined just a single resource (HomeR). A web +application is usually much more exciting with more than one page on it. Let’s +take a look:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data Links = Links
+
+mkYesod "Links" [parseRoutes|
+/ HomeR GET
+/page1 Page1R GET
+/page2 Page2R GET
+|]
+
+instance Yesod Links
+
+getHomeR  = defaultLayout [whamlet|<a href=@{Page1R}>Go to page 1!|]
+getPage1R = defaultLayout [whamlet|<a href=@{Page2R}>Go to page 2!|]
+getPage2R = defaultLayout [whamlet|<a href=@{HomeR}>Go home!|]
+
+main = warp 3000 Links
+

Overall, this is very similar to Hello World. Our foundation is now Links +instead of HelloWorld, and in addition to the HomeR resource, we’ve added +Page1R and Page2R. As such, we’ve also added two more handler functions: +getPage1R and getPage2R.

+

The only truly new feature is inside the whamlet quasi-quotation. We’ll delve +into syntax in the “Shakespeare” chapter, but we can see that:

+
<a href=@{Page1R}>Go to page 1!
+

creates a link to the Page1R resource. The important thing to note here is +that Page1R is a data constructor. By making each resource a data +constructor, we have a feature called type-safe URLs. Instead of splicing +together strings to create URLs, we simply create a plain old Haskell value. By +using at-sign interpolation (@{…}), Yesod automatically renders those +values to textual URLs before sending things off to the user. We can see how +this is implemented by looking again at the -ddump-splices output:

+
instance RenderRoute Links where
+    data Route Links = HomeR | Page1R | Page2R
+      deriving (Show, Eq, Read)
+
+    renderRoute HomeR  = ([], [])
+    renderRoute Page1R = (["page1"], [])
+    renderRoute Page2R = (["page2"], [])
+

In the Route associated type for Links, we have additional constructors for +Page1R and Page2R. We also now have a better glimpse of the return values +for renderRoute. The first part of the tuple gives the path pieces for the +given route. The second part gives the query string parameters; for almost all +use cases, this will be an empty list.

+

It’s hard to over-estimate the value of type-safe URLs. They give you a huge +amount of flexibility and robustness when developing your application. You can +move URLs around at will without ever breaking links. In the routing chapter, +we’ll see that routes can take parameters, such as a blog entry URL taking the +blog post ID.

+

Let’s say you want to switch from routing on the numerical post ID to a +year/month/slug setup. In a traditional web framework, you would need to go +through every single reference to your blog post route and update +appropriately. If you miss one, you’ll have 404s at runtime. In Yesod, all you +do is update your route and compile: GHC will pinpoint every single line of +code that needs to be corrected.

+
+
+

Non-HTML responses

+

Yesod can serve up any kind of content you want, and has first-class support +for many commonly used response formats. You’ve seen HTML so far, but JSON data +is just as easy, via the aeson package:

+
{-# LANGUAGE ExtendedDefaultRules #-}
+{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE QuasiQuotes          #-}
+{-# LANGUAGE TemplateHaskell      #-}
+{-# LANGUAGE TypeFamilies         #-}
+import Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR  = return $ object ["msg" .= "Hello World"]
+
+main = warp 3000 App
+

We’ll cover JSON responses in more detail in later chapters, including how to +automatically switch between HTML and JSON representations depending on the +Accept request header.

+
+
+

The scaffolded site

+

Installing Yesod will give you both the Yesod library, as well as a yesod +executable. This executable accepts a few commands, but the first one you’ll +want to be acquainted with is yesod init. It will ask you some questions, and +then generate a folder containing the default scaffolded site. Inside that +folder, you can run cabal install --only-dependencies to build any extra +dependencies (such as your database backends), and then yesod devel to run +your site.

+ +

The scaffolded site gives you a lot of best practices out of the box, setting +up files and dependencies in a time-tested approach used by most production +Yesod sites. However, all this convenience can get in the way of actually +learning Yesod. Therefore, most of this book will avoid the scaffolding tool, +and instead deal directly with Yesod as a library. But if you’re going to build +a real site, I strongly recommend using the scaffolding.

+

We will cover the structure of the scaffolded site in the scaffolding chapter.

+
+
+

Development server

+

One of the advantages interpreted languages have over compiled languages is +fast prototyping: you save changes to a file and hit refresh. If we want to +make any changes to our Yesod apps above, we’ll need to call runhaskell from +scratch, which can be a bit tedious.

+

Fortunately, there’s a solution to this: yesod devel automatically rebuilds +and reloads your code for you. This can be a great way to develop your Yesod +projects, and when you’re ready to move to production, you still get to compile +down to incredibly efficient code. The Yesod scaffolding automatically sets +things up for you. This gives you the best of both worlds: rapid prototyping +and fast production code.

+

It’s a little bit more involved to set up your code to be used by yesod +devel, so our examples will just use warp. Fortunately, the scaffolded site +is fully configured to use the development server, so when you’re ready to move +over to the real world, it will be waiting for you.

+
+
+

Summary

+

Every Yesod application is built around a foundation datatype. We associate +some resources with that datatype and define some handler functions, and Yesod +handles all of the routing. These resources are also data constructors, which +lets us have type-safe URLs.

+

By being built on top of WAI, Yesod applications can run with a number of +different backends. For simple apps, the warp function provides a convenient +way to use the Warp web server. For rapid development, using yesod devel is a +good choice. And when you’re ready to move to production, you have the full +power and flexibility to configure Warp (or any other WAI handler) to suit your +needs.

+

When developing in Yesod, we get a number of choices for coding style: +quasi-quotation or external files, warp or yesod devel, and so on. The +examples in this book will tend towards using the choices that are easiest to +copy-and-paste, but the more powerful options will be available when you start +building real Yesod applications.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/blog-example-advanced.html b/public/book-1.4/blog-example-advanced.html new file mode 100644 index 00000000..d82b098f --- /dev/null +++ b/public/book-1.4/blog-example-advanced.html @@ -0,0 +1,503 @@ + Blog: i18n, authentication, authorization, and database :: Yesod Web Framework Book- Version 1.4 +
+

Blog: i18n, authentication, authorization, and database

+ + +

This is a simple blog app. It allows an admin to add blog posts via a rich text +editor (nicedit), allows logged-in users to comment, and has full i18n support. +It is also a good example of using a Persistent database, leveraging Yesod’s +authorization system, and templates.

+

While in general we recommend placing templates, Persist entity definitions, +and routing in separate files, we’ll keep it all in one file here for +convenience. The one exception you’ll see below will be i18n messages.

+

We’ll start off with our language extensions. In scaffolded code, the language +extensions are specified in the cabal file, so you won’t need to put this in +your individual Haskell files.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, GADTs, FlexibleContexts,
+             MultiParamTypeClasses, DeriveDataTypeable,
+             GeneralizedNewtypeDeriving, ViewPatterns #-}
+

Now our imports.

+
import Yesod
+import Yesod.Auth
+import Yesod.Form.Nic (YesodNic, nicHtmlField)
+import Yesod.Auth.BrowserId (authBrowserId, def)
+import Data.Text (Text)
+import Network.HTTP.Client.TLS (tlsManagerSettings)
+import Network.HTTP.Conduit (Manager, newManager)
+import Database.Persist.Sqlite
+    ( ConnectionPool, SqlBackend, runSqlPool, runMigration
+    , createSqlitePool, runSqlPersistMPool
+    )
+import Data.Time (UTCTime, getCurrentTime)
+import Control.Applicative ((<$>), (<*>), pure)
+import Data.Typeable (Typeable)
+import Control.Monad.Logger (runStdoutLoggingT)
+

First we’ll set up our Persistent entities. We’re going to both create our data +types (via mkPersist) and create a migration function, which will automatically +create and update our SQL schema. If you were using the MongoDB backend, +migration would not be needed.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+

Keeps track of users. In a more robust application, we would also keep account +creation date, display name, etc.

+
User
+   email Text
+   UniqueUser email
+

In order to work with yesod-auth’s caching, our User type must be an instance +of Typeable.

+
   deriving Typeable
+

An individual blog entry (I’ve avoided using the word "post" due to the +confusion with the request method POST).

+
Entry
+   title Text
+   posted UTCTime
+   content Html
+

And a comment on the blog post.

+
Comment
+   entry EntryId
+   posted UTCTime
+   user UserId
+   name Text
+   text Textarea
+|]
+

Every site has a foundation datatype. This value is initialized before +launching your application, and is available throughout. We’ll store a database +connection pool and HTTP connection manager in ours. See the very end of this +file for how those are initialized.

+
data Blog = Blog
+   { connPool    :: ConnectionPool
+   , httpManager :: Manager
+   }
+

To make i18n easy and translator friendly, we have a special file format for +translated messages. There is a single file for each language, and each file is +named based on the language code (e.g., en, es, de-DE) and placed in that +folder. We also specify the main language file (here, "en") as a default +language.

+
mkMessage "Blog" "blog-messages" "en"
+

Our blog-messages/en.msg file contains the following content:

+
-- @blog-messages/en.msg
+NotAnAdmin: You must be an administrator to access this page.
+
+WelcomeHomepage: Welcome to the homepage
+SeeArchive: See the archive
+
+NoEntries: There are no entries in the blog
+LoginToPost: Admins can login to post
+NewEntry: Post to blog
+NewEntryTitle: Title
+NewEntryContent: Content
+
+PleaseCorrectEntry: Your submitted entry had some errors, please correct and try again.
+EntryCreated title@Text: Your new blog post, #{title}, has been created
+
+EntryTitle title@Text: Blog post: #{title}
+CommentsHeading: Comments
+NoComments: There are no comments
+AddCommentHeading: Add a Comment
+LoginToComment: You must be logged in to comment
+AddCommentButton: Add comment
+
+CommentName: Your display name
+CommentText: Comment
+CommentAdded: Your comment has been added
+PleaseCorrectComment: Your submitted comment had some errors, please correct and try again.
+
+HomepageTitle: Yesod Blog Demo
+BlogArchiveTitle: Blog Archive
+

Now we’re going to set up our routing table. We have four entries: a homepage, +an entry list page (BlogR), an individual entry page (EntryR) and our +authentication subsite. Note that BlogR and EntryR both accept GET and POST +methods. The POST methods are for adding a new blog post and adding a new +comment, respectively.

+
mkYesod "Blog" [parseRoutes|
+/              HomeR  GET
+/blog          BlogR  GET POST
+/blog/#EntryId EntryR GET POST
+/auth          AuthR  Auth getAuth
+|]
+

Every foundation needs to be an instance of the Yesod typeclass. This is where +we configure various settings.

+
instance Yesod Blog where
+

The base of our application. Note that in order to make BrowserID work +properly, this must be a valid URL.

+
    approot = ApprootStatic "http://localhost:3000"
+

Our authorization scheme. We want to have the following rules:

+
    +
  • +

    +Only admins can add a new entry. +

    +
  • +
  • +

    +Only logged in users can add a new comment. +

    +
  • +
  • +

    +All other pages can be accessed by anyone. +

    +
  • +
+

We set up our routes in a RESTful way, where the actions that could make +changes are always using a POST method. As a result, we can simply check for +whether or not a request is a write request, given by the True in the second +field.

+

First, we’ll authorize requests to add a new entry.

+
    isAuthorized BlogR True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just (Entity _ user)
+                | isAdmin user -> return Authorized
+                | otherwise    -> unauthorizedI MsgNotAnAdmin
+

Now we’ll authorize requests to add a new comment.

+
    isAuthorized (EntryR _) True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just _  -> return Authorized
+

And for all other requests, the result is always authorized.

+
    isAuthorized _ _ = return Authorized
+

Where a user should be redirected to if they get an AuthenticationRequired.

+
    authRoute _ = Just (AuthR LoginR)
+

This is where we define our site look-and-feel. The function is given the +content for the individual page, and wraps it up with a standard template.

+
    defaultLayout inside = do
+

Yesod encourages the get-following-post pattern, where after a POST, the user +is redirected to another page. In order to allow the POST page to give the user +some kind of feedback, we have the getMessage and setMessage functions. It’s a +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 +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 +CSS for all pages. We’ll use the Lucius template language to create the latter.

+
        pc <- widgetToPageContent $ do
+            toWidget [lucius|
+body {
+    width: 760px;
+    margin: 1em auto;
+    font-family: sans-serif;
+}
+textarea {
+    width: 400px;
+    height: 200px;
+}
+#message {
+  color: #900;
+}
+|]
+            inside
+

And finally we’ll use a new Hamlet template to wrap up the individual +components (title, head data and body data) into the final output.

+
        withUrlRenderer [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <div #message>#{msg}
+        ^{pageBody pc}
+|]
+

This is a simple function to check if a user is the admin. In a real +application, we would likely store the admin bit in the database itself, or +check with some external system. For now, I’ve just hard-coded my own email +address.

+
isAdmin :: User -> Bool
+isAdmin user = userEmail user == "michael@snoyman.com"
+

In order to access the database, we need to create a YesodPersist instance, +which says which backend we’re using and how to run an action.

+
instance YesodPersist Blog where
+   type YesodPersistBackend Blog = SqlBackend
+   runDB f = do
+       master <- getYesod
+       let pool = connPool master
+       runSqlPool f pool
+

This is a convenience synonym. It is defined automatically for you in the +scaffolding.

+
type Form x = Html -> MForm Handler (FormResult x, Widget)
+

In order to use yesod-form and yesod-auth, we need an instance of RenderMessage +for FormMessage. This allows us to control the i18n of individual form +messages.

+
instance RenderMessage Blog FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

In order to use the built-in nic HTML editor, we need this instance. We just +take the default values, which use a CDN-hosted version of Nic.

+
instance YesodNic Blog
+

In order to use yesod-auth, we need a YesodAuth instance.

+
instance YesodAuth Blog where
+    type AuthId Blog = UserId
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    authHttpManager = httpManager
+

We’ll use BrowserID (a.k.a. Mozilla Persona), +which is a third-party system using email addresses as your identifier. This +makes it easy to switch to other systems in the future, locally authenticated +email addresses (also included with yesod-auth).

+
    authPlugins _ = [authBrowserId def]
+

This function takes someone’s login credentials (i.e., his/her email address) +and gives back a UserId.

+
    getAuthId creds = do
+        let email = credsIdent creds
+            user = User email
+        res <- runDB $ insertBy user
+        return $ Just $ either entityKey id res
+

We also need to provide a YesodAuthPersist instance to work with Persistent.

+
instance YesodAuthPersist Blog
+

Homepage handler. The one important detail here is our usage of setTitleI, +which allows us to use i18n messages for the title. We also use this message +with a _{Msg…} interpolation in Hamlet.

+
getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitleI MsgHomepageTitle
+    [whamlet|
+<p>_{MsgWelcomeHomepage}
+<p>
+   <a href=@{BlogR}>_{MsgSeeArchive}
+|]
+

Define a form for adding new entries. We want the user to provide the title and +content, and then fill in the post date automatically via getCurrentTime.

+

Note that slightly strange lift (liftIO getCurrentTime) manner of running an +IO action. The reason is that applicative forms are not monads, and therefore +cannot be instances of MonadIO. Instead, we use lift to run the action in +the underlying Handler monad, and liftIO to convert the IO action into a +Handler action.

+
entryForm :: Form Entry
+entryForm = renderDivs $ Entry
+    <$> areq textField (fieldSettingsLabel MsgNewEntryTitle) Nothing
+    <*> lift (liftIO getCurrentTime)
+    <*> areq nicHtmlField (fieldSettingsLabel MsgNewEntryContent) Nothing
+

Get the list of all blog entries, and present an admin with a form to create a +new entry.

+
getBlogR :: Handler Html
+getBlogR = do
+    muser <- maybeAuth
+    entries <- runDB $ selectList [] [Desc EntryPosted]
+    (entryWidget, enctype) <- generateFormPost entryForm
+    defaultLayout $ do
+        setTitleI MsgBlogArchiveTitle
+        [whamlet|
+$if null entries
+    <p>_{MsgNoEntries}
+$else
+    <ul>
+        $forall Entity entryId entry <- entries
+            <li>
+                <a href=@{EntryR entryId}>#{entryTitle entry}
+

We have three possibilities: the user is logged in as an admin, the user is +logged in and is not an admin, and the user is not logged in. In the first +case, we should display the entry form. In the second, we’ll do nothing. In the +third, we’ll provide a login link.

+
$maybe Entity _ user <- muser
+    $if isAdmin user
+        <form method=post enctype=#{enctype}>
+            ^{entryWidget}
+            <div>
+                <input type=submit value=_{MsgNewEntry}>
+$nothing
+    <p>
+        <a href=@{AuthR LoginR}>_{MsgLoginToPost}
+|]
+

Process an incoming entry addition. We don’t do any permissions checking, since +isAuthorized handles it for us. If the form submission was valid, we add the +entry to the database and redirect to the new entry. Otherwise, we ask the user +to try again.

+
postBlogR :: Handler Html
+postBlogR = do
+    ((res, entryWidget), enctype) <- runFormPost entryForm
+    case res of
+        FormSuccess entry -> do
+            entryId <- runDB $ insert entry
+            setMessageI $ MsgEntryCreated $ entryTitle entry
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectEntry
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{entryWidget}
+    <div>
+        <input type=submit value=_{MsgNewEntry}>
+|]
+

A form for comments, very similar to our entryForm above. It takes the +EntryId of the entry the comment is attached to. By using pure, we embed +this value in the resulting Comment output, without having it appear in the +generated HTML.

+
commentForm :: EntryId -> Form Comment
+commentForm entryId = renderDivs $ Comment
+    <$> pure entryId
+    <*> lift (liftIO getCurrentTime)
+    <*> lift requireAuthId
+    <*> areq textField (fieldSettingsLabel MsgCommentName) Nothing
+    <*> areq textareaField (fieldSettingsLabel MsgCommentText) Nothing
+

Show an individual entry, comments, and an add comment form if the user is +logged in.

+
getEntryR :: EntryId -> Handler Html
+getEntryR entryId = do
+    (entry, comments) <- runDB $ do
+        entry <- get404 entryId
+        comments <- selectList [CommentEntry ==. entryId] [Asc CommentPosted]
+        return (entry, map entityVal comments)
+    muser <- maybeAuth
+    (commentWidget, enctype) <-
+        generateFormPost (commentForm entryId)
+    defaultLayout $ do
+        setTitleI $ MsgEntryTitle $ entryTitle entry
+        [whamlet|
+<h1>#{entryTitle entry}
+<article>#{entryContent entry}
+    <section .comments>
+        <h1>_{MsgCommentsHeading}
+        $if null comments
+            <p>_{MsgNoComments}
+        $else
+            $forall Comment _entry posted _user name text <- comments
+                <div .comment>
+                    <span .by>#{name}
+                    <span .at>#{show posted}
+                    <div .content>#{text}
+        <section>
+            <h1>_{MsgAddCommentHeading}
+            $maybe _ <- muser
+                <form method=post enctype=#{enctype}>
+                    ^{commentWidget}
+                    <div>
+                        <input type=submit value=_{MsgAddCommentButton}>
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>_{MsgLoginToComment}
+|]
+

Receive an incoming comment submission.

+
postEntryR :: EntryId -> Handler Html
+postEntryR entryId = do
+    ((res, commentWidget), enctype) <-
+        runFormPost (commentForm entryId)
+    case res of
+        FormSuccess comment -> do
+            _ <- runDB $ insert comment
+            setMessageI MsgCommentAdded
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectComment
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{commentWidget}
+    <div>
+        <input type=submit value=_{MsgAddCommentButton}>
+|]
+

Finally our main function.

+
main :: IO ()
+main = do
+    pool <- runStdoutLoggingT $ createSqlitePool "blog.db3" 10 -- create a new pool
+    -- perform any necessary migration
+    runSqlPersistMPool (runMigration migrateAll) pool
+    manager <- newManager tlsManagerSettings -- create a new HTTP manager
+    warp 3000 $ Blog pool manager -- start our server
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/case-study-sphinx.html b/public/book-1.4/case-study-sphinx.html new file mode 100644 index 00000000..fbdbb61f --- /dev/null +++ b/public/book-1.4/case-study-sphinx.html @@ -0,0 +1,791 @@ + Case Study: Sphinx-based Search :: Yesod Web Framework Book- Version 1.4 +
+

Case Study: Sphinx-based Search

+ + +

Sphinx is a search server, and powers the +search feature on many sites. While the actual code necessary to integrate +Yesod with Sphinx is relatively short, it touches on a number of complicated +topics, and is therefore a great case study in how to play with some of the +under-the-surface details of Yesod.

+

There are essentially three different pieces at play here:

+
    +
  • +

    +Storing the content we wish to search. This is fairly straight-forward + Persistent code, and we won’t dwell on it much in this chapter. +

    +
  • +
  • +

    +Accessing Sphinx search results from inside Yesod. Thanks to the sphinx + package, this is actually very easy. +

    +
  • +
  • +

    +Providing the document content to Sphinx. This is where the interesting stuff + happens, and will show how to deal with streaming content from a database + directly to XML, which gets sent directly over the wire to the client. +

    +
  • +
+

The full code for this example can be +found +on FP Haskell Center.

+
+

Sphinx Setup

+

Unlike many of our other examples, to start with here we’ll need to actually +configure and run our external Sphinx server. I’m not going to go into all the +details of Sphinx, partly because it’s not relevant to our point here, and +mostly because I’m not an expert on Sphinx.

+

Sphinx provides three main command line utilities: searchd is the actual +search daemon that receives requests from the client (in this case, our web +app) and returns the search results. indexer parses the set of documents and +creates the search index. search is a debugging utility that will run simple +queries against Sphinx.

+

There are two important settings: the source and the index. The source tells +Sphinx where to read document information from. It has direct support for MySQL +and PostgreSQL, as well as a more general XML format known as xmlpipe2. We’re +going to use the last one. This not only will give us more flexibility with +choosing Persistent backends, but will also demonstrate some more powerful +Yesod concepts.

+

The second setting is the index. Sphinx can handle multiple indices +simultaneously, which allows it to provide search for multiple services at +once. Each index will have a source it pulls from.

+

In our case, we’re going to provide a URL from our application +(/search/xmlpipe) that provides the XML file required by Sphinx, and then pipe +that through to the indexer. So we’ll add the following to our Sphinx config +file:

+
source searcher_src
+{
+        type = xmlpipe2
+        xmlpipe_command = curl http://localhost:3000/search/xmlpipe
+}
+
+index searcher
+{
+        source = searcher_src
+        path = /var/data/searcher
+        docinfo = extern
+        charset_type = utf-8
+}
+
+searchd
+{
+        listen                  = 9312
+        pid_file                = /var/run/sphinxsearch/searchd.pid
+}
+

In order to build your search index, you would run indexer searcher. +Obviously this won’t work until you have your web app running. For a production +site, it would make sense to run this command via a cron job so the index +is regularly updated.

+
+
+

Basic Yesod Setup

+

Let’s get our basic Yesod setup going. We’re going to have a single table in +the database for holding documents, which consist of a title and content. We’ll +store this in a SQLite database, and provide routes for searching, adding +documents, viewing documents and providing the xmlpipe file to Sphinx.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlBackend
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where -- see below
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

Hopefully all of this looks pretty familiar by now. The one new thing we’ve +defined here is an instance of YesodPersistRunner. This is a typeclass +necessary for creating streaming database responses. The default implementation +(defaultGetDBRunner) is almost always appropriate.

+

Next we’ll define some forms: one for creating documents, and one for searching:

+
addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+

The True parameter to searchField makes the field auto-focus on page load. +Finally, we have some standard handlers for the homepage (shows the add +document form and the search form), the document display, and adding a +document.

+
getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+
+

Searching

+

Now that we’ve got the boring stuff out of the way, let’s jump into the actual +searching. We’re going to need three pieces of information for displaying a +result: the document ID it comes from, the title of that document, and the +excerpts. Excerpts are the highlighted portions of the document which contain +the search term.

+

Search Result

+ + + + + + +
+

So let’s start off by defining a Result datatype:

+
data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+

Next we’ll look at the search handler:

+
getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+

Nothing magical here, we’re just relying on the searchForm defined above, and +the getResults function which hasn’t been defined yet. This function just +takes a search string, and returns a list of results. This is where we first +interact with the Sphinx API. We’ll be using two functions: query will return +a list of matches, and buildExcerpts will return the highlighted excerpts. +Let’s first look at getResults:

+
getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (toSqlKey . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+

query takes three parameters: the configuration options, the index to search +against (searcher in this case) and the search string. It returns a list of +document IDs that contain the search string. The tricky bit here is that those +documents are returned as Int64 values, whereas we need DocIds. +Fortunately, for the SQL Persist backends, we can just use the toSqlKey +function to perform the conversion.

+ +

We then loop over the resulting IDs to get a [Maybe Result] value, and use +catMaybes to turn it into a [Result]. In the where clause, we define our +local settings, which override the default port and set up the search to work +when any term matches the document.

+

Let’s finally look at the getResult function:

+
getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [escape $ docContent doc]
+        "searcher"
+        qstring
+    let excerpt =
+            case excerpt' of
+                ST.Ok texts -> preEscapedToHtml $ mconcat texts
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+

buildExcerpts takes four parameters: the configuration options, the textual +contents of the document, the search index and the search term. The interesting +bit is that we entity escape the text content. Sphinx won’t automatically +escape these for us, so we must do it explicitly.

+

Similarly, the result from Sphinx is a list of Texts. But of course, we’d +rather have Html. So we concat that list into a single Text and use +preEscapedToHtml to make sure that the tags inserted for matches are not +escaped. A sample of this HTML is:

+
&#8230; Departments.  The President shall have <span class='match'>Power</span> to fill up all Vacancies
+&#8230;  people. Amendment 11 The Judicial <span class='match'>power</span> of the United States shall
+&#8230; jurisdiction. 2. Congress shall have <span class='match'>power</span> to enforce this article by
+&#8230; 5. The Congress shall have <span class='match'>power</span> to enforce, by appropriate legislation
+&#8230;
+
+
+

Streaming xmlpipe output

+

We’ve saved the best for last. For the majority of Yesod handlers, the +recommended approach is to load up the database results into memory and then +produce the output document based on that. It’s simpler to work with, but more +importantly it’s more resilient to exceptions. If there’s a problem loading the +data from the database, the user will get a proper 500 response code.

+ +

However, generating the xmlpipe output is a perfect example of the alternative. +There are potentially a huge number of documents, and documents could easily be +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: +a content type, and a conduit Source providing a stream of blaze-builder +Builders. 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.

+

Now we know we want to create a stream of Builders from some XML content. +Fortunately, the xml-conduit package provides this interface directly. +xml-conduit provides some high-level interfaces for dealing with documents as +a whole, but in our case, we’re going to need to use the low-level Event +interface to ensure minimal memory impact. So the function we’re interested in +is:

+
renderBuilder :: Monad m => RenderSettings -> Conduit Event m Builder
+

In plain English, that means renderBuilder takes some settings (we’ll just use +the defaults), and will then convert a stream of Events to a stream of +Builders. This is looking pretty good, all we need now is a stream of +Events.

+

Speaking of which, what should our XML document actually look like? It’s pretty +simple, we have a sphinx:docset root element, a sphinx:schema element +containing a single sphinx:field (which defines the content field), and then +a sphinx:document for each document in our database. That last element will +have an id attribute and a child content element. Below is an example of +such a document:

+
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">
+    <sphinx:schema>
+        <sphinx:field name="content"/>
+    </sphinx:schema>
+    <sphinx:document id="1">
+        <content>bar</content>
+    </sphinx:document>
+    <sphinx:document id="2">
+        <content>foo bar baz</content>
+    </sphinx:document>
+</sphinx:docset>
+ +

Every document is going to start off with the same events (start the docset, +start the schema, etc) and end with the same event (end the docset). We’ll +start off by defining those:

+
toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+

Now that we have the shell of our document, we need to get the Events for +each individual document. This is actually a fairly simple function:

+
entityToEvents :: Entity Doc -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+

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.

+

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 +also provides the (more powerful) selectSource function. So we end up with +the function:

+
docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+

The $= operator joins together a source and a conduit into a new source. 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 +piece of cake:

+
fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+

Now we need to tie it together in getXmlpipeR. To do so, we’ll use the respondSourceDB function mentioned earlier. The last trick we need to do is convert our stream of Events into a stream of Chunk Builders. Converting to a stream of Builders is achieved with renderBuilder, and finally we’ll just wrap each Builder in its own Chunk:

+
getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+
+

Full code

+
{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# 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)
+import qualified Text.Search.Sphinx                      as S
+import qualified Text.Search.Sphinx.ExcerptConfiguration as E
+import qualified Text.Search.Sphinx.Types                as ST
+import           Text.XML.Stream.Render                  (def, renderBuilder)
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlBackend
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+
+getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [escape $ docContent doc]
+        "searcher"
+        qstring
+    let excerpt =
+            case excerpt' of
+                ST.Ok texts -> preEscapedToHtml $ mconcat texts
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+
+getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (toSqlKey . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+
+getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+
+fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+
+docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+
+toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+
+main :: IO ()
+main = runStdoutLoggingT $ withSqlitePool "searcher.db3" 10 $ \pool -> liftIO $ do
+    runSqlPool (runMigration migrateAll) pool
+    warp 3000 $ Searcher pool
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/creating-a-subsite.html b/public/book-1.4/creating-a-subsite.html new file mode 100644 index 00000000..9778918f --- /dev/null +++ b/public/book-1.4/creating-a-subsite.html @@ -0,0 +1,226 @@ + Creating a Subsite :: Yesod Web Framework Book- Version 1.4 +
+

Creating a Subsite

+ + +

How many sites provide authentication systems? Or need to provide +create-read-update-delete (CRUD) management of some objects? Or a blog? Or a +wiki?

+

The theme here is that many websites include common components that can be +reused throughout multiple sites. However, it is often quite difficult to get +code to be modular enough to be truly plug-and-play: a component will require +hooks into the routing system, usually for multiple routes, and will need some +way of sharing styling information with the master site.

+

In Yesod, the solution is subsites. A subsite is a collection of routes and +their handlers that can be easily inserted into a master site. By using type +classes, it is easy to ensure that the master site provides certain +capabilities, and to access the default site layout. And with type-safe URLs, +it’s easy to link from the master site to subsites.

+
+

Hello World

+

Perhaps the trickiest part of writing subsites is getting started. Let’s dive +in with a simple Hello World subsite. We need to create one module to contain +our subsite’s data types, another for the subsite’s dispatch code, and then a +final module for an application that uses the subsite.

+ +
-- @HelloSub/Data.hs
+{-# LANGUAGE QuasiQuotes     #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies    #-}
+module HelloSub.Data where
+
+import           Yesod
+
+-- Subsites have foundations just like master sites.
+data HelloSub = HelloSub
+
+-- We have a familiar analogue from mkYesod, with just one extra parameter.
+-- We'll discuss that later.
+mkYesodSubData "HelloSub" [parseRoutes|
+/ SubHomeR GET
+|]
+
-- @HelloSub.hs
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+module HelloSub
+    ( module HelloSub.Data
+    , module HelloSub
+    ) where
+
+import           HelloSub.Data
+import           Yesod
+
+-- And we'll spell out the handler type signature.
+getSubHomeR :: Yesod master => HandlerT HelloSub (HandlerT master IO) Html
+getSubHomeR = lift $ defaultLayout [whamlet|Welcome to the subsite!|]
+
+instance Yesod master => YesodSubDispatch HelloSub (HandlerT master IO) where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesHelloSub)
+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           HelloSub
+import           Yesod
+
+-- And let's create a master site that calls it.
+data Master = Master
+    { getHelloSub :: HelloSub
+    }
+
+mkYesod "Master" [parseRoutes|
+/ HomeR GET
+/subsite SubsiteR HelloSub getHelloSub
+|]
+
+instance Yesod Master
+
+-- Spelling out type signature again.
+getHomeR :: HandlerT Master IO Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>Welcome to the homepage
+        <p>
+            Feel free to visit the #
+            <a href=@{SubsiteR SubHomeR}>subsite
+            \ as well.
+    |]
+
+main = warp 3000 $ Master HelloSub
+

This simple example actually shows most of the complications involved in +creating a subsite. Like a normal Yesod application, everything in a subsite is +centered around a foundation datatype, HelloSub in our case. We then use +mkYesodSubData to generate our subsite route data type and associated parse +and render functions.

+

On the dispatch side, we start off by defining our handler function for the SubHomeR route. You should pay special attention to the type signature on this function:

+
getSubHomeR :: Yesod master
+            => HandlerT HelloSub (HandlerT master IO) Html
+

This is the heart and soul of what a subsite is all about. All of our actions +live in this layered monad, where we have our subsite wrapping around our main +site. Given this monadic layering, it should come as no surprise that we end up +calling lift. In this case, our subsite is using the master site’s +defaultLayout function to render a widget.

+

The defaultLayout function is part of the Yesod typeclass. Therefore, in +order to call it, the master type argument must be an instance of Yesod. +The advantage of this approach is that any modifications to the master site’s +defaultLayout method will automatically be reflected in subsites.

+

When we embed a subsite in our master site route definition, we need to specify +four pieces of information: the route to use as the base of the subsite (in +this case, /subsite), the constructor for the subsite routes (SubsiteR), +the subsite foundation data type (HelloSub) and a function that takes a +master foundation value and returns a subsite foundation value (getHelloSub).

+

In the definition of getHomeR, we can see how the route constructor gets used. +In a sense, SubsiteR promotes any subsite route to a master site route, +making it possible to safely link to it from any master site template.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/deploying-your-webapp.html b/public/book-1.4/deploying-your-webapp.html new file mode 100644 index 00000000..0375c734 --- /dev/null +++ b/public/book-1.4/deploying-your-webapp.html @@ -0,0 +1,582 @@ + Deploying your Webapp :: Yesod Web Framework Book- Version 1.4 +
+

Deploying your Webapp

+ + +

I can’t speak for others, but I personally prefer programming to system +administration. But the fact is that, eventually, you need to serve your app +somehow, and odds are that you’ll need to be the one to set it up.

+

There are some promising initiatives in the Haskell web community towards +making deployment easier. In the future, we may even have a service that allows +you to deploy your app with a single command.

+

But we’re not there yet. And even if we were, such a solution will never work +for everyone. This chapter covers the different options you have for +deployment, and gives some general recommendations on what you should choose in +different situations.

+

The Yesod team strongly recommends using the stack build tool as discussed in +the quick start guide for Yesod +development, check out the quick start if you haven’t already.

+
+

Keter

+

The Yesod scaffolding comes with some built-in support for the Keter deployment +engine, which is also written in Haskell and uses many of the same underlying +technologies like WAI and http-client. Keter works as a reverse proxy to your +applications, as well as a system for starting, monitoring, and redeploying +running apps. If you’d like to deploy with Keter, follow these steps:

+
    +
  1. +

    +Edit the config/keter.yaml file in your scaffolded application as necessary. +

    +
  2. +
  3. +

    +Set up some kind of server for hosting yours apps. I recommend trying Ubuntu on Amazon EC2. +

    +
  4. +
  5. +

    +Install Keter on that machine. Please follow the instructions on the Keter website, which will be the most up to date. +

    +
  6. +
  7. +

    +Run yesod keter to generate a Keter bundle, e.g., myapp.keter. +

    +
  8. +
  9. +

    +Copy myapp.keter to the /opt/keter/incoming directory on your server. +

    +
  10. +
+

If you’ve got things configured correctly, you should now be able to view +your website, running in a production environment! In the future, upgrades can +be handled by simply rerunning yesod keter and recopying the myapp.keter +bundle to the server. Note that Keter will automatically detect the presence of +the new file and reload your application.

+

The rest of this chapter will go into so more details about various steps, and +provide some alternatives for people looking to either not use the scaffolding +or not use Keter.

+
+
+

Compiling

+

The biggest advice I can give is: don’t compile on your server. It’s tempting to do so, as you have to just transfer source code around, and you avoid confusing dependency issues. However, compiling a Yesod application takes significant memory and CPU, which means:

+
    +
  • +

    +While you’re recompiling your app, your existing application will suffer performance-wise. +

    +
  • +
  • +

    +You will need to get a much larger machine to handle compilation, and that capacity will likely sit idle most of the time, since Yesod applications tend to require far less CPU and memory than GHC itself. +

    +
  • +
+

Once you’re ready to compile, you should always make sure to stack clean +before a new production build, to make sure no old files are lying around. +Then, you can run stack build to get an executable, which +will be located at dist/build/myapp/myapp.

+
+
+

Files to deploy

+

With a Yesod scaffolded application, there are essentially three sets of files that need +to be deployed:

+
    +
  1. +

    +Your executable. +

    +
  2. +
  3. +

    +The config folder. +

    +
  4. +
  5. +

    +The static folder. +

    +
  6. +
+

Everything else, such as Shakespearean templates, gets compiled into the +executable itself.

+

There is one caveat, however: the config/client_session_key.aes file. This +file controls the server side encryption used for securing client side session +cookies. Yesod will automatically generate a new one of these keys if none is +present. In practice, this means that, if you do not include this file in +deployment, all of your users will have to log in again when you redeploy. If +you follow the advice above and include the config folder, this issue will be +partially resolved. Another approach is to +put +your session key in an environment variable.

+

The other half of the resolution is to ensure that once you generate a +config/client_session_key.aes file, you keep the same one for all future +deployments. The simplest way to ensure this is to keep that file in your +version control. However, if your version control is open source, this will be +dangerous: anyone with access to your repository will be able to spoof login +credentials!

+

The problem described here is essentially one of system administration, not +programming. Yesod does not provide any built-in approach for securely storing +client session keys. If you have an open source repository, or do not trust +everyone who has access to your source code repository, it’s vital to figure +out a safe storage solution for the client session key.

+
+
+

SSL and static files

+

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 +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:

+
    +
  • +

    +Serve your static files over HTTPS as well. +

    +
  • +
  • +

    +Serve your static files from the same domain name as your main site. +

    +
  • +
+

Note that if you go for the first option (which is the better one), you’ll +either need two separate SSL certificates, or a wildcard certificate.

+
+
+

Warp

+

As we have mentioned before, Yesod is built on the Web Application Interface +(WAI), allowing it to run on any WAI backend. At the time of writing, the +following backends are available:

+
    +
  • +

    +Warp +

    +
  • +
  • +

    +FastCGI +

    +
  • +
  • +

    +SCGI +

    +
  • +
  • +

    +CGI +

    +
  • +
  • +

    +Webkit +

    +
  • +
  • +

    +Development server +

    +
  • +
+

The last two are not intended for production deployments. Of the remaining +four, all can be used for production deployment in theory. In practice, a CGI +backend will likely be horribly inefficient, since a new process must be +spawned for each connection. And SCGI is not nearly as well supported by +frontend web servers as Warp (via reverse proxying) or FastCGI.

+

So between the two remaining choices, Warp gets a very strong recommendation +because:

+
    +
  • +

    +It is significantly faster. +

    +
  • +
  • +

    +Like FastCGI, it can run behind a frontend server like Nginx, using reverse + HTTP proxy. +

    +
  • +
  • +

    +In addition, it is a fully capable server of its own accord, and can + therefore be used without any frontend server. +

    +
  • +
+

So that leaves one last question: should Warp run on its own, or via reverse +proxy behind a frontend server? For most use cases, I recommend the latter, +because:

+
    +
  • +

    +Having a reverse proxy in front of your app makes it easier to deploy new versions. +

    +
  • +
  • +

    +Also, if you have a bug in your application, a reverse proxy can give slightly nicer error messages to users. +

    +
  • +
  • +

    +You can host multiple applications from a single host via virtual hosting. +

    +
  • +
  • +

    +Your reverse proxy can function as both a load balancer or SSL proxy as well, simplifying your application. +

    +
  • +
+

As discussed above, Keter is a great way to get started. If you have an +existing web server running like Nginx, Yesod will work just fine sitting +behind it instead.

+
+

Nginx Configuration

+

Keter configuration is trivial, since it is designed to work with Yesod +applications. But if you want to instead use Nginx, how do you set it up?

+

In general, Nginx will listen on port 80 and your Yesod/Warp app will listen on +some unprivileged port (lets say 4321). You will then need to provide a +nginx.conf file, such as:

+
daemon off; # Don't run nginx in the background, good for monitoring apps
+events {
+    worker_connections 4096;
+}
+
+http {
+    server {
+        listen 80; # Incoming port for Nginx
+        server_name www.myserver.com;
+        location / {
+            proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+        }
+    }
+}
+

You can add as many server blocks as you like. A common addition is to ensure +users always access your pages with the www prefix on the domain name, ensuring +the RESTful principle of canonical URLs. (You could just as easily do the +opposite and always strip the www, just make sure that your choice is reflected +in both the nginx config and the approot of your site.) In this case, we would +add the block:

+
server {
+    listen 80;
+    server_name myserver.com;
+    rewrite ^/(.*) http://www.myserver.com/$1 permanent;
+}
+

A highly recommended optimization is to serve static files from a separate +domain name, therefore bypassing the cookie transfer overhead. Assuming that +our static files are stored in the static folder within our site folder, and +the site folder is located at /home/michael/sites/mysite, this would look +like:

+
server {
+    listen 80;
+    server_name static.myserver.com;
+    root /home/michael/sites/mysite/static;
+    # Since yesod-static appends a content hash in the query string,
+    # we are free to set expiration dates far in the future without
+    # concerns of stale content.
+    expires max;
+}
+

In order for this to work, your site must properly rewrite static URLs to this +alternate domain name. The scaffolded site is set up to make this fairly simple +via the Settings.staticRoot function and the definition of +urlRenderOverride. However, if you just want to get the benefit of nginx’s +faster static file serving without dealing with separate domain names, you can +instead modify your original server block like so:

+
server {
+    listen 80; # Incoming port for Nginx
+    server_name www.myserver.com;
+    location / {
+        proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+    }
+    location /static {
+        root /home/michael/sites/mysite; # Notice that we do *not* include /static
+        expires max;
+    }
+}
+
+
+

Server Process

+

Many people are familiar with an Apache/mod_php or Lighttpd/FastCGI kind of +setup, where the web server automatically spawns the web application. With +nginx, either for reverse proxying or FastCGI, this is not the case: you are +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 +/etc/systemd/system/yesod-sample.service:

+
[Service]
+ExecStart=/home/sibi/.local/bin/my-yesod-executable
+Restart=always
+StandardOutput=syslog
+StandardError=syslog
+SyslogIdentifier=yesod-sample
+
+[Install]
+WantedBy=multi-user.target
+

Now you can start your service with:

+
systemctl enable yesod-sample
+systemctl start yesod-sample
+

You can also see the status of your process using systemctl status +yesod-sample.

+
+
+
+

Nginx + FastCGI

+

Some people may prefer using FastCGI for deployment. In this case, you’ll need +to add an extra tool to the mix. FastCGI works by receiving new connection from +a file descriptor. The C library assumes that this file descriptor will be 0 +(standard input), so you need to use the spawn-fcgi program to bind your +application’s standard input to the correct socket.

+

It can be very convenient to use Unix named sockets for this instead of binding +to a port, especially when hosting multiple applications on a single host. A +possible script to load up your app could be:

+
spawn-fcgi \
+    -d /home/michael/sites/mysite \
+    -s /tmp/mysite.socket \
+    -n \
+    -M 511 \
+    -u michael \
+    -- /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi
+

You will also need to configure your frontend server to speak to your app over +FastCGI. This is relatively painless in Nginx:

+
server {
+    listen 80;
+    server_name www.myserver.com;
+    location / {
+        fastcgi_pass unix:/tmp/mysite.socket;
+    }
+}
+

That should look pretty familiar from above. The only last trick is that, with +Nginx, you need to manually specify all of the FastCGI variables. It is +recommended to store these in a separate file (say, fastcgi.conf) and then add +include fastcgi.conf; to the end of your http block. The contents of the +file, to work with WAI, should be:

+
fastcgi_param  QUERY_STRING       $query_string;
+fastcgi_param  REQUEST_METHOD     $request_method;
+fastcgi_param  CONTENT_TYPE       $content_type;
+fastcgi_param  CONTENT_LENGTH     $content_length;
+fastcgi_param  PATH_INFO          $fastcgi_script_name;
+fastcgi_param  SERVER_PROTOCOL    $server_protocol;
+fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
+fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
+fastcgi_param  REMOTE_ADDR        $remote_addr;
+fastcgi_param  SERVER_ADDR        $server_addr;
+fastcgi_param  SERVER_PORT        $server_port;
+fastcgi_param  SERVER_NAME        $server_name;
+
+
+

Desktop

+

Another nifty backend is wai-handler-webkit. This backend combines Warp and +QtWebkit to create an executable that a user simply double-clicks. This can be +a convenient way to provide an offline version of your application.

+

One of the very nice conveniences of Yesod for this is that your templates are +all compiled into the executable, and thus do not need to be distributed with +your application. Static files do, however.

+ +

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 +doesn’t receive a ping for two minutes, it shuts down.

+
+
+

CGI on Apache

+

CGI and FastCGI work almost identically on Apache, so it should be fairly +straight-forward to port this configuration. You essentially need to accomplish +two goals:

+
    +
  1. +

    +Get the server to serve your file as (Fast)CGI. +

    +
  2. +
  3. +

    +Rewrite all requests to your site to go through the (Fast)CGI executable. +

    +
  4. +
+

Here is a configuration file for serving a blog application, with an executable +named "bloggy.cgi", living in a subfolder named "blog" of the document root. +This example was taken from an application living in the path +/f5/snoyman/public/blog.

+
Options +ExecCGI
+AddHandler cgi-script .cgi
+Options +FollowSymlinks
+
+RewriteEngine On
+RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1]
+RewriteCond $1 !^bloggy.cgi
+RewriteCond $1 !^static/
+RewriteRule ^(.*) bloggy.cgi/$1 [L]
+

The first RewriteRule is to deal with subfolders. In particular, it redirects a +request for /blog to /blog/. The first RewriteCond prevents directly +requesting the executable, the second allows Apache to serve the static files, +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." +when absent and serves static files from a separate domain. However, this +should serve to show the basics.

+

Here, "/home/michael/fastcgi" is the fastcgi application. The idea is to +rewrite all requests to start with "/app", and then serve everything beginning +with "/app" via the FastCGI executable.

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_fastcgi", "mod_rewrite")
+
+url.rewrite-once = (
+  "(.*)" => "/app/$1"
+)
+
+fastcgi.server = (
+    "/app" => ((
+        "socket" => "/tmp/test.fastcgi.socket",
+        "check-local" => "disable",
+        "bin-path" => "/home/michael/fastcgi", # full path to executable
+        "min-procs" => 1,
+        "max-procs" => 30,
+        "idle-timeout" => 30
+    ))
+)
+
+
+

CGI on lighttpd

+

This is basically the same as the FastCGI version, but tells lighttpd to run a +file ending in ".cgi" as a CGI executable. In this case, the file lives at +"/home/michael/myapp.cgi".

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_cgi", "mod_rewrite")
+
+url.rewrite-once = (
+    "(.*)" => "/myapp.cgi/$1"
+)
+
+cgi.assign = (".cgi" => "")
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/environment-variables.html b/public/book-1.4/environment-variables.html new file mode 100644 index 00000000..c83d7c5e --- /dev/null +++ b/public/book-1.4/environment-variables.html @@ -0,0 +1,172 @@ + Environment variables for configuration :: Yesod Web Framework Book- Version 1.4 +
+

Environment variables for configuration

+ + +

There’s a recent move, perhaps most prominently advocated by +the twelve-factor app, to store all app +configuration in environment variables, instead of using config files or +hard-coding them into the application source code (you don’t do that, right?).

+

Yesod’s scaffolding comes built in with some support for this, most +specifically for respecting the APPROOT environment variable to indicate how +URLs should be generated, the PORT environment variable for which port to +listen for requests on, and database connection settings. (Incidentally, this +ties in nicely with the Keter deployment +manager.)

+

The technique for doing this is quite easy: just do the environment variable +lookup in your main function. This example demonstrates this technique, along +with the slightly special handling necessary for setting the application root.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text          (Text, pack)
+import           System.Environment
+import           Yesod
+
+data App = App
+    { myApproot      :: Text
+    , welcomeMessage :: Text
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    approot = ApprootMaster myApproot
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    App {..} <- getYesod
+    setTitle "Environment variables"
+    [whamlet|
+        <p>Here's the welcome message: #{welcomeMessage}
+        <p>
+            <a href=@{HomeR}>And a link to: @{HomeR}
+    |]
+
+main :: IO ()
+main = do
+    myApproot <- fmap pack $ getEnv "APPROOT"
+    welcomeMessage <- fmap pack $ getEnv "WELCOME_MESSAGE"
+    warp 3000 App {..}
+

The only tricky things here are:

+
    +
  1. +

    +You need to convert the String value returned by getEnv into a Text by using pack. +

    +
  2. +
  3. +

    +We use the ApprootMaster constructor for approot, which says "apply this function to the foundation datatype to get the actual application root." +

    +
  4. +
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/forms.html b/public/book-1.4/forms.html new file mode 100644 index 00000000..4385aef0 --- /dev/null +++ b/public/book-1.4/forms.html @@ -0,0 +1,1128 @@ + Forms :: Yesod Web Framework Book- Version 1.4 +
+

Forms

+ + +

I’ve mentioned the boundary issue already: whenever data enters or leaves an +application, we need to validate it. Probably the most difficult place this +occurs is forms. Coding forms is complex; in an ideal world, we’d like a +solution that addresses the following problems:

+
    +
  • +

    +Ensure data is valid. +

    +
  • +
  • +

    +Marshal string data in the form submission to Haskell datatypes. +

    +
  • +
  • +

    +Generate HTML code for displaying the form. +

    +
  • +
  • +

    +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. +

    +
  • +
  • +

    +Automatically assign names to our fields that are guaranteed to be unique. +

    +
  • +
+

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 +Haskell’s type system to make sure everything is working correctly.

+
+

Synopsis

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative ((<$>), (<*>))
+import           Data.Text           (Text)
+import           Data.Time           (Day)
+import           Yesod
+import           Yesod.Form.Jquery
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/person PersonR POST
+|]
+
+instance Yesod App
+
+-- Tells our application to use the standard English messages.
+-- If you want i18n, then you can supply a translating function instead.
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- And tell us where to find the jQuery libraries. We'll just use the defaults,
+-- which point to the Google CDN.
+instance YesodJquery App
+
+-- The datatype we wish to receive from the form
+data Person = Person
+    { personName          :: Text
+    , personBirthday      :: Day
+    , personFavoriteColor :: Maybe Text
+    , personEmail         :: Text
+    , personWebsite       :: Maybe Text
+    }
+  deriving Show
+
+-- Declare the form. The type signature is a bit intimidating, but here's the
+-- overview:
+--
+-- * The Html parameter is used for encoding some extra information. See the
+-- discussion regarding runFormGet and runFormPost below for further
+-- explanation.
+--
+-- * We have our Handler as the inner monad, which indicates which site this is
+-- running in.
+--
+-- * FormResult can be in three states: FormMissing (no data available),
+-- FormFailure (invalid data) and FormSuccess
+--
+-- * The Widget is the viewable form to place into the web page.
+--
+-- Note that the scaffolded site provides a convenient Form type synonym,
+-- so that our signature could be written as:
+--
+-- > personForm :: Form Person
+--
+-- For our purposes, it's good to see the long version.
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm = renderDivs $ Person
+    <$> areq textField "Name" Nothing
+    <*> areq (jqueryDayField def
+        { jdsChangeYear = True -- give a year dropdown
+        , jdsYearRange = "1900:-5" -- 1900 till five years ago
+        }) "Birthday" Nothing
+    <*> aopt textField "Favorite color" Nothing
+    <*> areq emailField "Email address" Nothing
+    <*> aopt urlField "Website" Nothing
+
+-- The GET handler displays the form
+getHomeR :: Handler Html
+getHomeR = do
+    -- Generate the form to be displayed
+    (widget, enctype) <- generateFormPost personForm
+    defaultLayout
+        [whamlet|
+            <p>
+                The widget generated contains only the contents
+                of the form, not the form tag itself. So...
+            <form method=post action=@{PersonR} enctype=#{enctype}>
+                ^{widget}
+                <p>It also doesn't include the submit button.
+                <button>Submit
+        |]
+
+-- The POST handler processes the form. If it is successful, it displays the
+-- parsed person. Otherwise, it displays the form again with error messages.
+postPersonR :: Handler Html
+postPersonR = do
+    ((result, widget), enctype) <- runFormPost personForm
+    case result of
+        FormSuccess person -> defaultLayout [whamlet|<p>#{show person}|]
+        _ -> defaultLayout
+            [whamlet|
+                <p>Invalid input, let's try again.
+                <form method=post action=@{PersonR} enctype=#{enctype}>
+                    ^{widget}
+                    <button>Submit
+            |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Kinds of Forms

+

Before jumping into the types themselves, we should begin with an overview of +the different kinds of forms. There are three categories:

+
+
+Applicative +
+

+These are the most commonly used (it’s what appeared in the +synopsis). Applicative gives us some nice properties of letting error messages +coalesce together and keep a very high-level, declarative approach. (For more +information on applicative code, see +the Haskell +wiki.) +

+
+
+Monadic +
+

+A more powerful alternative to applicative. While this allows you +more flexibility, it does so at the cost of being more verbose. Useful if you +want to create forms that don’t fit into the standard two-column look. +

+
+
+Input +
+

+Used only for receiving input. Does not generate any HTML for receiving +the user input. Useful for interacting with existing forms. +

+
+
+

In addition, there are a number of different variables that come into play for +each form and field you will want to set up:

+
    +
  • +

    +Is the field required or optional? +

    +
  • +
  • +

    +Should it be submitted with GET or POST? +

    +
  • +
  • +

    +Does it have a default value, or not? +

    +
  • +
+

An overriding goal is to minimize the number of field definitions and let them +work in as many contexts as possible. One result of this is that we end up with +a few extra words for each field. In the synopsis, you may have noticed things +like areq and that extra Nothing parameter. We’ll cover why all of those +exist in the course of this chapter, but for now realize that by making these +parameters explicit, we are able to reuse the individuals fields (like +intField) in many different ways.

+

A quick note on naming conventions. Each form type has a one-letter prefix (A, +M and I) which is used in a few places, such as saying MForm. We also use req +and opt to mean required and optional. Combining these, we create a required +applicative field with areq, or an optional input field with iopt.

+
+
+

Types

+

The Yesod.Form.Types module declares a few types. We won’t cover all the types +available, but will instead focus on the most crucial. Let’s start with some of +the simple ones:

+
+
+Enctype +
+

+The encoding type, either UrlEncoded or Multipart. This datatype +declares an instance of ToHtml, so you can use the enctype directly in +Hamlet. +

+
+
+FormResult +
+

+Has one of three possible states: FormMissing if no data was +submitted, FormFailure if there was an error parsing the form (e.g., missing +a required field, invalid content), or FormSuccess if everything went +smoothly. +

+
+
+FormMessage +
+

+Represents all of the different messages that can be generated as +a data type. For example, MsgInvalidInteger is used by the library to +indicate that the textual value provided is not an integer. By keeping this +data highly structured, you are able to provide any kind of rendering function +you want, which allows for internationalization (i18n) of your application. +

+
+
+

Next we have some datatypes used for defining individual fields. We define a +field as a single piece of information, such as a number, a string, or an email +address. Fields are combined together to build forms.

+
+
+Field +
+

+Defines two pieces of functionality: how to parse the text input from a +user into a Haskell value, and how to create the widget to be displayed to the +user. yesod-form defines a number of individual Fields in Yesod.Form.Fields. +

+
+
+FieldSettings +
+

+Basic information on how a field should be displayed, such as +the display name, an optional tooltip, and possibly hardcoded id and name +attributes. (If none are provided, they are automatically generated.) Note that +FieldSettings provides an IsString instance, so when you need to provide a +FieldSettings value, you can actually type in a literal string. That’s how we +interacted with it in the synopsis. +

+
+
+

And finally, we get to the important stuff: the forms themselves. There are +three types for this: MForm is for monadic forms, AForm for applicative and +FormInput for input. MForm is actually a type synonym for a +monad stack that provides the following features:

+
    +
  • +

    +A Reader monad giving us the parameters submitted by the user, the + foundation datatype and the list of languages the user supports. The last two + are used for rendering of the FormMessages to support i18n (more on this + later). +

    +
  • +
  • +

    +A Writer monad keeping track of the Enctype. A form will always be + UrlEncoded, unless there is a file input field, which will force us to use + multipart instead. +

    +
  • +
  • +

    +A State monad keeping track of generated names and identifiers for fields. +

    +
  • +
+

An AForm is pretty similar. However, there are a few major differences:

+
    +
  • +

    +It produces a list of FieldViews, which are used for tracking what we + will display to the user. This allows us to keep an abstract idea of the form + display, and then at the end of the day choose an appropriate function for + laying it out on the page. In the synopsis, we used renderDivs, which + creates a bunch of div tags. Two other options are renderBootstrap and + renderTable. +

    +
  • +
  • +

    +It does not provide a Monad instance. The goal of Applicative is to allow + the entire form to run, grab as much information on each field as possible, + and then create the final result. This cannot work in the context of Monad. +

    +
  • +
+

A FormInput is even simpler: it returns either a list of error messages or a +result.

+
+
+

Converting

+

“But wait a minute,” you say. “You said the synopsis uses applicative forms, +but I’m sure the type signature said MForm. Shouldn’t it be Monadic?” That’s +true, the final form we produced was monadic. But what really happened is that +we converted an applicative form to a monadic one.

+

Again, our goal is to reuse code as much as possible, and minimize the number +of functions in the API. And Monadic forms are more powerful than Applicative, +if a bit clumsy, so anything that can be expressed in an Applicative form could +also be expressed in a Monadic form. There are two core functions that help out +with this: aformToForm converts any applicative form to a monadic one, and +formToAForm converts certain kinds of monadic forms to applicative forms.

+

“But wait another minute,” you insist. “I didn’t see any aformToForm!” +Also true. The renderDivs function takes care of that for us.

+
+
+

Create AForms

+

Now that I’ve (hopefully) convinced you that in our synopsis we were really +dealing with applicative forms, let’s have a look and try to understand how +these things get created. Let’s take a simple example:

+
data Car = Car
+    { carModel :: Text
+    , carYear  :: Int
+    }
+  deriving Show
+
+carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable carAForm
+

Here, we’ve explicitly split up applicative and monadic forms. In carAForm, +we use the <$> and <*> operators. This should not be surprising; these are +almost always used in applicative-style code. And we have one line for each +record in our Car datatype. Perhaps also unsurprisingly, we have a +textField for the Text record, and an intField for the Int record.

+

Let’s look a bit more closely at the areq function. Its (simplified) type +signature is Field a → FieldSettings → Maybe a → AForm a. That +first argument specifies the datatype of this field, how to parse +it, and how to render it. The next argument, FieldSettings, tells us the +label, tooltip, name and ID of the field. In this case, we’re using the +previously-mentioned IsString instance of FieldSettings.

+

And what’s up with that Maybe a? It provides the optional default value. For +example, if we want our form to fill in "2007" as the default car year, we +would use areq intField "Year" (Just 2007). We can even take this to the next +level, and have a form that takes an optional parameter giving the default +values.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+
+

Optional fields

+

Suppose we wanted to have an optional field (like the car color). All we do +instead is use the aopt function.

+
carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+    <*> aopt textField "Color" Nothing
+

And like required fields, the last argument is the optional default value. +However, this has two layers of Maybe wrapping. This is actually a bit +redundant, but it makes it much easier to write code that takes an optional +default form parameter, such as in the next example.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+    <*> aopt textField "Color" (carColor <$> mcar)
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable $ carAForm $ Just $ Car "Forte" 2010 $ Just "gray"
+
+
+
+

Validation

+

How would we make our form only accept cars created after 1990? If you +remember, we said above that the Field itself contained the information on +what is a valid entry. So all we need to do is write a new Field, right? +Well, that would be a bit tedious. Instead, let’s just modify an existing one:

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField    "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year"  (carYear  <$> mcar)
+    <*> aopt textField    "Color" (carColor <$> mcar)
+  where
+    errorMessage :: Text
+    errorMessage = "Your car is too old, get a new one!"
+
+    carYearField = check validateYear intField
+
+    validateYear y
+        | y < 1990 = Left errorMessage
+        | otherwise = Right y
+

The trick here is the check function. It takes a function (validateYear) +that returns either an error message or a modified field value. In this +example, we haven’t modified the value at all. That is usually going to be the +case. This kind of checking is very common, so we have a shortcut:

+
carYearField = checkBool (>= 1990) errorMessage intField
+

checkBool takes two parameters: a condition that must be fulfilled, and an +error message to be displayed if it was not.

+ +

It’s great to make sure the car isn’t too old. But what if we want to make sure +that the year specified is not from the future? In order to look up the current +year, we’ll need to run some IO. For such circumstances, we’ll need checkM, +which allows our validation code to perform arbitrary actions:

+
    carYearField = checkM inPast $ checkBool (>= 1990) errorMessage intField
+
+    inPast y = do
+        thisYear <- liftIO getCurrentYear
+        return $ if y <= thisYear
+            then Right y
+            else Left ("You have a time machine!" :: Text)
+
+getCurrentYear :: IO Int
+getCurrentYear = do
+    now <- getCurrentTime
+    let today = utctDay now
+    let (year, _, _) = toGregorian today
+    return $ fromInteger year
+

inPast is a function that will return an Either result in the Handler +monad. We use liftIO getCurrentYear to get the current year and then compare +it against the user-supplied year. Also, notice how we can chain together +multiple validators.

+ +
+
+

More sophisticated fields

+

Our color entry field is nice, but it’s not exactly user-friendly. What we +really want is a drop-down list.

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectFieldList colors) "Color" (carColor <$> mcar)
+  where
+    colors :: [(Text, Color)]
+    colors = [("Red", Red), ("Blue", Blue), ("Gray", Gray), ("Black", Black)]
+

selectFieldList takes a list of pairs. The first item in the pair is the text displayed to the user in the drop-down list, and the second item is the actual Haskell value. Of course, the code above looks really repetitive; we can get the same result using the Enum and Bounded instance GHC automatically derives for us.

+
colors = map (pack . show &&& id) [minBound..maxBound]
+

[minBound..maxBound] gives us a list of all the different Color values. We +then apply a map and &&& (a.k.a, the fan-out operator) to turn that into a +list of pairs. And even this can be simplified by using the optionsEnum +function provided by yesod-form, which would turn out original code into:

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectField optionsEnum) "Color" (carColor <$> mcar)
+

Some people prefer radio buttons to drop-down lists. Fortunately, this is just a one-word change.

+
carAForm = Car
+    <$> areq textField                    "Model" Nothing
+    <*> areq intField                     "Year"  Nothing
+    <*> aopt (radioField optionsEnum) "Color" Nothing
+
+
+

Running forms

+

At some point, we’re going to need to take our beautiful forms and produce some +results. There are a number of different functions available for this, each +with its own purpose. I’ll go through them, starting with the most common.

+
+
+runFormPost +
+

+This will run your form against any submitted POST parameters. +If this is not a POST submission, it will return a FormMissing. This +automatically inserts a security token as a hidden form field to avoid +cross-site request +forgery (CSRF) attacks. +

+
+
+runFormGet +
+

+The equivalent of runFormPost for GET parameters. In order to +distinguish a normal GET page load from a GET submission, it includes an +extra _hasdata hidden field in the form. Unlike runFormPost, it does +not include CSRF protection. +

+
+
+runFormPostNoToken +
+

+Same as runFormPost, but does not include (or require) +the CSRF security token. +

+
+
+generateFormPost +
+

+Instead of binding to existing POST parameters, acts as if +there are none. This can be useful when you want to generate a new form after a +previous form was submitted, such as in a wizard. +

+
+
+generateFormGet +
+

+Same as generateFormPost, but for GET. +

+
+
+

The return type from the first three is ((FormResult a, Widget), Enctype). +The Widget will already have any validation errors and previously submitted +values.

+ +
+
+

i18n

+

There have been a few references to i18n in this chapter. The topic will get +more thorough coverage in its own chapter, but since it has such a profound +effect on yesod-form, I wanted to give a brief overview. The idea behind i18n +in Yesod is to have data types represent messages. Each site can have an +instance of RenderMessage for a given datatype which will translate that +message based on a list of languages the user accepts. As a result of all this, +there are a few things you should be aware of:

+
    +
  • +

    +There is an automatic instance of RenderMessage for Text in every site, + so you can just use plain strings if you don’t care about i18n support. + However, you may need to use explicit type signatures occasionally. +

    +
  • +
  • +

    +yesod-form expresses all of its messages in terms of the FormMessage datatype. Therefore, to use yesod-form, you’ll need to have an appropriate RenderMessage instance. A simple one that uses the default English translations would be: +

    +
  • +
+
instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

This is provided automatically by the scaffolded site.

+
+
+

Monadic Forms

+

Often times, a simple form layout is adequate, and applicative forms excel at +this approach. Sometimes, however, you’ll want to have a more customized look +to your form.

+

A non-standard form layout

+ + + + + + +
+

For these use cases, monadic forms fit the bill. They are a bit more verbose +than their applicative cousins, but this verbosity allows you to have complete +control over what the form will look like. In order to generate the form above, +we could code something like this.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm extra = do
+    (nameRes, nameView) <- mreq textField "this is not used" Nothing
+    (ageRes, ageView) <- mreq intField "neither is this" Nothing
+    let personRes = Person <$> nameRes <*> ageRes
+    let widget = do
+            toWidget
+                [lucius|
+                    ##{fvId ageView} {
+                        width: 3em;
+                    }
+                |]
+            [whamlet|
+                #{extra}
+                <p>
+                    Hello, my name is #
+                    ^{fvInput nameView}
+                    \ and I am #
+                    ^{fvInput ageView}
+                    \ years old. #
+                    <input type=submit value="Introduce myself">
+            |]
+    return (personRes, widget)
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet personForm
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+        |]
+
+main :: IO ()
+main = warp 3000 App
+

Similar to the applicative areq, we use mreq for monadic forms. (And yes, +there’s also mopt for optional fields.) But there’s a big difference: mreq +gives us back a pair of values. Instead of hiding away the FieldView value and +automatically inserting it into a widget, we have the ability to insert it as +we see fit.

+

FieldView has a number of pieces of information. The most important is +fvInput, which is the actual form field. In this example, we also use fvId, +which gives us back the HTML id attribute of the input tag. In our example, +we use that to specify the width of the field.

+

You might be wondering what the story is with the “this is not used” and +“neither is this” values. mreq takes a FieldSettings as its second +argument. Since FieldSettings provides an IsString instance, the strings +are essentially expanded by the compiler to:

+
fromString "this is not used" == FieldSettings
+    { fsLabel = "this is not used"
+    , fsTooltip = Nothing
+    , fsId = Nothing
+    , fsName = Nothing
+    , fsAttrs = []
+    }
+

In the case of applicative forms, the fsLabel and fsTooltip values are used +when constructing your HTML. In the case of monadic forms, Yesod does not +generate any of the “wrapper” HTML for you, and therefore these values are +ignored. However, we still keep the FieldSettings parameter to allow you to +override the id and name attributes of your fields if desired.

+

The other interesting bit is the extra value. GET forms include an extra +field to indicate that they have been submitted, and POST forms include a +security token to prevent CSRF attacks. If you don’t include this extra hidden +field in your form, the form submission will fail.

+

Other than that, things are pretty straight-forward. We create our personRes +value by combining together the nameRes and ageRes values, and then return +a tuple of the person and the widget. And in the getHomeR function, +everything looks just like an applicative form. In fact, you could swap out our +monadic form with an applicative one and the code would still work.

+
+
+

Input forms

+

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 +forms.

+

These work mostly the same as applicative and monadic forms, with some differences:

+
    +
  • +

    +You use runInputPost and runInputGet. +

    +
  • +
  • +

    +You use ireq and iopt. These functions now only take two arguments: the + field type and the name (i.e., HTML name attribute) of the field in + question. +

    +
  • +
  • +

    +After running a form, it returns the value. It doesn’t return a widget or an + encoding type. +

    +
  • +
  • +

    +If there are any validation errors, the page returns an "invalid arguments" + error page. +

    +
  • +
+

You can use input forms to recreate the previous example. Note, however, that +the input version is less user friendly. If you make a mistake in an +applicative or monadic form, you will be brought back to the same page, with +your previously entered values in the form, and an error message explaining what +you need to correct. With input forms, the user simply gets an error message.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/input InputR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form action=@{InputR}>
+            <p>
+                My name is
+                <input type=text name=name>
+                and I am
+                <input type=text name=age>
+                years old.
+                <input type=submit value="Introduce myself">
+    |]
+
+getInputR :: Handler Html
+getInputR = do
+    person <- runInputGet $ Person
+                <$> ireq textField "name"
+                <*> ireq intField "age"
+    defaultLayout [whamlet|<p>#{show person}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Custom fields

+

The fields that come built-in with Yesod will likely cover the vast majority of +your form needs. But occasionally, you’ll need something more specialized. +Fortunately, you can create new fields in Yesod yourself. The Field constructor +has three values: fieldParse takes a list of values submitted by the user and +returns one of three results:

+
    +
  • +

    +An error message saying validation failed. +

    +
  • +
  • +

    +The parsed value. +

    +
  • +
  • +

    +Nothing, indicating that no data was supplied. +

    +
  • +
+

That last case might sound surprising. It would seem that Yesod can +automatically know that no information is supplied when the input list is +empty. But in reality, for some field types, the lack of any input is actually +valid input. Checkboxes, for instance, indicate an unchecked state by sending +in an empty list.

+

Also, what’s up with the list? Shouldn’t it be a Maybe? That’s also not the +case. With grouped checkboxes and multi-select lists, you’ll have multiple +widgets with the same name. We also use this trick in our example below.

+

The second value in the constructor is fieldView, and it renders a widget to display to the +user. This function has the following arguments:

+
    +
  1. +

    +The id attribute. +

    +
  2. +
  3. +

    +The name attribute. +

    +
  4. +
  5. +

    +Any other arbitrary attributes. +

    +
  6. +
  7. +

    +The result, given as an Either value. This will provide either the unparsed +input (when parsing failed) or the successfully parsed value. intField is a +great example of how this works. If you type in 42, the value of result +will be Right 42. But if you type in turtle, the result will be Left +"turtle". This lets you put in a value attribute on your input tag that will +give the user a consistent experience. +

    +
  8. +
  9. +

    +A Bool indicating if the field is required. +

    +
  10. +
+

The final value in the constructor is fieldEnctype. If you’re dealing with +file uploads, this should be Multipart; otherwise, it should be UrlEncoded.

+

As a small example, let’s create a new field type that is a password confirm +field. This field has two text inputs- both with the same name attribute- and +returns an error message if the values don’t match. Note that, unlike most +fields, it does not provide a value attribute on the input tags, as you don’t +want to send back a user-entered password in your HTML ever.

+
passwordConfirmField :: Field Handler Text
+passwordConfirmField = Field
+    { fieldParse = \rawVals _fileVals ->
+        case rawVals of
+            [a, b]
+                | a == b -> return $ Right $ Just a
+                | otherwise -> return $ Left "Passwords don't match"
+            [] -> return $ Right Nothing
+            _ -> return $ Left "You must enter two values"
+    , fieldView = \idAttr nameAttr otherAttrs eResult isReq ->
+        [whamlet|
+            <input id=#{idAttr} name=#{nameAttr} *{otherAttrs} type=password>
+            <div>Confirm:
+            <input id=#{idAttr}-confirm name=#{nameAttr} *{otherAttrs} type=password>
+        |]
+    , fieldEnctype = UrlEncoded
+    }
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet $ renderDivs
+        $ areq passwordConfirmField "Password" Nothing
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+                <input type=submit value="Change password">
+        |]
+
+
+

Values that don’t come from the user

+

Imagine you’re writing a blog hosting web app, and you want to have a form for +users to enter a blog post. A blog post will consist of four pieces of +information:

+
    +
  • +

    +Title +

    +
  • +
  • +

    +HTML contents +

    +
  • +
  • +

    +User ID of the author +

    +
  • +
  • +

    +Publication date +

    +
  • +
+

We want the user to enter the first two values, but not the second two. User ID +should be determined automatically by authenticating the user (a topic we +haven’t covered yet), and the publication date should just be the current time. +The question is, how do we keep our simple applicative form syntax, and yet +pull in values that don’t come from the user?

+

The answer is two separate helper functions:

+
    +
  • +

    +pure allows us to wrap up a plain value as an applicative form value. +

    +
  • +
  • +

    +lift allows us to run arbitrary Handler actions inside an applicative form. +

    +
  • +
+

Let’s see an example of using these two functions:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Data.Time
+import           Yesod
+
+-- In the authentication chapter, we'll address this properly
+newtype UserId = UserId Int
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+type Form a = Html -> MForm Handler (FormResult a, Widget)
+
+data Blog = Blog
+    { blogTitle    :: Text
+    , blogContents :: Textarea
+    , blogUser     :: UserId
+    , blogPosted   :: UTCTime
+    }
+    deriving Show
+
+form :: UserId -> Form Blog
+form userId = renderDivs $ Blog
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+    <*> pure userId
+    <*> lift (liftIO getCurrentTime)
+
+getHomeR :: Handler Html
+getHomeR = do
+    let userId = UserId 5 -- again, see the authentication chapter
+    ((res, widget), enctype) <- runFormPost $ form userId
+    defaultLayout
+        [whamlet|
+            <p>Previous result: #{show res}
+            <form method=post action=@{HomeR} enctype=#{enctype}>
+                ^{widget}
+                <input type=submit>
+        |]
+
+postHomeR :: Handler Html
+postHomeR = getHomeR
+
+main :: IO ()
+main = warp 3000 App
+

One trick we’ve introduced here is using the same handler code for both the +GET and POST request methods. This is enabled by the implementation of +runFormPost, which will behave exactly like generateFormPost in the case of +a GET request. Using the same handler for both request methods cuts down on +some boilerplate.

+
+
+

Summary

+

Forms in Yesod are broken up into three groups. Applicative is the most common, +as it provides a nice user interface with an easy-to-use API. Monadic forms +give you more power, but are harder to use. Input forms are intended when you +just want to read data from the user, not generate the input widgets.

+

There are a number of different Fields provided by Yesod out-of-the-box. In +order to use these in your forms, you need to indicate the kind of form and +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 +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.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/haskell.html b/public/book-1.4/haskell.html new file mode 100644 index 00000000..3bca2c8d --- /dev/null +++ b/public/book-1.4/haskell.html @@ -0,0 +1,458 @@ + Haskell :: Yesod Web Framework Book- Version 1.4 +
+

Haskell

+ + +

Haskell is a powerful, fast, type-safe, functional programming language. This +book takes as an assumption that you are already familiar with most of the +basics of Haskell. There are two wonderful books for learning Haskell, both of +which are available for reading online:

+ +

Additionally, there are a number of great articles on +School of Haskell.

+

In order to use Yesod, you’re going to have to know at least the basics of +Haskell. Additionally, Yesod uses some features of Haskell that aren’t covered +in most introductory texts. While this book assumes the reader has a basic +familiarity with Haskell, this chapter is intended to fill in the gaps.

+

If you are already fluent in Haskell, feel free to completely skip this +chapter. Also, if you would prefer to start off by getting your feet wet with +Yesod, you can always come back to this chapter later as a reference.

+
+

Terminology

+

Even for those familiar with Haskell as a language, there can sometimes be some +confusion about terminology. Let’s establish some base terms that we can use +throughout this book.

+
+
+Data type +
+

+This is one of the core building blocks for a strongly typed +language like Haskell. Some data types, like Int, can be treated as primitive +values, while other data types will build on top of these to create more +complicated values. For example, you might represent a person with: +

+
data Person = Person Text Int
+

Here, the Text would give the person’s name, and the Int would give the +person’s age. Due to its simplicity, this specific example type will recur +throughout the book. There are essentially three ways you can create a new data +type:

+
    +
  • +

    +A type declaration such as type GearCount = Int merely creates a + synonym for an existing type. The type system will do nothing to prevent + you from using an Int where you asked for a GearCount. Using this can + make your code more self-documenting. +

    +
  • +
  • +

    +A newtype declaration such as newtype Make = Make Text. In this case, + you cannot accidentally use a Text in place of a Make; the compiler + will stop you. The newtype wrapper always disappears during compilation, + and will introduce no overhead. +

    +
  • +
  • +

    +A data declaration, such as Person above. You can also create + Algebraic Data Types (ADTs), such as data Vehicle = Bicycle GearCount | + Car Make Model. +

    +
  • +
+
+
+Data constructor +
+

+In our examples above, Person, Make, Bicycle, and +Car are all data constructors. +

+
+
+Type constructor +
+

+In our examples above, Person, Make, and Vehicle are +all type constructors. +

+
+
+Type variables +
+

+Consider the data type data Maybe a = Just a | Nothing. In +this case, a is a type variable. +

+
+
+ +
+
+

Tools

+

Since July 2015, the tooling recommendation for Yesod has become very simple: +use stack. stack is a +complete build tool for Haskell which deals with your compiler (Glasgow Haskell +Compiler, aka GHC), libraries (including Yesod), additional build tools (like +alex and happy), and much more. There are other build tools available in +Haskell, and most of them support Yesod quite well. But for the easiest +experience, it’s strongly recommended to stick with stack. The Yesod website +keeps an up-to-date quick start +guide, which provides instructions on installing stack and getting started +with a new scaffolded site.

+

Once you have your toolchain set up correctly, you’ll need to install a number +of Haskell libraries. For the vast majority of the book, the following command +will install all the libraries you need:

+
stack build yesod persistent-sqlite yesod-static esqueleto
+

In order to run an example from the book, save it in a file, e.g., +yesod-example.hs, and then run it with:

+
stack runghc yesod-example.hs
+
+
+

Language Pragmas

+

GHC will run by default in something very close to Haskell98 mode. It also +ships with a large number of language extensions, allowing more powerful type +classes, syntax changes, and more. There are multiple ways to tell GHC to turn +on these extensions. For most of the code snippets in this book, you’ll see +language pragmas, which look like this:

+
{-# LANGUAGE MyLanguageExtension #-}
+

These should always appear at the top of your source file. Additionally, there +are two other common approaches:

+
    +
  • +

    +On the GHC command line, pass an extra argument -XMyLanguageExtension. +

    +
  • +
  • +

    +In your cabal file, add an default-extensions block. +

    +
  • +
+

I personally never use the GHC command line argument approach. It’s a personal +preference, but I like to have my settings clearly stated in a file. In general +it’s recommended to avoid putting extensions in your cabal file; however, +this rule mostly applies when writing publicly available libraries. When you’re +writing an application that you and your team will be working on, having all of +your language extensions defined in a single location makes a lot of sense. +The Yesod scaffolded site specifically uses this approach to avoid the +boilerplate of specifying the same language pragmas in every source file.

+

We’ll end up using quite a few language extensions in this book (at the time of +writing, the scaffolding uses 13). We will not cover the meaning of all of +them. Instead, please see the +GHC +documentation.

+
+
+

Overloaded Strings

+

What’s the type of "hello"? Traditionally, it’s String, which is defined as +type String = [Char]. Unfortunately, there are a number of limitations with +this:

+
    +
  • +

    +It’s a very inefficient implementation of textual data. We need to allocate + extra memory for each cons cell, plus the characters themselves each take up + a full machine word. +

    +
  • +
  • +

    +Sometimes we have string-like data that’s not actually text, such as + ByteStrings and HTML. +

    +
  • +
+

To work around these limitations, GHC has a language extension called +OverloadedStrings. When enabled, literal strings no longer have the +monomorphic type String; instead, they have the type IsString a ⇒ a, +where IsString is defined as:

+
class IsString a where
+    fromString :: String -> a
+

There are IsString instances available for a number of types in Haskell, such +as Text (a much more efficient packed String type), ByteString, and +Html. Virtually every example in this book will assume that this language +extension is turned on.

+

Unfortunately, there is one drawback to this extension: it can sometimes +confuse GHC’s type checker. Imagine we have:

+
{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}
+import Data.Text (Text)
+
+class DoSomething a where
+    something :: a -> IO ()
+
+instance DoSomething String where
+    something _ = putStrLn "String"
+
+instance DoSomething Text where
+    something _ = putStrLn "Text"
+
+myFunc :: IO ()
+myFunc = something "hello"
+

Will the program print out String or Text? It’s not clear. So instead, +you’ll need to give an explicit type annotation to specify whether "hello" +should be treated as a String or Text.

+ +
+
+

Type Families

+

The basic idea of a type family is to state some association between two +different types. Suppose we want to write a function that will safely take the +first element of a list. But we don’t want it to work just on lists; we’d like +it to treat a ByteString like a list of Word8s. To do so, we need to +introduce some associated type to specify what the contents of a certain type +are.

+
{-# LANGUAGE TypeFamilies, OverloadedStrings #-}
+import Data.Word (Word8)
+import qualified Data.ByteString as S
+import Data.ByteString.Char8 () -- get an orphan IsString instance
+
+class SafeHead a where
+    type Content a
+    safeHead :: a -> Maybe (Content a)
+
+instance SafeHead [a] where
+    type Content [a] = a
+    safeHead [] = Nothing
+    safeHead (x:_) = Just x
+
+instance SafeHead S.ByteString where
+    type Content S.ByteString = Word8
+    safeHead bs
+        | S.null bs = Nothing
+        | otherwise = Just $ S.head bs
+
+main :: IO ()
+main = do
+    print $ safeHead ("" :: String)
+    print $ safeHead ("hello" :: String)
+
+    print $ safeHead ("" :: S.ByteString)
+    print $ safeHead ("hello" :: S.ByteString)
+

The new syntax is the ability to place a type inside of a class and +instance. We can also use data instead, which will create a new datatype +instead of reference an existing one.

+ +
+
+

Template Haskell

+

Template Haskell (TH) is an approach to code generation. We use it in Yesod +in a number of places to reduce boilerplate, and to ensure that the generated +code is correct. Template Haskell is essentially Haskell which generates a +Haskell Abstract Syntax Tree (AST).

+ +

Writing TH code can be tricky, and unfortunately there isn’t very much type +safety involved. You can easily write TH that will generate code that won’t +compile. This is only an issue for the developers of Yesod, not for its users. +During development, we use a large collection of unit tests to ensure that the +generated code is correct. As a user, all you need to do is call these already +existing functions. For example, to include an externally defined Hamlet +template, you can write:

+
$(hamletFile "myfile.hamlet")
+

(Hamlet is discussed in the Shakespeare chapter.) The dollar sign immediately +followed by parentheses tell GHC that what follows is a Template Haskell +function. The code inside is then run by the compiler and generates a Haskell +AST, which is then compiled. And yes, it’s even possible to +go meta +with this.

+

A nice trick is that TH code is allowed to perform arbitrary IO actions, and +therefore we can place some input in external files and have it parsed at +compile time. One example usage is to have compile-time checked HTML, CSS, and +Javascript templates.

+

If your Template Haskell code is being used to generate declarations, and is +being placed at the top level of our file, we can leave off the dollar sign and +parentheses. In other words:

+
{-# LANGUAGE TemplateHaskell #-}
+
+-- Normal function declaration, nothing special
+myFunction = ...
+
+-- Include some TH code
+$(myThCode)
+
+-- Or equivalently
+myThCode
+

It can be useful to see what code is being generated by Template Haskell for +you. To do so, you should use the -ddump-splices GHC option.

+ +

Template Haskell introduces something called the stage +restriction, which essentially means that code before a Template Haskell splice +cannot refer to code in the Template Haskell, or what follows. This will +sometimes require you to rearrange your code a bit. The same restriction +applies to QuasiQuotes.

+

While out of the box, Yesod is really geared for using code generation to avoid +boilerplate, it’s perfectly acceptable to use Yesod in a Template Haskell-free +way. There’s more information on that in the "Yesod for Haskellers" chapter.

+
+
+

QuasiQuotes

+

QuasiQuotes (QQ) are a minor extension of Template Haskell that let us embed +arbitrary content within our Haskell source files. For example, we mentioned +previously the hamletFile TH function, which reads the template contents from +an external file. We also have a quasi-quoter named hamlet that takes the +content inline:

+
{-# LANGUAGE QuasiQuotes #-}
+
+[hamlet|<p>This is quasi-quoted Hamlet.|]
+

The syntax is set off using square brackets and pipes. The name of the +quasi-quoter is given between the opening bracket and the first pipe, and the +content is given between the pipes.

+

Throughout the book, we will often times use the QQ-approach over a TH-powered +external file since the former is simpler to copy-and-paste. However, in +production, external files are recommended for all but the shortest of inputs +as it gives a nice separation of the non-Haskell syntax from your Haskell code.

+
+
+

API Documentation

+

The standard API documentation program in Haskell is called Haddock. The +standard Haddock search tool is called Hoogle. My recommendation is to use +FP Complete’s Hoogle search and its +accompanying Haddocks for searching and browsing documentation. The reason for +this is that the FP Complete Hoogle database covers a very large number of open +source Haskell packages, and the documentation provided is always fully +generated and known to link to other working Haddocks.

+

The more commonly used sources for these are +Hackage itself, and +haskell.org’s Hoogle instance. The +downsides to these are that- based on build issues on the server- documentation +is sometimes not generated, and the Hoogle search defaults to searching only a +subset of available packages. Most importantly for us, Yesod is indexed by FP +Complete’s Hoogle, but not by haskell.org’s.

+

If when reading this book you run into types or functions that you do not +understand, try doing a Hoogle search with FP Complete’s Hoogle to get more +information.

+
+
+

Summary

+

You don’t need to be an expert in Haskell to use Yesod, a basic familiarity +will suffice. This chapter hopefully gave you just enough extra information to +feel more comfortable following the rest of the book.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/http-conduit.html b/public/book-1.4/http-conduit.html new file mode 100644 index 00000000..018f2ff1 --- /dev/null +++ b/public/book-1.4/http-conduit.html @@ -0,0 +1,112 @@ + http-conduit :: Yesod Web Framework Book- Version 1.4 +
+ +
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/initializing-foundation-data.html b/public/book-1.4/initializing-foundation-data.html new file mode 100644 index 00000000..c16e8ad9 --- /dev/null +++ b/public/book-1.4/initializing-foundation-data.html @@ -0,0 +1,271 @@ + Initializing data in the foundation datatype :: Yesod Web Framework Book- Version 1.4 +
+

Initializing data in the foundation datatype

+ + +

This example is meant to demonstrate a relatively simple concept: performing +some initialization of data to be kept in the foundation datatype. There are +various reasons to do this, though the two most important are:

+
    +
  • +

    +Efficiency: by initializing data once, at process startup, you can avoid + having to recompute the same value in each request. +

    +
  • +
  • +

    +Persistence: we want to store some information in a mutable location which + will be persisted between individual requests. Often times, this is done via + an external database, but it can also be done via an in-memory mutable + variable. +

    +
  • +
+ +

To demonstrate, we’ll implement a very simple website. It will contain a single +route, and will serve content stored in a Markdown file. In addition to serving +that content, we’ll also display an old-school visitor counter indicating how +many visitors have been to the site.

+
+

Step 1: define your foundation

+

We’ve identified two pieces of information to be initialized: the Markdown +content to be display on the homepage, and a mutable variable holding the +visitor count. Remember that our goal is to perform as much of the work in the +initialization phase as possible and thereby avoid performing the same work in +the handlers themselves. Therefore, we want to preprocess the Markdown content +into HTML. As for the visitor count, a simple IORef should be sufficient. So +our foundation data type is:

+
data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+
+

Step 2: use the foundation

+

For this trivial example, we only have one route: the homepage. All we need to do is:

+
    +
  1. +

    +Increment the visitor count. +

    +
  2. +
  3. +

    +Get the new visitor count. +

    +
  4. +
  5. +

    +Display the Markdown content together with the visitor count. +

    +
  6. +
+

One trick we’ll use to make the code a bit shorter is to utilize record +wildcard syntax: App {..}. This is convenient when we want to deal with a +number of different fields in a datatype.

+
getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+
+

Step 3: create the foundation value

+

When we initialize our application, we’ll now need to provide values for the +two fields we described above. This is normal IO code, and can perform any +arbitrary actions needed. In our case, we need to:

+
    +
  1. +

    +Read the Markdown from the file. +

    +
  2. +
  3. +

    +Convert that Markdown to HTML. +

    +
  4. +
  5. +

    +Create the visitor counter variable. +

    +
  6. +
+

The code ends up being just as simple as those steps imply:

+
go :: IO ()
+go = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+

Conclusion

+

There’s no rocket science involved in this example, just very straightforward +programming. The purpose of this chapter is to demonstrate the commonly used +best practice for achieving these often needed objectives. In your own +applications, the initialization steps will likely be much more complicated: +setting up database connection pools, starting background jobs to batch process +large data, or anything else. After reading this chapter, you should now have a +good idea of where to place your application-specific initialization code.

+

Below is the full source code for the example described above:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import qualified Data.Text.Lazy.IO as TLIO
+import           Text.Markdown
+import           Yesod
+
+data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+main :: IO ()
+main = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/internationalization.html b/public/book-1.4/internationalization.html new file mode 100644 index 00000000..21a99abc --- /dev/null +++ b/public/book-1.4/internationalization.html @@ -0,0 +1,411 @@ + Internationalization :: Yesod Web Framework Book- Version 1.4 +
+

Internationalization

+ + +

Users expect our software to speak their language. Unfortunately for us, there +will likely be more than one language involved. While doing simple string +replacement isn’t too involved, correctly dealing with all the grammar issues +can be tricky. After all, who wants to see "List 1 file(s)" from a program +output?

+

But a real i18n solution needs to do more than just provide a means of +achieving the correct output. It needs to make this process easy for both the +programmer and the translator and relatively error-proof. Yesod’s answer to the +problem gives you:

+
    +
  • +

    +Intelligent guessing of the user’s desired language based on request headers, + with the ability to override. +

    +
  • +
  • +

    +A simple syntax for giving translations which requires no Haskell knowledge. + (After all, most translators aren’t programmers.) +

    +
  • +
  • +

    +The ability to bring in the full power of Haskell for tricky grammar issues + as necessary, along with a default selection of helper functions to cover + most needs. +

    +
  • +
  • +

    +Absolutely no issues at all with word order. +

    +
  • +
+
+

Synopsis

+
-- @messages/en.msg
+Hello: Hello
+EnterItemCount: I would like to buy:
+Purchase: Purchase
+ItemCount count@Int: You have purchased #{showInt count} #{plural count "item" "items"}.
+SwitchLanguage: Switch language to:
+Switch: Switch
+
-- @messages/he.msg
+Hello: שלום
+EnterItemCount: אני רוצה לקנות:
+Purchase: קנה
+ItemCount count: קנית #{showInt count} #{plural count "דבר" "דברים"}.
+SwitchLanguage: החלף שפה ל:
+Switch: החלף
+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkMessage "App" "messages" "en"
+
+plural :: Int -> String -> String -> String
+plural 1 x _ = x
+plural _ _ y = y
+
+showInt :: Int -> String
+showInt = show
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+mkYesod "App" [parseRoutes|
+/     HomeR GET
+/buy  BuyR  GET
+/lang LangR POST
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>_{MsgHello}
+        <form action=@{BuyR}>
+            _{MsgEnterItemCount}
+            <input type=text name=count>
+            <input type=submit value=_{MsgPurchase}>
+        <form action=@{LangR} method=post>
+            _{MsgSwitchLanguage}
+            <select name=lang>
+                <option value=en>English
+                <option value=he>Hebrew
+            <input type=submit value=_{MsgSwitch}>
+    |]
+
+getBuyR :: Handler Html
+getBuyR = do
+    count <- runInputGet $ ireq intField "count"
+    defaultLayout [whamlet|<p>_{MsgItemCount count}|]
+
+postLangR :: Handler ()
+postLangR = do
+    lang <- runInputPost $ ireq textField "lang"
+    setLanguage lang
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Overview

+

Most existing i18n solutions out there, like gettext or Java message bundles, +work on the principle of string lookups. Usually some form of +printf-interpolation is used to interpolate variables into the strings. In +Yesod, as you might guess, we instead rely on types. This gives us all of our +normal advantages, such as the compiler automatically catching mistakes.

+

Let’s take a concrete example. Suppose our application has two things it wants +to say to a user: say hello, and state how many users are logged into the +system. This can be modeled with a sum type:

+
data MyMessage = MsgHello | MsgUsersLoggedIn Int
+

I can also write a function to turn this datatype into an English representation:

+
toEnglish :: MyMessage -> String
+toEnglish MsgHello = "Hello there!"
+toEnglish (MsgUsersLoggedIn 1) = "There is 1 user logged in."
+toEnglish (MsgUsersLoggedIn i) = "There are " ++ show i ++ " users logged in."
+

We can also write similar functions for other languages. The advantage to this +inside-Haskell approach is that we have the full power of Haskell for +addressing tricky grammar issues, especially pluralization.

+ +

The downside, however, is that you have to write all of this inside of Haskell, +which won’t be very translator-friendly. To solve this, Yesod introduces the +concept of message files. We’ll cover that in a little bit.

+

Assuming we have this full set of translation functions, how do we go about +using them? What we need is a new function to wrap them all up together, and +then choose the appropriate translation function based on the user’s selected +language. Once we have that, Yesod can automatically choose the most relevant +render function and call it on the values you provide.

+

In order to simplify things a bit, Hamlet has a special interpolation syntax, +_{…}, which handles all the calls to the render functions. And in order to +associate a render function with your application, you use the YesodMessage +typeclass.

+
+
+

Message files

+

The simplest approach to creating translations is via message files. The setup +is simple: there is a single folder containing all of your translation files, +with a single file for each language. Each file is named based on its language +code, e.g. en.msg. And each line in a file handles one phrase, which +correlates to a single constructor in your message data type.

+ +

So firstly, a word about language codes. There are really two choices +available: using a two-letter language code, or a language-LOCALE code. For +example, when I load up a page in my web browser, it sends two language codes: +en-US and en. What my browser is saying is "if you have American English, I +like that the most. If you have English, I’ll take that instead."

+

So which format should you use in your application? Most likely two-letter +codes, unless you are actually creating separate translations by locale. This +ensures that someone asking for Canadian English will still see your English. +Behind the scenes, Yesod will add the two-letter codes where relevant. For +example, suppose a user has the following language list:

+
pt-BR, es, he
+

What this means is "I like Brazilian Portuguese, then Spanish, and then +Hebrew." Suppose your application provides the languages pt (general +Portuguese) and English, with English as the default. Strictly following the +user’s language list would result in the user being served English. Instead, +Yesod translates that list into:

+
pt-BR, es, he, pt
+

In other words: unless you’re giving different translations based on locale, +just stick to the two-letter language codes.

+

Now what about these message files? The syntax should be very familiar after +your work with Hamlet and Persistent. The line starts off with the name of the +message. Since this is a data constructor, it must start with a capital letter. +Next, you can have individual parameters, which must be given as lower case. +These will be arguments to the data constructor.

+

The argument list is terminated by a colon, and then followed by the translated +string, which allows usage of our typical variable interpolation syntax +translation helper functions to deal with issues like pluralization, you can +create all the translated messages you need.

+
+

Specifying types

+

Since we will be creating a datatype out of our message specifications, each +parameter to a data constructor must be given a data type. We use a @-syntax +for this. For example, to create the datatype data MyMessage = MsgHello | +MsgSayAge Int, we would write:

+
Hello: Hi there!
+SayAge age@Int: Your age is: #{show age}
+

But there are two problems with this:

+
    +
  1. +

    +It’s not very DRY (don’t repeat yourself) to have to specify this datatype in every file. +

    +
  2. +
  3. +

    +Translators will be confused having to specify these datatypes. +

    +
  4. +
+

So instead, the type specification is only required in the main language file. +This is specified as the third argument in the mkMessage function. This also +specifies what the backup language will be, to be used when none of the +languages provided by your application match the user’s language list.

+
+
+
+

RenderMessage typeclass

+

Your call to mkMessage creates an instance of the RenderMessage typeclass, +which is the core of Yesod’s i18n. It is defined as:

+
class RenderMessage master message where
+    renderMessage :: master
+                  -> [Text] -- ^ languages
+                  -> message
+                  -> Text
+

Notice that there are two parameters to the RenderMessage class: the master +site and the message type. In theory, we could skip the master type here, but +that would mean that every site would need to have the same set of translations +for each message type. When it comes to shared libraries like forms, that would +not be a workable solution.

+

The renderMessage function takes a parameter for each of the class’s type +parameters: master and message. The extra parameter is a list of languages the +user will accept, in descending order of priority. The method then returns a +user-ready Text that can be displayed.

+

A simple instance of RenderMessage may involve no actual translation of +strings; instead, it will just display the same value for every language. For +example:

+
data MyMessage = Hello | Greet Text
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ _ Hello = "Hello"
+    renderMessage _ _ (Greet name) = "Welcome, " <> name <> "!"
+

Notice how we ignore the first two parameters to renderMessage. We can now +extend this to support multiple languages:

+
renderEn Hello = "Hello"
+renderEn (Greet name) = "Welcome, " <> name <> "!"
+renderHe Hello = "שלום"
+renderHe (Greet name) = "ברוכים הבאים, " <> name <> "!"
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ ("en":_) = renderEn
+    renderMessage _ ("he":_) = renderHe
+    renderMessage master (_:langs) = renderMessage master langs
+    renderMessage _ [] = renderEn
+

The idea here is fairly straight-forward: we define helper functions to support +each language. We then add a clause to catch each of those languages in the +renderMessage definition. We then have two final cases: if no languages +matched, continue checking with the next language in the user’s priority list. +If we’ve exhausted all languages the user specified, then use the default +language (in our case, English).

+

But odds are that you will never need to worry about writing this stuff +manually, as the message file interface does all this for you. But it’s always +a good idea to have an understanding of what’s going on under the surface.

+
+
+

Interpolation

+

One way to use your new RenderMessage instance would be to directly call the +renderMessage function. This would work, but it’s a bit tedious: you need to +pass in the foundation value and the language list manually. Instead, Hamlet +provides a specialized i18n interpolation, which looks like _{…}.

+ +

Hamlet will then automatically translate that to a call to renderMessage. +Once Hamlet gets the output Text value, it uses the toHtml function to +produce an Html value, meaning that any special characters (<, &, +>) will be automatically escaped.

+
+
+

Phrases, not words

+

As a final note, I’d just like to give some general i18n advice. Let’s say you +have an application for selling turtles. You’re going to use the word "turtle" +in multiple places, like "You have added 4 turtles to your cart." and "You have +purchased 4 turtles, congratulations!" As a programmer, you’ll immediately +notice the code reuse potential: we have the phrase "4 turtles" twice. So you +might structure your message file as:

+
AddStart: You have added
+AddEnd: to your cart.
+PurchaseStart: You have purchased
+PurchaseEnd: , congratulations!
+Turtles count@Int: #{show count} #{plural count "turtle" "turtles"}
+

STOP RIGHT THERE! This is all well and good from a programming perspective, but translations are not programming. There are a many things that could go wrong with this, such as:

+
    +
  • +

    +Some languages might put "to your cart" before "You have added." +

    +
  • +
  • +

    +Maybe "added" will be constructed differently depending on whether you added 1 or more turtles. +

    +
  • +
  • +

    +There are a bunch of whitespace issues as well. +

    +
  • +
+

So the general rule is: translate entire phrases, not just words.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/introduction.html b/public/book-1.4/introduction.html new file mode 100644 index 00000000..9edbc624 --- /dev/null +++ b/public/book-1.4/introduction.html @@ -0,0 +1,221 @@ + Introduction :: Yesod Web Framework Book- Version 1.4 +
+

Introduction

+ + +

Since web programming began, people have been trying to make the development +process a more pleasant one. As a community, we have continually pushed new +techniques to try and solve some of the lingering difficulties of security +threats, the stateless nature of HTTP, the multiple languages (HTML, CSS, +Javascript) necessary to create a powerful web application, and more.

+

Yesod attempts to ease the web development process by playing to the strengths +of the Haskell programming language. Haskell’s strong compile-time guarantees +of correctness not only encompass types; referential transparency ensures that +we don’t have any unintended side effects. Pattern matching on algebraic data +types can help guarantee we’ve accounted for every possible case. By building +upon Haskell, entire classes of bugs disappear.

+

Unfortunately, using Haskell isn’t enough. The web, by its very nature, is +not type safe. Even the simplest case of distinguishing between an integer +and string is impossible: all data on the web is transferred as raw bytes, +evading our best efforts at type safety. Every app writer is left with the task +of validating all input. I call this problem the boundary issue: as much as +your application is type safe on the inside, every boundary with the outside +world still needs to be sanitized.

+
+

Type Safety

+

This is where Yesod comes in. By using high-level declarative techniques, you +can specify the exact input types you are expecting. And the process works the +other way as well: using a process of type-safe URLs, you can make sure that +the data you send out is also guaranteed to be well formed.

+

The boundary issue is not just a problem when dealing with the client: the same +problem exists when persisting and loading data. Once again, Yesod saves you on +the boundary by performing the marshaling of data for you. You can specify your +entities in a high-level definition and remain blissfully ignorant of the +details.

+
+
+

Concise

+

We all know that there is a lot of boilerplate coding involved in web +applications. Wherever possible, Yesod tries to use Haskell’s features to save +your fingers the work:

+
    +
  • +

    +The forms library reduces the amount of code used for common cases by + leveraging the Applicative type class. +

    +
  • +
  • +

    +Routes are declared in a very terse format, without sacrificing type safety. +

    +
  • +
  • +

    +Serializing your data to and from a database is handled automatically via + code generation. +

    +
  • +
+

In Yesod, we have two kinds of code generation. To get your project started, we +provide a scaffolding tool to set up your file and folder structure. However, +most code generation is done at compile time via meta-programming. This means +your generated code will never get stale, as a simple library upgrade will +bring all your generated code up-to-date.

+

But for those who like to stay in control, and know exactly what their code is +doing, you can always run closer to the compiler and write all your code +yourself.

+
+
+

Performance

+

Haskell’s main compiler, the GHC, has amazing performance characteristics, and +is improving all the time. This choice of language by itself gives Yesod a +large performance advantage over other offerings. But that’s not enough: we +need an architecture designed for performance.

+

Our approach to templates is one example: by allowing HTML, CSS and JavaScript +to be analyzed at compile time, Yesod both avoids costly disk I/O at runtime +and can optimize the rendering of this code. But the architectural decisions go +deeper: we use advanced techniques such as conduits and builders in the +underlying libraries to make sure our code runs in constant memory, without +exhausting precious file handles and other resources. By offering high-level +abstractions, you can get highly compressed and properly cached CSS and +JavaScript.

+

Yesod’s flagship web server, Warp, is the fastest Haskell web server around. +When these two pieces of technology are combined, it produces one of the +fastest web application deployment solutions available.

+
+
+

Modular

+

Yesod has spawned the creation of dozens of packages, most of which are usable +in a context outside of Yesod itself. One of the goals of the project is to +contribute back to the community as much as possible; as such, even if you are +not planning on using Yesod in your next project, a large portion of this book +may still be relevant for your needs.

+

Of course, these libraries have all been designed to integrate well together. +Using the Yesod Framework should give you a strong feeling of consistency +throughout the various APIs.

+
+
+

A solid foundation

+

I remember once seeing a PHP framework advertising support for UTF-8. This +struck me as surprising: you mean having UTF-8 support isn’t automatic? In the +Haskell world, issues like character encoding are already well addressed and +fully supported. In fact, we usually have the opposite problem: there are a +number of packages providing powerful and well-designed support for the +problem. The Haskell community is constantly pushing the boundaries finding the +cleanest, most efficient solutions for each challenge.

+

The downside of such a powerful ecosystem is the complexity of choice. By using +Yesod, you will already have most of the tools chosen for you, and you can be +guaranteed they work together. Of course, you always have the option of pulling +in your own solution.

+

As a real-life example, Yesod and Hamlet (the default templating language) use +blaze-builder for textual content generation. This choice was made because +blaze provides the fastest interface for generating UTF-8 data. Anyone who +wants to use one of the other great libraries out there, such as text, should +have no problem dropping it in.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/json-web-service.html b/public/book-1.4/json-web-service.html new file mode 100644 index 00000000..090d5137 --- /dev/null +++ b/public/book-1.4/json-web-service.html @@ -0,0 +1,222 @@ + JSON Web Service :: Yesod Web Framework Book- Version 1.4 +
+

JSON Web Service

+ + +

Let’s create a very simple web service: it takes a JSON request and returns a +JSON response. We’re going to write the server in WAI/Warp, and the client in +http-conduit. We’ll be using aeson for JSON parsing and rendering. We could +also write the server in Yesod itself, but for such a simple example, the extra +features of Yesod don’t add much.

+
+

Server

+

WAI uses the conduit package to handle streaming request bodies, and +efficiently generates responses using blaze-builder. aeson uses attoparsec for +parsing; by using attoparsec-conduit we get easy interoperability with WAI. +This plays out as:

+
{-# LANGUAGE OverloadedStrings #-}
+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.ByteString          (ByteString)
+import           Data.Conduit             (($$))
+import           Data.Conduit.Attoparsec  (sinkParser)
+import           Network.HTTP.Types       (status200, status400)
+import           Network.Wai              (Application, Response, responseLBS)
+import           Network.Wai.Conduit      (sourceRequestBody)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req sendResponse = handle (sendResponse . invalidJson) $ do
+    value <- sourceRequestBody req $$ sinkParser json
+    newValue <- liftIO $ modValue value
+    sendResponse $ responseLBS
+        status200
+        [("Content-Type", "application/json")]
+        $ encode newValue
+
+invalidJson :: SomeException -> Response
+invalidJson ex = responseLBS
+    status400
+    [("Content-Type", "application/json")]
+    $ encode $ object
+        [ ("message" .= show ex)
+        ]
+
+-- Application-specific logic would go here.
+modValue :: Value -> IO Value
+modValue = return
+
+
+

Client

+

http-conduit was written as a companion to WAI. It too uses conduit and +blaze-builder pervasively, meaning we once again get easy interop with +aeson. A few extra comments for those not familiar with http-conduit:

+
    +
  • +

    +A Manager is present to keep track of open connections, so that multiple + requests to the same server use the same connection. You usually want to use + the withManager function to create and clean up this Manager, since it is + exception safe. +

    +
  • +
  • +

    +We need to know the size of our request body, which can’t be determined + directly from a Builder. Instead, we convert the Builder into a lazy + ByteString and take the size from there. +

    +
  • +
  • +

    +There are a number of different functions for initiating a request. We use + http, which allows us to directly access the data stream. There are other + higher level functions (such as httpLbs) that let you ignore the issues of + sources and get the entire body directly. +

    +
  • +
+
{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Data.Aeson              (Value (Object, String))
+import           Data.Aeson              (encode, object, (.=))
+import           Data.Aeson.Parser       (json)
+import           Data.Conduit            (($$+-))
+import           Data.Conduit.Attoparsec (sinkParser)
+import           Network.HTTP.Conduit    (RequestBody (RequestBodyLBS),
+                                          Response (..), http, method, parseUrl,
+                                          requestBody, withManager)
+
+main :: IO ()
+main = withManager $ \manager -> do
+    value <- liftIO makeValue
+    -- We need to know the size of the request body, so we convert to a
+    -- ByteString
+    let valueBS = encode value
+    req' <- liftIO $ parseUrl "http://localhost:3000/"
+    let req = req' { method = "POST", requestBody = RequestBodyLBS valueBS }
+    res <- http req manager
+    resValue <- responseBody res $$+- sinkParser json
+    liftIO $ handleResponse resValue
+
+-- Application-specific function to make the request value
+makeValue :: IO Value
+makeValue = return $ object
+    [ ("foo" .= ("bar" :: String))
+    ]
+
+-- Application-specific function to handle the response from the server
+handleResponse :: Value -> IO ()
+handleResponse = print
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/monad-control.html b/public/book-1.4/monad-control.html new file mode 100644 index 00000000..c2ade955 --- /dev/null +++ b/public/book-1.4/monad-control.html @@ -0,0 +1,451 @@ + monad-control :: Yesod Web Framework Book- Version 1.4 +
+

monad-control

+ + +

monad-control is used in a few places within Yesod, most notably to ensure +proper exception handling within Persistent. It is a general purpose package to +extend standard functionality in monad transformers.

+
+

Overview

+

One of the powerful, and sometimes confusing, features in Haskell is monad +transformers. They allow you to take different pieces of functionality- such as +mutable state, error handling, or logging- and compose them together easily. +Though I swore I’d never write a monad tutorial, I’m going to employ a painful +analogy here: monads are like onions. (Monads are not like cakes.) By that, I +mean layers.

+

We have the core monad- also known as the innermost or bottom monad. On top of +this core, we add layers, each adding a new feature and spreading +outward/upward. As a motivating example, let’s consider an ErrorT transformer +stacked on top of the IO monad:

+
newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
+type MyStack = ErrorT MyError IO
+

Now pay close attention here: ErrorT is just a simple newtype around an Either +wrapped in a monad. Getting rid of the newtype, we have:

+
type ErrorTUnwrapped e m a = m (Either e a)
+

At some point, we’ll need to actually perform some IO inside our MyStack. If we +went with the unwrapped approach, it would be trivial, since there would be no +ErrorT constructor in the way. However, we need that newtype wrapper for a +whole bunch of type reasons I won’t go into here (this isn’t a monad +transformer tutorial after all). So the solution is the MonadTrans typeclass:

+
class MonadTrans t where
+    lift :: Monad m => m a -> t m a
+

I’ll admit, the first time I saw that type signature, my response was stunned +confusion, and incredulity that it actually meant anything. But looking at an +instance helps a bit:

+
instance (Error e) => MonadTrans (ErrorT e) where
+    lift m = ErrorT $ do
+        a <- m
+        return (Right a)
+

All we’re doing is wrapping the inside of the IO with a Right value, and then +applying our newtype wrapper. This allows us to take an action that lives in +IO, and "lift" it to the outer/upper monad.

+

But now to the point at hand. This works very well for simple functions. For +example:

+
sayHi :: IO ()
+sayHi = putStrLn "Hello"
+
+sayHiError :: ErrorT MyError IO ()
+sayHiError = lift $ putStrLn "Hello"
+

But let’s take something slightly more complicated, like a callback:

+
withMyFile :: (Handle -> IO a) -> IO a
+withMyFile = withFile "test.txt" WriteMode
+
+sayHi :: Handle -> IO ()
+sayHi handle = hPutStrLn handle "Hi there"
+
+useMyFile :: IO ()
+useMyFile = withMyFile sayHi
+

So far so good, right? Now let’s say that we need a version of sayHi that has +access to the Error monad:

+
sayHiError :: Handle -> ErrorT MyError IO ()
+sayHiError handle = do
+    lift $ hPutStrLn handle "Hi there, error!"
+    throwError MyError
+

We would like to write a function that combines withMyFile and sayHiError. +Unfortunately, GHC doesn’t like this very much:

+
useMyFileErrorBad :: ErrorT MyError IO ()
+useMyFileErrorBad = withMyFile sayHiError
+
+    Couldn't match expected type `ErrorT MyError IO ()'
+                with actual type `IO ()'
+

Why does this happen, and how can we work around it?

+
+
+

Intuition

+

Let’s try and develop an external intuition of what’s happening here. The +ErrorT monad transformer adds extra functionality to the IO monad. We’ve +defined a way to "tack on" that extra functionality to normal IO actions: we +add that Right constructor and wrap it all in ErrorT. Wrapping in Right is our +way of saying "it went OK," there wasn’t anything wrong with this action.

+

Now this intuitively makes sense: since the IO monad doesn’t have the concept +of returning a MyError when something goes wrong, it will always succeed in the +lifting phase. (Note: This has nothing to do with runtime exceptions, don’t +even think about them.) What we have is a guaranteed one-directional +translation up the monad stack.

+

Let’s take another example: the Reader monad. A Reader has access to some extra +piece of data floating around. Whatever is running in the inner monad doesn’t +know about that extra piece of information. So how would you do a lift? You +just ignore that extra information. The Writer monad? Don’t write anything. +State? Don’t change anything. I’m seeing a pattern here.

+

But now let’s try and go in the opposite direction: I have something in a +Reader, and I’d like to run it in the base monad (e.g., IO). Well… that’s not +going to work, is it? I need that extra piece of information, I’m relying on +it, and it’s not there. There’s simply no way to go in the opposite direction +without providing that extra value.

+

Or is there? If you remember, we’d pointed out earlier that ErrorT is just a +simple wrapper around the inner monad. In other words, if I have errorValue +:: ErrorT MyError IO MyValue, I can apply runErrorT and get a value of +type IO (Either MyError MyValue). The looks quite a bit like bi-directional +translation, doesn’t it?

+

Well, not quite. We originally had an ErrorT MyError IO monad, with a value +of type MyValue. Now we have a monad of type IO with a value of type +Either MyError MyValue. So this process has in fact changed the value, while +the lifting process leaves it the same.

+

But still, with a little fancy footwork we can unwrap the ErrorT, do some +processing, and then wrap it back up again.

+
useMyFileError1 :: ErrorT MyError IO ()
+useMyFileError1 =
+    let unwrapped :: Handle -> IO (Either MyError ())
+        unwrapped handle = runErrorT $ sayHiError handle
+        applied :: IO (Either MyError ())
+        applied = withMyFile unwrapped
+        rewrapped :: ErrorT MyError IO ()
+        rewrapped = ErrorT applied
+     in rewrapped
+

This is the crucial point of this whole article, so look closely. We first +unwrap our monad. This means that, to the outside world, it’s now just a plain +old IO value. Internally, we’ve stored all the information from our ErrorT +transformer. Now that we have a plain old IO, we can easily pass it off to +withMyFile. withMyFile takes in the internal state and passes it back out +unchanged. Finally, we wrap everything back up into our original ErrorT.

+

This is the entire pattern of monad-control: we embed the extra features of our +monad transformer inside the value. Once in the value, the type system ignores +it and focuses on the inner monad. When we’re done playing around with that +inner monad, we can pull our state back out and reconstruct our original monad +stack.

+
+
+

Types

+

I purposely started with the ErrorT transformer, as it is one of the simplest +for this inversion mechanism. Unfortunately, others are a bit more complicated. +Take for instance ReaderT. It is defined as newtype ReaderT r m a = ReaderT { +runReaderT :: r -> m a }. If we apply runReaderT to it, we get a +function that returns a monadic value. So we’re going to need some extra +machinery to deal with all that stuff. And this is when we leave Kansas behind.

+

There are a few approaches to solving these problems. In the past, I +implemented a solution using type families in the neither package. Anders +Kaseorg implemented a much more straight-forward solution in monad-peel. And +for efficiency, in monad-control, Bas van Dijk uses CPS (continuation passing +style) and existential types.

+ +

The first type we’re going to look at is:

+
type Run t = forall n o b. (Monad n, Monad o, Monad (t o)) => t n b -> n (t o b)
+

That’s incredibly dense, let’s talk it out. The only "input" datatype to this +thing is t, a monad transformer. A Run is a function that will then work with +any combination of types n, o and b (that’s what the forall means). n and o +are both monads, while b is a simple value contained by them.

+

The left hand side of the Run function, t n b, is our monad transformer +wrapped around the n monad and holding a b value. So for example, that could be +a MyTrans FirstMonad MyValue. It then returns a value with the transformer +"popped" inside, with a brand new monad at its core. In other words, +FirstMonad (MyTrans NewMonad MyValue).

+

That might sound pretty scary at first, but it actually isn’t as foreign as +you’d think: this is essentially what we did with ErrorT. We started with +ErrorT on the outside, wrapping around IO, and ended up with an IO by itself +containing an Either. Well guess what: another way to represent an Either is +ErrorT MyError Identity. So essentially, we pulled the IO to the outside and +plunked an Identity in its place. We’re doing the same thing in a Run: pulling +the FirstMonad outside and replacing it with a NewMonad.

+ +

Alright, now we’re getting somewhere. If we had access to one of those Run +functions, we could use it to peel off the ErrorT on our sayHiError function +and pass it to withMyFile. With the magic of undefined, we can play such a +game:

+
errorRun :: Run (ErrorT MyError)
+errorRun = undefined
+
+useMyFileError2 :: IO (ErrorT MyError Identity ())
+useMyFileError2 =
+    let afterRun :: Handle -> IO (ErrorT MyError Identity ())
+        afterRun handle = errorRun $ sayHiError handle
+        applied :: IO (ErrorT MyError Identity ())
+        applied = withMyFile afterRun
+     in applied
+

This looks eerily similar to our previous example. In fact, errorRun is acting +almost identically to runErrorT. However, we’re still left with two problems: +we don’t know where to get that errorRun value from, and we still need to +restructure the original ErrorT after we’re done.

+
+

MonadTransControl

+

Obviously in the specific case we have before us, we could use our knowledge of +the ErrorT transformer to beat the types into submission and create our Run +function manually. But what we really want is a general solution for many +transformers. At this point, you know we need a typeclass.

+

So let’s review what we need: access to a Run function, and some way to +restructure our original transformer after the fact. And thus was born +MonadTransControl, with its single method liftControl:

+
class MonadTrans t => MonadTransControl t where
+    liftControl :: Monad m => (Run t -> m a) -> t m a
+

Let’s look at this closely. liftControl takes a function (the one we’ll be +writing). That function is provided with a Run function, and must return a +value in some monad (m). liftControl will then take the result of that function +and reinstate the original transformer on top of everything.

+
useMyFileError3 :: Monad m => ErrorT MyError IO (ErrorT MyError m ())
+useMyFileError3 =
+    liftControl inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

Close, but not exactly what I had in mind. What’s up with the double monads? +Well, let’s start at the end: sayHiError handle returns a value of type ErrorT +MyError IO (). This we knew already, no surprises. What might be a little +surprising (it got me, at least) is the next two steps.

+

First we apply run to that value. Like we’d discussed before, the result is +that the IO inner monad is popped to the outside, to be replaced by some +arbitrary monad (represented by m here). So we end up with an IO (ErrorT +MyError m ()). Ok… We then get the same result after applying withMyFile. Not +surprising.

+

The last step took me a long time to understand correctly. Remember how we said +that we reconstruct the original transformer? Well, so we do: by plopping it +right on top of everything else we have. So our end result is the previous +type- IO (ErrorT MyError m ())- with a ErrorT MyError stuck on the front.

+

Well, that seems just about utterly worthless, right? Well, almost. But don’t +forget, that "m" can be any monad, including IO. If we treat it that way, we +get ErrorT MyError IO (ErrorT MyError IO ()). That looks a lot like m (m +a), and we want just plain old m a. Fortunately, now we’re in luck:

+
useMyFileError4 :: ErrorT MyError IO ()
+useMyFileError4 = join useMyFileError3
+

And it turns out that this usage is so common, that Bas had mercy on us and +defined a helper function:

+
control :: (Monad m, Monad (t m), MonadTransControl t)
+        => (Run t -> m (t m a)) -> t m a
+control = join . liftControl
+

So all we need to write is:

+
useMyFileError5 :: ErrorT MyError IO ()
+useMyFileError5 =
+    control inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

And just to make it a little shorter:

+
useMyFileError6 :: ErrorT MyError IO ()
+useMyFileError6 = control $ \run -> withMyFile $ run . sayHiError
+
+
+

MonadControlIO

+

The MonadTrans class provides the lift method, which allows you to lift an +action one level in the stack. There is also the MonadIO class that provides +liftIO, which lifts an IO action as far in the stack as desired. We have the +same breakdown in monad-control. But first, we need a corrolary to Run:

+
type RunInBase m base = forall b. m b -> base (m b)
+

Instead of dealing with a transformer, we’re dealing with two monads. base is +the underlying monad, and m is a stack built on top of it. RunInBase is a +function that takes a value of the entire stack, pops out that base, and puts +in on the outside. Unlike in the Run type, we don’t replace it with an +arbitrary monad, but with the original one. To use some more concrete types:

+
RunInBase (ErrorT MyError IO) IO = forall b. ErrorT MyError IO b -> IO (ErrorT MyError IO b)
+

This should look fairly similar to what we’ve been looking at so far, the only +difference is that we want to deal with a specific inner monad. Our +MonadControlIO class is really just an extension of MonadControlTrans using +this RunInBase.

+
class MonadIO m => MonadControlIO m where
+    liftControlIO :: (RunInBase m IO -> IO a) -> m a
+

Simply put, liftControlIO takes a function which receives a RunInBase. That +RunInBase can be used to strip down our monad to just an IO, and then +liftControlIO builds everything back up again. And like MonadControlTrans, it +comes with a helper function

+
controlIO :: MonadControlIO m => (RunInBase m IO -> IO (m a)) -> m a
+controlIO = join . liftControlIO
+

We can easily rewrite our previous example with it:

+
useMyFileError7 :: ErrorT MyError IO ()
+useMyFileError7 = controlIO $ \run -> withMyFile $ run . sayHiError
+

And as an advantage, it easily scales to multiple transformers:

+
sayHiCrazy :: Handle -> ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+sayHiCrazy handle = liftIO $ hPutStrLn handle "Madness!"
+
+useMyFileCrazy :: ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+useMyFileCrazy = controlIO $ \run -> withMyFile $ run . sayHiCrazy
+
+
+
+

Real Life Examples

+

Let’s solve some real-life problems with this code. Probably the biggest +motivating use case is exception handling in a transformer stack. For example, +let’s say that we want to automatically run some cleanup code when an exception +is thrown. If this were normal IO code, we’d use:

+
onException :: IO a -> IO b -> IO a
+

But if we’re in the ErrorT monad, we can’t pass in either the action or the +cleanup. In comes controlIO to the rescue:

+
onExceptionError :: ErrorT MyError IO a
+                 -> ErrorT MyError IO b
+                 -> ErrorT MyError IO a
+onExceptionError action after = controlIO $ \run ->
+    run action `onException` run after
+

Let’s say we need to allocate some memory to store a Double in. In the IO +monad, we could just use the alloca function. Once again, our solution is +simple:

+
allocaError :: (Ptr Double -> ErrorT MyError IO b)
+            -> ErrorT MyError IO b
+allocaError f = controlIO $ \run -> alloca $ run . f
+
+
+

Lost State

+

Let’s rewind a bit to our onExceptionError. It uses onException under the +surface, which has a type signature: IO a -> IO b -> IO a. Let me ask +you something: what happened to the b in the output? Well, it was thoroughly +ignored. But that seems to cause us a bit of a problem. After all, we store our +transformer state information in the value of the inner monad. If we ignore it, +we’re essentially ignoring the monadic side effects as well!

+

And the answer is that, yes, this does happen with monad-control. Certain functions will drop some of the monadic side effects. This is put best by Bas, in the comments on the relevant functions:[quote]

+
+

Note, any monadic side effects in m of the "release" computation will be discarded; it is run only for its side effects in IO.

+
+

In practice, monad-control will usually be doing the right thing for you, but +you need to be aware that some side effects may disappear.

+
+
+

More Complicated Cases

+

In order to make our tricks work so far, we’ve needed to have functions that +give us full access to play around with their values. Sometimes, this isn’t the +case. Take, for instance:

+
addMVarFinalizer :: MVar a -> IO () -> IO ()
+

In this case, we are required to have no value inside our finalizer function. +Intuitively, the first thing we should notice is that there will be no way to +capture our monadic side effects. So how do we get something like this to +compile? Well, we need to explicitly tell it to drop all of its state-holding +information:

+
addMVarFinalizerError :: MVar a -> ErrorT MyError IO () -> ErrorT MyError IO ()
+addMVarFinalizerError mvar f = controlIO $ \run ->
+    return $ liftIO $ addMVarFinalizer mvar (run f >> return ())
+

Another case from the same module is:

+
modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b
+

Here, we have a restriction on the return type in the second argument: it must +be a tuple of the value passed to that function and the final return value. +Unfortunately, I can’t see a way of writing a little wrapper around modifyMVar +to make it work for ErrorT. Instead, in this case, I copied the definition of +modifyMVar and modified it:

+
modifyMVar :: MVar a
+           -> (a -> ErrorT MyError IO (a, b))
+           -> ErrorT MyError IO b
+modifyMVar m io =
+  Control.Exception.Control.mask $ \restore -> do
+    a      <- liftIO $ takeMVar m
+    (a',b) <- restore (io a) `onExceptionError` liftIO (putMVar m a)
+    liftIO $ putMVar m a'
+    return b
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/persistent.html b/public/book-1.4/persistent.html new file mode 100644 index 00000000..c57d8ada --- /dev/null +++ b/public/book-1.4/persistent.html @@ -0,0 +1,1594 @@ + Persistent :: Yesod Web Framework Book- Version 1.4 +
+

Persistent

+ + +

Forms deal with the boundary between the user and the application. Another +boundary we need to deal with is between the application and the storage layer. +Whether it be a SQL database, a YAML file, or a binary blob, odds are your +storage layer does not natively understand your application’s data types, and +you’ll need to perform some marshaling. Persistent is Yesod’s answer to data +storage- a type-safe, universal data store interface for Haskell.

+

Haskell has many different database bindings available. However, most of these +have little knowledge of a schema and therefore do not provide useful static +guarantees. They also force database-dependent APIs and data types on the +programmer.

+

Some Haskellers have attempted a more revolutionary route: creating Haskell +specific data stores that allow one to easily store any strongly typed Haskell +data. These options are great for certain use cases, but they constrain one to +the storage techniques provided by the library and do not interface well with +other languages.

+

In contrast, Persistent allows us to choose among existing databases that are +highly tuned for different data storage use cases, interoperate with other +programming languages, and to use a safe and productive query interface, while +still keeping the type safety of Haskell datatypes.

+

Persistent follows the guiding principles of type safety and concise, +declarative syntax. Some other nice features are:

+
    +
  • +

    +Database-agnostic. There is first class support for PostgreSQL, SQLite, MySQL + and MongoDB, with experimental Redis support. +

    +
  • +
  • +

    +Convenient data modeling. + Persistent lets you model relationships and use them in type-safe ways. + The default type-safe persistent API does not support joins, allowing support for a + wider number of storage layers. + Joins and other SQL specific functionality can be achieved through using + a raw SQL layer (with very little type safety). + An additional library, Esqueleto, + builds on top of the Persistent data model, adding type-safe joins and SQL functionality. +

    +
  • +
  • +

    +Automatically perform database migrations +

    +
  • +
+

Persistent works well with Yesod, but it is quite +usable on its own as a standalone library. Most of this chapter will address +Persistent on its own.

+
+

Synopsis

+

The required dependencies for the below are: persistent, persistent-sqlite and persistent-template.

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Sqlite
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+    janeId <- insert $ Person "Jane Doe" Nothing
+
+    insert $ BlogPost "My fr1st p0st" johnId
+    insert $ BlogPost "One more for good measure" johnId
+
+    oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+    liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+    john <- get johnId
+    liftIO $ print (john :: Maybe Person)
+
+    delete janeId
+    deleteWhere [BlogPostAuthorId ==. johnId]
+ +
+
+

Solving the boundary issue

+

Suppose you are storing information on people in a SQL database. Your table +might look something like:

+
CREATE TABLE person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)
+

And if you are using a database like PostgreSQL, you can be guaranteed that the +database will never store some arbitrary text in your age field. (The same +cannot be said of SQLite, but let’s forget about that for now.) To mirror this +database table, you would likely create a Haskell datatype that looks something +like:

+
data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+

It looks like everything is type safe: the database schema matches our Haskell +datatypes, the database ensures that invalid data can never make it into our +data store, and everything is generally awesome. Well, until:

+
    +
  • +

    +You want to pull data from the database, and the database layer gives you the + data in an untyped format. +

    +
  • +
  • +

    +You want to find everyone older than 32, and you accidentally write "thirtytwo" + in your SQL statement. Guess what: that will compile just fine, and you won’t + find out you have a problem until runtime. +

    +
  • +
  • +

    +You decide you want to find the first 10 people alphabetically. No problem… + until you make a typo in your SQL. Once again, you don’t find out until + runtime. +

    +
  • +
+

In dynamic languages, the answer to these issues is unit testing. For +everything that can go wrong, make sure you write a test case. But as I am +sure you are aware by now, that doesn’t jive well with the Yesod approach to +things. We like to take advantage of Haskell’s strong typing to save us +wherever possible, and data storage is no exception.

+

So the question remains: how can we use Haskell’s type system to save the day?

+
+

Types

+

Like routing, there is nothing intrinsically difficult about type-safe data +access. It just requires a lot of monotonous, error prone, boiler plate code. +As usual, this means we can use the type system to keep us honest. And to avoid +some of the drudgery, we’ll use a sprinkling of Template Haskell.

+

PersistValue is the basic building block of Persistent. It is a sum type that +can represent data that gets sent to and from a database. Its definition is:

+
data PersistValue
+    = PersistText Text
+    | PersistByteString ByteString
+    | PersistInt64 Int64
+    | PersistDouble Double
+    | PersistRational Rational
+    | PersistBool Bool
+    | PersistDay Day
+    | PersistTimeOfDay TimeOfDay
+    | PersistUTCTime UTCTime
+    | 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
+

Each Persistent backend needs to know how to translate the relevant values into +something the database can understand. However, it would be awkward to have to +express all of our data simply in terms of these basic types. The next layer is +the PersistField typeclass, which defines how an arbitrary Haskell datatype +can be marshaled to and from a PersistValue. A PersistField correlates to a +column in a SQL database. In our person example above, name and age would be +our PersistFields.

+

To tie up the user side of the code, our last typeclass is PersistEntity. An +instance of PersistEntity correlates with a table in a SQL database. This +typeclass defines a number of functions and some associated types. To review, +we have the following correspondence between Persistent and SQL:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SQLPersistent

Datatypes (VARCHAR, INTEGER, etc)

PersistValue

Column

PersistField

Table

PersistEntity

+
+
+

Code Generation

+

In order to ensure that the PersistEntity instances match up properly with your +Haskell datatypes, Persistent takes responsibility for both. This is also good +from a DRY (Don’t Repeat Yourself) perspective: you only need to define your +entities once. Let’s see a quick example:

+
{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+mkPersist sqlSettings [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+

We use a combination of Template Haskell and Quasi-Quotation (like when +defining routes): persistLowerCase is a quasi-quoter which converts a +whitespace-sensitive syntax into a list of entity definitions. "Lower case" +refers to the format of the generated table names. In this scheme, an +entity like SomeTable would become the SQL table some_table. You can also +declare your entities in a separate file using persistFileWith. mkPersist +takes that list of entities and declares:

+
    +
  • +

    +One Haskell datatype for each entity. +

    +
  • +
  • +

    +A PersistEntity instance for each datatype defined. +

    +
  • +
+

The example above generates code that looks like the following:

+
{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+import Control.Applicative
+
+data Person = Person
+    { personName :: !String
+    , personAge :: !Int
+    }
+  deriving Show
+
+type PersonId = Key Person
+
+instance PersistEntity Person where
+    newtype Key Person = PersonKey (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 Unique Person
+    type PersistEntityBackend Person = SqlBackend
+
+    toPersistFields (Person name age) =
+        [ SomePersistField name
+        , SomePersistField age
+        ]
+
+    fromPersistValues [nameValue, ageValue] = Person
+        <$> fromPersistValue nameValue
+        <*> fromPersistValue ageValue
+    fromPersistValues _ = Left "Invalid fromPersistValues input"
+
+    -- Information on each field, used internally to generate SQL statements
+    persistFieldDef PersonId = FieldDef
+        (HaskellName "Id")
+        (DBName "id")
+        (FTTypeCon Nothing "PersonId")
+        SqlInt64
+        []
+        True
+        NoReference
+    persistFieldDef PersonName = FieldDef
+        (HaskellName "name")
+        (DBName "name")
+        (FTTypeCon Nothing "String")
+        SqlString
+        []
+        True
+        NoReference
+    persistFieldDef PersonAge = FieldDef
+        (HaskellName "age")
+        (DBName "age")
+        (FTTypeCon Nothing "Int")
+        SqlInt64
+        []
+        True
+        NoReference
+

As you might expect, our Person datatype closely matches the definition we +gave in the original Template Haskell version. We also have a Generalized +Algebraic Datatype (GADT) which gives a separate constructor for each field. +This GADT encodes both the type of the entity and the type of the field. We use +its constructors throughout Persistent, such as to ensure that when we apply a +filter, the types of the filtering value match the field. There’s another +associated newtype for the database primary key of this entity.

+

We can use the generated Person type like any other Haskell type, and then +pass it off to other Persistent functions.

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Sqlite
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    michaelId <- insert $ Person "Michael" $ Just 26
+    michael <- get michaelId
+    liftIO $ print michael
+ +

We start off with some standard database connection code. In this case, we used +the single-connection functions. Persistent also comes built in with connection +pool functions, which we will generally want to use in production.

+

In this example, we have seen two functions: insert creates a new record in +the database and returns its ID. Like everything else in Persistent, IDs are +type safe. We’ll get into more details of how these IDs work later. So when you +call insert $ Person "Michael" 26, it gives you a value back of type +PersonId.

+

The next function we see is get, which attempts to load a value from the +database using an Id. In Persistent, you never need to worry that you are +using the key from the wrong table: trying to load up a different entity (like +House) using a PersonId will never compile.

+
+
+

PersistStore

+

One last detail is left unexplained from the previous example: what exactly +does runSqlite do, and what is that monad that our database actions are +running in?

+

All database actions require a parameter which is an instance of +PersistStore. As its name implies, every data store (PostgreSQL, SQLite, +MongoDB) has an instance of PersistStore. This is where all the translations +from PersistValue to database-specific values occur, where SQL query +generation happens, and so on.

+ +

runSqlite creates a single connection to a database using its supplied +connection string. For our test cases, we will use :memory:, which uses an +in-memory database. All of the SQL backends share the same instance of +PersistStore: SqlBackend. runSqlite then provides the SqlBackend value +as an environment parameter to the action via runReaderT.

+ +

One important thing to note is that everything which occurs inside a single +call to runSqlite runs in a single transaction. This has two important +implications:

+
    +
  • +

    +For many databases, committing a transaction can be a costly activity. By + putting multiple steps into a single transaction, you can speed up code + dramatically. +

    +
  • +
  • +

    +If an exception is thrown anywhere inside a single call to runSqlite, all + actions will be rolled back (assuming your backend has rollback support). +

    + +
  • +
+
+
+
+

Migrations

+

I’m sorry to tell you, but so far I have lied to you a bit: the example from +the previous section does not actually work. If you try to run it, you will get +an error message about a missing table.

+

For SQL databases, one of the major pains can be managing schema changes. +Instead of leaving this to the user, Persistent steps in to help, but you have +to ask it to help. Let’s see what this looks like:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+share [mkPersist sqlSettings, mkSave "entityDefs"] [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    -- this line added: that's it!
+    runMigration $ migrate entityDefs $ entityDef (Nothing :: Maybe Person)
+    michaelId <- insert $ Person "Michael" 26
+    michael <- get michaelId
+    liftIO $ print michael
+

With this one little code change, Persistent will automatically create your +Person table for you. This split between runMigration and migrate allows +you to migrate multiple tables simultaneously.

+

This works when dealing with just a few entities, but can quickly get tiresome +once we are dealing with a dozen entities. Instead of repeating yourself, +Persistent provides a helper function, mkMigrate:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+Car
+    color String
+    make String
+    model String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do runMigration migrateAll
+

mkMigrate is a Template Haskell function which creates a new function that +will automatically call migrate on all entities defined in the persist +block. The share function is just a little helper that passes the information +from the persist block to each Template Haskell function and concatenates the +results.

+

Persistent has very conservative rules about what it will do during a +migration. It starts by loading up table information from the database, +complete with all defined SQL datatypes. It then compares that against the +entity definition given in the code. For the following cases, it will +automatically alter the schema:

+
    +
  • +

    +The datatype of a field changed. However, the database may object to this + modification if the data cannot be translated. +

    +
  • +
  • +

    +A field was added. However, if the field is not null, no default value is + supplied (we’ll discuss defaults later) and there is already data in the + database, the database will not allow this to happen. +

    +
  • +
  • +

    +A field is converted from not null to null. In the opposite case, Persistent + will attempt the conversion, contingent upon the database’s approval. +

    +
  • +
  • +

    +A brand new entity is added. +

    +
  • +
+

However, there are some cases that Persistent will not handle:

+
    +
  • +

    +Field or entity renames: Persistent has no way of knowing that "name" has now + been renamed to "fullName": all it sees is an old field called name and a new + field called fullName. +

    +
  • +
  • +

    +Field removals: since this can result in data loss, Persistent by default + will refuse to perform the action (you can force the issue by using + runMigrationUnsafe instead of runMigration, though it is not + recommended). +

    +
  • +
+

runMigration will print out the migrations it is running on stderr (you can +bypass this by using runMigrationSilent). Whenever possible, it uses ALTER +TABLE calls. However, in SQLite, ALTER TABLE has very limited abilities, and +therefore Persistent must resort to copying the data from one table to another.

+

Finally, if instead of performing a migration, you want Persistent to give +you hints about what migrations are necessary, use the printMigration +function. This function will print out the migrations which runMigration +would perform for you. This may be useful for performing migrations that +Persistent is not capable of, for adding arbitrary SQL to a migration, or just +to log what migrations occurred.

+
+
+

Uniqueness

+

In addition to declaring fields within an entity, you can also declare +uniqueness constraints. A typical example would be requiring that a username be +unique.

+
User
+    username Text
+    UniqueUsername username
+

While each field name must begin with a lowercase letter, the uniqueness +constraints must begin with an uppercase letter, since it will be represented +in Haskell as a data constructor.

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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|
+Person
+    firstName String
+    lastName String
+    age Int
+    PersonName firstName lastName
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael" "Snoyman" 26
+    michael <- getBy $ PersonName "Michael" "Snoyman"
+    liftIO $ print michael
+

To declare a unique combination of fields, we add an extra line to our +declaration. Persistent knows that it is defining a unique constructor, since +the line begins with a capital letter. Each following word must be a field in +this entity.

+

The main restriction on uniqueness is that it can only be applied to non-null +fields. The reason for this is that the SQL standard is ambiguous on how +uniqueness should be applied to NULL (e.g., is NULL=NULL true or false?). +Besides that ambiguity, most SQL engines in fact implement rules which would be +contrary to what the Haskell datatypes anticipate (e.g., PostgreSQL says that +NULL=NULL is false, whereas Haskell says Nothing == Nothing is True).

+

In addition to providing nice guarantees at the database level about +consistency of your data, uniqueness constraints can also be used to perform +some specific queries within your Haskell code, like the getBy demonstrated +above. This happens via the Unique associated type. In the example above, we +end up with a new constructor:

+
PersonName :: String -> String -> Unique Person
+ +
+
+

Queries

+

Depending on what your goal is, there are different approaches to querying the +database. Some commands query based on a numeric ID, while others will filter. +Queries also differ in the number of results they return: some lookups should +return no more than one result (if the lookup key is unique) while others can +return many results.

+

Persistent therefore provides a few different query functions. As usual, we try +to encode as many invariants in the types as possible. For example, a query +that can return only 0 or 1 results will use a Maybe wrapper, whereas a query +returning many results will return a list.

+
+

Fetching by ID

+

The simplest query you can perform in Persistent is getting based on an ID. +Since this value may or may not exist, its return type is wrapped in a Maybe.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+maybePerson <- get personId
+case maybePerson of
+    Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+    Just person -> liftIO $ print person
+

This can be very useful for sites that provide URLs like /person/5. However, +in such a case, we don’t usually care about the Maybe wrapper, and just want +the value, returning a 404 message if it is not found. Fortunately, the +get404 (provided by the yesod-persistent package) function helps us out here. +We’ll go into more details when we see integration with Yesod.

+
+
+

Fetching by unique constraint

+

getBy is almost identical to get, except:

+
    +
  1. +

    +it takes a uniqueness constraint; that is, instead of an ID it takes a Unique value. +

    +
  2. +
  3. +

    +it returns an Entity instead of a value. An Entity is a combination of database ID and value. +

    +
  4. +
+
personId <- insert $ Person "Michael" "Snoyman" 26
+maybePerson <- getBy $ PersonName "Michael" "Snoyman"
+case maybePerson of
+    Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+    Just (Entity personId person) -> liftIO $ print person
+

Like get404, there is also a getBy404 function.

+
+
+

Select functions

+

Most likely, you’re going to want more powerful queries. You’ll want to find +everyone over a certain age; all cars available in blue; all users without a +registered email address. For this, you need one of the select functions.

+

All the select functions use a similar interface, with slightly different outputs:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionReturns

selectSource

A Source 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 +recommend reading the +School +of Haskell 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.

+

selectList is the most commonly used, so we will cover it specifically. Understanding the others should be trivial after that.

+

selectList takes two arguments: a list of Filters, and a list of +SelectOpts. The former is what limits your results based on +characteristics; it allows for equals, less than, is member of, and such. +SelectOpts provides for three different features: sorting, limiting output +to a certain number of rows, and offsetting results by a certain number of +rows.

+ +

Let’s jump straight into an example of filtering, and then analyze it.

+
people <- selectList [PersonAge >. 25, PersonAge <=. 30] []
+liftIO $ print people
+

As simple as that example is, we really need to cover three points:

+
    +
  1. +

    +PersonAge is a constructor for an associated phantom type. That might sound +scary, but what’s important is that it uniquely identifies the "age" column of +the "person" table, and that it knows that the age field is an Int. (That’s +the phantom part.) +

    +
  2. +
  3. +

    +We have a bunch of Persistent filtering operators. They’re all pretty +straight-forward: just tack a period to the end of what you’d expect. There are +three gotchas here, I’ll explain below. +

    +
  4. +
  5. +

    +The list of filters is ANDed together, so that our constraint means "age is +greater than 25 AND age is less than or equal to 30". We’ll describe ORing +later. +

    +
  6. +
+

The one operator that’s surprisingly named is "not equals." We use !=., since +/=. is used for updates (for "divide-and-set", described later). Don’t worry: +if you use the wrong one, the compiler will catch you. The other two surprising +operators are the "is member" and "is not member". They are, respectively, +<-. and /<-. (both end with a period).

+

And regarding ORs, we use the ||. operator. For example:

+
people <- selectList
+    (       [PersonAge >. 25, PersonAge <=. 30]
+        ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
+        ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
+    )
+    []
+liftIO $ print people
+

This (completely nonsensical) example means: find people who are 26-30, +inclusive, OR whose names are neither Adam or Bonny, OR whose age is either 50 +or 60.

+
+

SelectOpt

+

All of our selectList calls have included an empty list as the second +parameter. That specifies no options, meaning: sort however the database wants, +return all results, and don’t skip any results. A SelectOpt has four +constructors that can be used to change all that.

+
+
+Asc +
+

+Sort by the given column in ascending order. This uses the same phantom type as filtering, such as PersonAge. +

+
+
+Desc +
+

+Same as Asc, in descending order. +

+
+
+LimitTo +
+

+Takes an Int argument. Only return up to the specified number of results. +

+
+
+OffsetBy +
+

+Takes an Int argument. Skip the specified number of results. +

+
+
+

The following code defines a function that will break down results into pages. +It returns all people aged 18 and over, and then sorts them by age (oldest +person first). For people with the same age, they are sorted alphabetically by +last name, then first name.

+
resultsForPage pageNumber = do
+    let resultsPerPage = 10
+    selectList
+        [ PersonAge >=. 18
+        ]
+        [ Desc PersonAge
+        , Asc PersonLastName
+        , Asc PersonFirstName
+        , LimitTo resultsPerPage
+        , OffsetBy $ (pageNumber - 1) * resultsPerPage
+        ]
+
+
+
+
+

Manipulation

+

Querying is only half the battle. We also need to be able to add data to and +modify existing data in the database.

+
+

Insert

+

It’s all well and good to be able to play with data in the database, but how +does it get there in the first place? The answer is the insert function. You +just give it a value, and it gives back an ID.

+

At this point, it makes sense to explain a bit of the philosophy behind +Persistent. In many other ORM solutions, the datatypes used to hold data are +opaque: you need to go through their defined interfaces to get at and modify +the data. That’s not the case with Persistent: we’re using plain old Algebraic +Data Types for the whole thing. This means you still get all the great benefits +of pattern matching, currying and everything else you’re used to.

+

However, there are a few things we can’t do. For one, there’s no way to +automatically update values in the database every time the record is updated in +Haskell. Of course, with Haskell’s normal stance of purity and immutability, +this wouldn’t make much sense anyway, so I don’t shed any tears over it.

+

However, there is one issue that newcomers are often bothered by: why are IDs +and values completely separate? It seems like it would be very logical to embed +the ID inside the value. In other words, instead of having:

+
data Person = Person { name :: String }
+

have

+
data Person = Person { personId :: PersonId, name :: String }
+

Well, there’s one problem with this right off the bat: how do we do an insert? If a Person needs to have an ID, and we get the ID by inserting, and an insert needs a Person, we have an impossible loop. We could solve this with undefined, but that’s just asking for trouble.

+

OK, you say, let’s try something a bit safer:

+
data Person = Person { personId :: Maybe PersonId, name :: String }
+

I definitely prefer insert $ Person Nothing "Michael" to insert $ Person +undefined "Michael". And now our types will be much simpler, right? For +example, selectList could return a simple [Person] instead of that ugly +[Entity SqlPersist Person].

+

The problem is that the "ugliness" is incredibly useful. Having Entity Person +makes it obvious, at the type level, that we’re dealing with a value that +exists in the database. Let’s say we want to create a link to another page that +requires the PersonId (not an uncommon occurrence as we’ll discuss later). +The Entity Person form gives us unambiguous access to that information; +embedding PersonId within Person with a Maybe wrapper means an extra +runtime check for Just, instead of a more error-proof compile time check.

+

Finally, there’s a semantic mismatch with embedding the ID within the value. +The Person is the value. Two people are identical (in the context of +Haskell) if all their fields are the same. By embedding the ID in the value, +we’re no longer talking about a person, but about a row in the database. +Equality is no longer really equality, it’s identity: is this the same +person, as opposed to an equivalent person.

+

In other words, there are some annoyances with having the ID separated out, but +overall, it’s the right approach, which in the grand scheme of things leads +to better, less buggy code.

+
+
+

Update

+

Now, in the context of that discussion, let’s think about updating. The simplest way to update is:

+
let michael = Person "Michael" 26
+    michaelAfterBirthday = michael { personAge = 27 }
+

But that’s not actually updating anything, it’s just creating a new Person +value based on the old one. When we say update, we’re not talking about +modifications to the values in Haskell. (We better not be of course, since +data in Haskell is immutable.)

+

Instead, we’re looking at ways of modifying rows in a table. And the simplest +way to do that is with the update function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+update personId [PersonAge =. 27]
+

update takes two arguments: an ID, and a list of Updates. The simplest +update is assignment, but it’s not always the best. What if you want to +increase someone’s age by 1, but you don’t have their current age? Persistent +has you covered:

+
haveBirthday personId = update personId [PersonAge +=. 1]
+

And as you might expect, we have all the basic mathematical operators: ++=., -=., *=., and /=. (full stop). These can be convenient for +updating a single record, but they are also essential for proper ACID +guarantees. Imagine the alternative: pull out a Person, increment the age, +and update the new value. If you have two threads/processes working on this +database at the same time, you’re in for a world of hurt (hint: race +conditions).

+

Sometimes you’ll want to update many rows at once (give all your employees a +5% pay increase, for example). updateWhere takes two parameters: a list of +filters, and a list of updates to apply.

+
updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- it's been a long day
+

Occasionally, you’ll just want to completely replace the value in a database +with a different value. For that, you use (surprise) the replace function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+replace personId $ Person "John" "Doe" 20
+
+
+

Delete

+

As much as it pains us, sometimes we must part with our data. To do so, we have three functions:

+
+
+delete +
+

+Delete based on an ID +

+
+
+deleteBy +
+

+Delete based on a unique constraint +

+
+
+deleteWhere +
+

+Delete based on a set of filters +

+
+
+
personId <- insert $ Person "Michael" "Snoyman" 26
+delete personId
+deleteBy $ PersonName "Michael" "Snoyman"
+deleteWhere [PersonFirstName ==. "Michael"]
+

We can even use deleteWhere to wipe out all the records in a table, we just +need to give some hints to GHC as to what table we’re interested in:

+
    deleteWhere ([] :: [Filter Person])
+
+
+
+

Attributes

+

So far, we have seen a basic syntax for our persistLowerCase blocks: a line +for the name of our entities, and then an indented line for each field with two +words: the name of the field and the datatype of the field. Persistent handles +more than this: you can assign an arbitrary list of attributes after the first +two words on a line.

+

Suppose we want to have a Person entity with an (optional) age and the +timestamp of when he/she was added to the system. For entities already in the +database, we want to just use the current date-time for that timestamp.

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+import Control.Monad.IO.Class
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    time <- liftIO getCurrentTime
+    runMigration migrateAll
+    insert $ Person "Michael" (Just 26) time
+    insert $ Person "Greg" Nothing time
+    return ()
+

Maybe is a built in, single word attribute. It makes the field optional. In +Haskell, this means it is wrapped in a Maybe. In SQL, it makes the column +nullable.

+

The default attribute is backend specific, and uses whatever syntax is +understood by the database. In this case, it uses the database’s built-in +CURRENT_TIME function. Suppose that we now want to add a field for a person’s +favorite programming language:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+ +

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:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    country String "default='El Salvador'"
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+

One last trick you can do with attributes is to specify the names to be used +for the SQL tables and columns. This can be convenient when interacting with +existing databases.

+
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"
+    PersonName firstName lastName
+    deriving Show
+|]
+

There are a number of other features to the entity definition syntax. An +up-to-date list is maintained +on the +Yesod wiki.

+
+
+

Relations

+

Persistent allows references between your data types in a manner that is +consistent with supporting non-SQL databases. We do this by embedding an ID in +the related entity. So if a person has many cars:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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
+    name String
+    deriving Show
+Car
+    ownerId PersonId
+    name String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    bruce <- insert $ Person "Bruce Wayne"
+    insert $ Car bruce "Bat Mobile"
+    insert $ Car bruce "Porsche"
+    -- this could go on a while
+    cars <- selectList [CarOwnerId ==. bruce] []
+    liftIO $ print cars
+

Using this technique, you can define one-to-many relationships. To define +many-to-many relationships, we need a join entity, which has a one-to-many +relationship with each of the original tables. It is also a good idea to use +uniqueness constraints on these. For example, to model a situation where we +want to track which people have shopped in which stores:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+Store
+    name String
+PersonStore
+    personId PersonId
+    storeId StoreId
+    UniquePersonStore personId storeId
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    bruce <- insert $ Person "Bruce Wayne"
+    michael <- insert $ Person "Michael"
+
+    target <- insert $ Store "Target"
+    gucci <- insert $ Store "Gucci"
+    sevenEleven <- insert $ Store "7-11"
+
+    insert $ PersonStore bruce gucci
+    insert $ PersonStore bruce sevenEleven
+
+    insert $ PersonStore michael target
+    insert $ PersonStore michael sevenEleven
+
+    return ()
+
+
+

Closer look at types

+

So far, we’ve spoken about Person and PersonId without really explaining +what they are. In the simplest sense, for a SQL-only system, the PersonId +could just be type PersonId = Int64. However, that means there is nothing +binding a PersonId at the type level to the Person entity. As a result, you +could accidentally use a PersonId and get a Car. In order to model this +relationship, we could use phantom types. So, our next naive step would be:

+
newtype Key entity = Key Int64
+type PersonId = Key Person
+

And that works out really well, until you get to a backend that doesn’t use +Int64 for its IDs. And that’s not just a theoretical question; MongoDB uses +ByteStrings instead. So what we need is a key value that can contain an +Int and a ByteString. Seems like a great time for a sum type:

+
data Key entity = KeyInt Int64 | KeyByteString ByteString
+

But that’s just asking for trouble. Next we’ll have a backend that uses +timestamps, so we’ll need to add another constructor to Key. This could go on +for a while. Fortunately, we already have a sum type intended for representing +arbitrary data: PersistValue:

+
newtype Key entity = Key PersistValue
+

And this is (more or less) what Persistent did until version 2.0. However, this +has a different problem: it throws away data. For example, when dealing with a +SQL database, we know that the key type will be an Int64 (assuming defaults +are being used). However, you can’t assert that at the type level with this +construction. So instead, starting with Persistent 2.0, we now use an +associated datatype inside the PersistEntity class:

+
class PersistEntity record where
+    data Key record
+    ...
+

When you’re working with a SQL backend, and aren’t using a custom key type, +this becomes a newtype wrapper around an Int64, and the +toSqlKey/fromSqlKey functions can perform that type-safe conversion for +you. With MongoDB, on the other hand, it’s a wrapper around a ByteString.

+
+

More complicated, more generic

+

By default, Persistent will hard-code your datatypes to work with a specific +database backend. When using sqlSettings, this is the SqlBackend type. But +if you want to write Persistent code that can be used on multiple backends, you +can enable more generic types by replacing sqlSettings with sqlSettings { +mpsGeneric = True }.

+

To understand why this is necessary, consider relations. Let’s say we want to +represent blogs and blog posts. We would use the entity definition:

+
Blog
+    title Text
+Post
+    title Text
+    blogId BlogId
+

We know that BlogId is just a type synonym for Key Blog, but how will Key +Blog be defined? We can’t use an Int64, since that won’t work for MongoDB. +And we can’t use ByteString, since that won’t work for SQL databases.

+

To allow for this, once mpsGeneric is set to True, out resulting datatypes have a type parameter to indicate the database backend they use, so that keys can be properly encoded. This looks like:

+
data BlogGeneric backend = Blog { blogTitle :: Text }
+data PostGeneric backend = Post
+    { postTitle  :: Text
+    , postBlogId :: Key (BlogGeneric backend)
+    }
+

Notice that we still keep the short names for the constructors and the records. +Finally, to give a simple interface for normal code, we define some type +synonyms:

+
type Blog   = BlogGeneric SqlBackend
+type BlogId = Key Blog
+type Post   = PostGeneric SqlBackend
+type PostId = Key Post
+

And no, SqlBackend isn’t hard-coded into Persistent anywhere. That +sqlSettings parameter you’ve been passing to mkPersist is what tells us to +use SqlBackend. Mongo code will use mongoSettings instead.

+

This might be quite complicated under the surface, but user code hardly ever +touches this. Look back through this whole chapter: not once did we need to +deal with the Key or Generic stuff directly. The most common place for it +to pop up is in compiler error messages. So it’s important to be aware that +this exists, but it shouldn’t affect you on a day-to-day basis.

+
+
+
+

Custom Fields

+

Occasionally, you will want to define a custom field to be used in your +datastore. The most common case is an enumeration, such as employment status. +For this, Persistent provides a helper Template Haskell function:

+
-- @Employment.hs
+{-# LANGUAGE TemplateHaskell #-}
+module Employment where
+
+import Database.Persist.TH
+
+data Employment = Employed | Unemployed | Retired
+    deriving (Show, Read, Eq)
+derivePersistField "Employment"
+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Employment
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    employment Employment
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    insert $ Person "Bruce Wayne" Retired
+    insert $ Person "Peter Parker" Unemployed
+    insert $ Person "Michael" Employed
+
+    return ()
+

derivePersistField stores the data in the database using a string field, and +performs marshaling using the Show and Read instances of the datatype. This +may not be as efficient as storing via an integer, but it is much more future +proof: even if you add extra constructors in the future, your data will still +be valid.

+ +
+
+

Persistent: Raw SQL

+

The Persistent package provides a type safe interface to data stores. It tries +to be backend-agnostic, such as not relying on relational features of SQL. My +experience has been you can easily perform 95% of what you need to do with the +high-level interface. (In fact, most of my web apps use the high level +interface exclusively.)

+

But occassionally you’ll want to use a feature that’s specific to a backend. One feature I’ve used in the past is full text search. In this case, we’ll use the SQL "LIKE" operator, which is not modeled in Persistent. We’ll get all people with the last name "Snoyman" and print the records out.

+ +
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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
+    name Text
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael Snoyman"
+    insert $ Person "Miriam Snoyman"
+    insert $ Person "Eliezer Snoyman"
+    insert $ Person "Gavriella Snoyman"
+    insert $ Person "Greg Weber"
+    insert $ Person "Rick Richardson"
+
+    -- 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)
+

There is also higher-level support that allows for automated data marshaling. +Please see the Haddock API docs for more details.

+
+
+

Integration with Yesod

+

So you’ve been convinced of the power of Persistent. How do you integrate it +with your Yesod application? If you use the scaffolding, most of the work is +done for you already. But as we normally do, we’ll build up everything manually +here to point out how it works under the surface.

+

The yesod-persistent package provides the meeting point between Persistent and +Yesod. It provides the YesodPersist typeclass, which standardizes access to +the database via the runDB method. Let’s see this in action.

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import Yesod
+import Database.Persist.Sqlite
+import Control.Monad.Trans.Resource (runResourceT)
+import Control.Monad.Logger (runStderrLoggingT)
+
+-- Define our entities as usual
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    firstName String
+    lastName String
+    age Int
+    deriving Show
+|]
+
+-- We keep our connection pool in the foundation. At program initialization, we
+-- create our initial pool, and each time we need to perform an action we check
+-- out a single connection from the pool.
+data PersistTest = PersistTest ConnectionPool
+
+-- We'll create a single route, to access a person. It's a very common
+-- occurrence to use an Id type in routes.
+mkYesod "PersistTest" [parseRoutes|
+/ HomeR GET
+/person/#PersonId PersonR GET
+|]
+
+-- Nothing special here
+instance Yesod PersistTest
+
+-- Now we need to define a YesodPersist instance, which will keep track of
+-- which backend we're using and how to run an action.
+instance YesodPersist PersistTest where
+    type YesodPersistBackend PersistTest = SqlBackend
+
+    runDB action = do
+        PersistTest pool <- getYesod
+        runSqlPool action pool
+
+-- List all people in the database
+getHomeR :: Handler Html
+getHomeR = do
+    people <- runDB $ selectList [] [Asc PersonAge]
+    defaultLayout
+        [whamlet|
+            <ul>
+                $forall Entity personid person <- people
+                    <li>
+                        <a href=@{PersonR personid}>#{personFirstName person}
+        |]
+
+-- We'll just return the show value of a person, or a 404 if the Person doesn't
+-- exist.
+getPersonR :: PersonId -> Handler String
+getPersonR personId = do
+    person <- runDB $ get404 personId
+    return $ show person
+
+openConnectionCount :: Int
+openConnectionCount = 10
+
+main :: IO ()
+main = runStderrLoggingT $ withSqlitePool "test.db3" openConnectionCount $ \pool -> liftIO $ do
+    runResourceT $ flip runSqlPool pool $ do
+        runMigration migrateAll
+        insert $ Person "Michael" "Snoyman" 26
+    warp 3000 $ PersistTest pool
+

There are two important pieces here for general use. runDB is used to run a +DB action from within a Handler. Within the runDB, you can use any of the +functions we’ve spoken about so far, such as insert and selectList.

+ +

The other new feature is get404. It works just like get, but instead of +returning a Nothing when a result can’t be found, it returns a 404 message +page. The getPersonR function is a very common approach used in real-world +Yesod applications: get404 a value and then return a response based on it.

+
+
+

More complex SQL

+

Persistent strives to be backend-agnostic. The advantage of this approach is +code which easily moves from different backend types. The downside is that you +lose out on some backend-specific features. Probably the biggest casualty is +SQL join support.

+

Fortunately, thanks to Felipe Lessa, you can have your cake and eat it too. The +Esqueleto library provides +support for writing type safe SQL queries, using the existing Persistent +infrastructure. The Haddocks for that package provide a good introduction to +its usage. And since it uses many Persistent concepts, most of your existing +Persistent knowledge should transfer over easily.

+

For a simple example of using Esqueleto, please see the SQL Joins chapter.

+
+
+

Something besides SQLite

+

To keep the examples in this chapter simple, we’ve used the SQLite backend. Just to round things out, here’s our original synopsis rewritten to work with PostgreSQL:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Control.Monad.Logger    (runStderrLoggingT)
+import           Database.Persist
+import           Database.Persist.Postgresql
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+connStr = "host=localhost dbname=test user=test password=test port=5432"
+
+main :: IO ()
+main = runStderrLoggingT $ withPostgresqlPool connStr 10 $ \pool -> liftIO $ do
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        johnId <- insert $ Person "John Doe" $ Just 35
+        janeId <- insert $ Person "Jane Doe" Nothing
+
+        insert $ BlogPost "My fr1st p0st" johnId
+        insert $ BlogPost "One more for good measure" johnId
+
+        oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+        liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+        john <- get johnId
+        liftIO $ print (john :: Maybe Person)
+
+        delete janeId
+        deleteWhere [BlogPostAuthorId ==. johnId]
+
+
+

Summary

+

Persistent brings the type safety of Haskell to your data access layer. Instead +of writing error-prone, untyped data access, or manually writing boilerplate +marshal code, you can rely on Persistent to automate the process for you.

+

The goal is to provide everything you need, most of the time. For the times +when you need something a bit more powerful, Persistent gives you direct access +to the underlying data store, so you can write whatever 5-way joins you want.

+

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/wiki

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/restful-content.html b/public/book-1.4/restful-content.html new file mode 100644 index 00000000..90dd4664 --- /dev/null +++ b/public/book-1.4/restful-content.html @@ -0,0 +1,601 @@ + RESTful Content :: Yesod Web Framework Book- Version 1.4 +
+

RESTful Content

+ + +

One of the stories from the early days of the web is how search engines wiped +out entire websites. When dynamic web sites were still a new concept, +developers didn’t appreciate the difference between a GET and POST request. +As a result, they created pages- accessed with the GET method- that would +delete pages. When search engines started crawling these sites, they could wipe +out all the content.

+

If these web developers had followed the HTTP spec properly, this would not +have happened. A GET request is supposed to cause no side effects (you know, +like wiping out a site). Recently, there has been a move in web development to +properly embrace Representational State Transfer, also known as REST. This +chapter describes the RESTful features in Yesod and how you can use them to +create more robust web applications.

+
+

Request methods

+

In many web frameworks, you write one handler function per resource. In Yesod, +the default is to have a separate handler function for each request method. The +two most common request methods you will deal with in creating web sites are +GET and POST. These are the most well-supported methods in HTML, since they +are the only ones supported by web forms. However, when creating RESTful APIs, +the other methods are very useful.

+

Technically speaking, you can create whichever request methods you like, but it +is strongly recommended to stick to the ones spelled out in the HTTP spec. The +most common of these are:

+
+
+GET +
+

+Read-only requests. Assuming no other changes occur on the server, +calling a GET request multiple times should result in the same response, +barring such things as "current time" or randomly assigned results. +

+
+
+POST +
+

+A general mutating request. A POST request should never be submitted +twice by the user. A common example of this would be to transfer funds from one +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. +

+
+
+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 +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.

+

See the routing and handlers chapter for more information on the syntax +of defining handler functions for each request method.

+
+
+

Representations

+

Suppose we have a Haskell datatype and value:

+
data Person = Person { name :: String, age :: Int }
+michael = Person "Michael" 25
+

We could represent that data as HTML:

+
<table>
+    <tr>
+        <th>Name</th>
+        <td>Michael</td>
+    </tr>
+    <tr>
+        <th>Age</th>
+        <td>25</td>
+    </tr>
+</table>
+

or we could represent it as JSON:

+
{"name":"Michael","age":25}
+

or as XML:

+
<person>
+    <name>Michael</name>
+    <age>25</age>
+</person>
+

Often times, web applications will use a different URL to get each of these +representations; perhaps /person/michael.html, /person/michael.json, etc. +Yesod follows the RESTful principle of a single URL for each resource. So in +Yesod, all of these would be accessed from /person/michael.

+

Then the question becomes how do we determine which representation to serve. +The answer is the HTTP Accept header: it gives a prioritized list of content +types the client is expecting. Yesod provides a pair of functions to abstract +away the details of parsing that header directly, and instead allows you to +talk at a much higher level of representations. Let’s make that last sentence +a bit more concrete with some code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideRep $ return $ object
+        [ "name" .= name
+        , "age" .= age
+        ]
+  where
+    name = "Michael" :: Text
+    age = 28 :: Int
+
+main :: IO ()
+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 +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 +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 +little bit.

+

To test this out, start up the server and then try running the following +different curl commands:

+
curl http://localhost:3000 --header "accept: application/json"
+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 +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 +"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 +from your browser. To try this out, you can visit +http://localhost:3000/?_accept=application/json.

+
+

JSON conveniences

+

Since JSON is such a commonly used data format in web applications today, we +have some built-in helper functions for providing JSON representations. These +are built off of the wonderful aeson library, so let’s start off with a quick +explanation of how that library works.

+

aeson has a core datatype, Value, which represents any valid JSON value. It +also provides two typeclasses- ToJSON and FromJSON- to automate +marshaling to and from JSON values, respectively. For our purposes, we’re +currently interested in ToJSON. Let’s look at a quick example of creating a +ToJSON instance for our ever-recurring Person data type examples.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+import           Data.Aeson
+import qualified Data.ByteString.Lazy.Char8 as L
+import           Data.Text                  (Text)
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+main :: IO ()
+main = L.putStrLn $ encode $ Person "Michael" 28
+

I won’t go into further detail on aeson, as +the Haddock documentation +already provides a great introduction to the library. What I’ve described so +far is enough to understand our convenience functions.

+

Let’s suppose that you have such a Person datatype, with a corresponding +value, and you’d like to use it as the representation for your current page. +For that, you can use the returnJson function.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Value
+getHomeR = returnJson $ Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

returnJson is actually a trivial function; it is implemented as return . +toJSON. However, it makes things just a bit more convenient. Similarly, if you +would like to provide a JSON value as a representation inside a selectRep, +you can use provideJson.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideJson person
+  where
+    person@Person {..} = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

provideJson is similarly trivial, in this case provideRep . returnJson.

+
+
+

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 +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.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+getHomeR :: Handler TypedContent
+getHomeR =
+    return $ TypedContent mimeType $ toContent $ show person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

There are a few important things to note here.

+
    +
  • +

    +We’ve used the toContent function. This is a typeclass function that can + convert a number of data types to raw data ready to be sent over the wire. In + this case, we’ve used the instance for String, which uses UTF8 encoding. + Other common data types with instances are Text, ByteString, Html, and + 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 + alias for a strict ByteString. +

    +
  • +
+

That’s all well and good, but it bothers me that the type signature for +getHomeR is so uninformative. Also, the implementation of getHomeR looks +pretty boilerplate. I’d rather just have a datatype representing "Haskell Show" +data, and provide some simple means of creating such values. Let’s try this on +for size:

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+
+getHomeR :: Handler HaskellShow
+getHomeR =
+    return $ HaskellShow person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

The magic here lies in two typeclasses. As we mentioned before, ToContent +tells how to convert a value into a raw response. In our case, we would like to +show the original value to get a String, and then convert that String +into the raw content. Often times, instances of ToContent will build on each +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.

+

Finally, let’s make this a bit more complicated, and get this to play well with +selectRep.

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE RecordWildCards           #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+instance HasContentType HaskellShow where
+    getContentType _ = mimeType
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return $ HaskellShow person
+    provideJson person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+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. +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.

+ +
+
+
+

Other request headers

+

There are a great deal of other request headers available. Some of them only +affect the transfer of data between the server and client, and should not +affect the application at all. For example, Accept-Encoding informs the +server which compression schemes the client understands, and Host informs the +server which virtual host to serve up.

+

Other headers do affect the application, but are automatically read by Yesod. +For example, the Accept-Language header specifies which human language +(English, Spanish, German, Swiss-German) the client prefers. See the i18n +chapter for details on how this header is used.

+
+
+

Summary

+

Yesod adheres to the following tenets of REST:

+
    +
  • +

    +Use the correct request method. +

    +
  • +
  • +

    +Each resource should have precisely one URL. +

    +
  • +
  • +

    +Allow multiple representations of data on the same URL. +

    +
  • +
  • +

    +Inspect request headers to determine extra information about what the client wants. +

    +
  • +
+

This makes it easy to use Yesod not just for building websites, but for +building APIs. In fact, using techniques such as selectRep/provideRep, you +can serve both a user-friendly, HTML page and a machine-friendly, JSON page +from the same URL.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/route-attributes.html b/public/book-1.4/route-attributes.html new file mode 100644 index 00000000..6ccffc93 --- /dev/null +++ b/public/book-1.4/route-attributes.html @@ -0,0 +1,324 @@ + Route attributes :: Yesod Web Framework Book- Version 1.4 +
+

Route attributes

+ + +

Route attributes allow you to set some metadata on each of your routes, in the +routes description itself. The syntax is trivial: just an exclamation point +followed by a value. Using it is also trivial: just use the routeAttrs +function.

+

It’s easiest to understand how it all fits together, and when you might want it, with a motivating example. The case I personally most use this for is annotating administrative routes. Imagine having a website with about 12 different admin actions. You could manually add a call to requireAdmin or some such at the beginning of each action, but:

+
    +
  1. +

    +It’s tedious. +

    +
  2. +
  3. +

    +It’s error prone: you could easily forget one. +

    +
  4. +
  5. +

    +Worse yet, it’s not easy to notice that you’ve missed one. +

    +
  6. +
+

Modifying your isAuthorized method with an explicit list of administrative +routes is a bit better, but it’s still difficult to see at a glance when you’ve +missed one.

+

This is why I like to use route attributes for this: you add a single word to +each relevant part of the route definition, and then you just check for that +attribute in isAuthorized. Let’s see the code!

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/unprotected UnprotectedR GET
+/admin1 Admin1R GET !admin
+/admin2 Admin2R GET !admin
+/admin3 Admin3R GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+    isAuthorized route _writable
+        | "admin" `member` routeAttrs route = do
+            muser <- maybeAuthId
+            case muser of
+                Nothing -> return AuthenticationRequired
+                Just ident
+                    -- Just a hack since we're using the dummy module
+                    | ident == "admin" -> return Authorized
+                    | otherwise -> return $ Unauthorized "Admin access only"
+        | otherwise = return Authorized
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Hacky YesodAuth instance for just the dummy auth plugin
+instance YesodAuth App where
+    type AuthId App = Text
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authPlugins _ = [authDummy]
+    maybeAuthId = lookupSession credsKey
+    authHttpManager = error "no http manager provided"
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Route attr homepage"
+    [whamlet|
+        <p>
+            <a href=@{UnprotectedR}>Unprotected
+        <p>
+            <a href=@{Admin1R}>Admin 1
+        <p>
+            <a href=@{Admin2R}>Admin 2
+        <p>
+            <a href=@{Admin3R}>Admin 3
+    |]
+
+getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
+getUnprotectedR = defaultLayout [whamlet|Unprotected|]
+getAdmin1R = defaultLayout [whamlet|Admin1|]
+getAdmin2R = defaultLayout [whamlet|Admin2|]
+getAdmin3R = defaultLayout [whamlet|Admin3|]
+
+main :: IO ()
+main = warp 3000 App
+

And it was so glaring, I bet you even caught the security hole about Admin3R.

+
+

Alternative approach: hierarchical routes

+

Another approach that can be used in some cases is hierarchical routes. This +allows you to group a number of related routes under a single parent. If you +want to keep all of your admin routes under a single URL structure (e.g., +/admin), this can be a good solution. Using them is fairly simple. You need +to add a line to your routes declaration with a path, a name, and a colon, +e.g.:

+
/admin AdminR:
+

Then, you place all children routes beneath that line, and indented at least one space, e.g.:

+
    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+

To refer to these routes using type-safe URLs, you simply wrap them with the +AdminR constructor, e.g. AdminR Admin1R. Here is the previous route +attribute example rewritten to use hierarchical routes:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/unprotected UnprotectedR GET
+/admin AdminR:
+    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+    isAuthorized (AdminR _) _writable = do
+        muser <- maybeAuthId
+        case muser of
+            Nothing -> return AuthenticationRequired
+            Just ident
+                -- Just a hack since we're using the dummy module
+                | ident == "admin" -> return Authorized
+                | otherwise -> return $ Unauthorized "Admin access only"
+    isAuthorized _route _writable = return Authorized
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Hacky YesodAuth instance for just the dummy auth plugin
+instance YesodAuth App where
+    type AuthId App = Text
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authPlugins _ = [authDummy]
+    maybeAuthId = lookupSession credsKey
+    authHttpManager = error "no http manager provided"
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Route attr homepage"
+    [whamlet|
+        <p>
+            <a href=@{UnprotectedR}>Unprotected
+        <p>
+            <a href=@{AdminR Admin1R}>Admin 1
+        <p>
+            <a href=@{AdminR Admin2R}>Admin 2
+        <p>
+            <a href=@{AdminR Admin3R}>Admin 3
+    |]
+
+getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
+getUnprotectedR = defaultLayout [whamlet|Unprotected|]
+getAdmin1R = defaultLayout [whamlet|Admin1|]
+getAdmin2R = defaultLayout [whamlet|Admin2|]
+getAdmin3R = defaultLayout [whamlet|Admin3|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Hierarchical routes with attributes

+

Of course, you can mix the two approaches. Children of a hierarchical route will inherit the attributes of their parents, e.g.:

+
/admin AdminR !admin:
+    /1 Admin1R GET !1
+    /2 Admin2R GET !2
+    /3 Admin3R Get !3
+

AdminR Admin1R has the admin and 1 attributes.

+

With this technique, you can use the admin attributes in the isAuthorized function, like in the first example. +You are also sure that you won’t forget any attributes as we did with Admin3R. +Compared to the original code corresponding to the hierarchical route, this method has no real benefit : both methods being somehow equivalent. +We replaced the pattern matching on (AdminR _) with "admin" member routeAttrs route. +However, the benefit becomes more obvious when the admin pages are not all grouped under the same url structures but belong to different subtrees, e.g:

+
/admin AdminR !admin:
+    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R Get
+
+/a AR !a:
+  /1 A1R GET
+  /2 A2R GET
+  /admin AAdminR !admin:
+    /1 AAdmin1R GET
+    /2 AAdmin2R GET
+

The pages under /admin and /a/admin have all the admin attribute and can be checked using "admin" member routeAttrs route. Pattern matching on (AdminR _) will not work for this example and only match /admin/* routes but not /a/admin/\*

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/routing-and-handlers.html b/public/book-1.4/routing-and-handlers.html new file mode 100644 index 00000000..e56a6e68 --- /dev/null +++ b/public/book-1.4/routing-and-handlers.html @@ -0,0 +1,792 @@ + Routing and Handlers :: Yesod Web Framework Book- Version 1.4 +
+

Routing and Handlers

+ + +

If we look at Yesod as a Model-View-Controller framework, routing and handlers +make up the controller. For contrast, let’s describe two other routing +approaches used in other web development environments:

+
    +
  • +

    +Dispatch based on file name. This is how PHP and ASP work, for example. +

    +
  • +
  • +

    +Have a centralized routing function that parses routes based on regular + expressions. Django and Rails follow this approach. +

    +
  • +
+

Yesod is closer in principle to the latter technique. Even so, there are +significant differences. Instead of using regular expressions, Yesod matches on +pieces of a route. Instead of having a one-way route-to-handler mapping, Yesod +has an intermediate data type (called the route datatype, or a type-safe URL) +and creates two-way conversion functions.

+

Coding this more advanced system manually is tedious and error prone. +Therefore, Yesod defines a Domain Specific Language (DSL) for specifying +routes, and provides Template Haskell functions to convert this DSL to Haskell +code. This chapter will explain the syntax of the routing declarations, give +you a glimpse of what code is generated for you, and explain the interaction +between routing and handler functions.

+
+

Route Syntax

+

Instead of trying to shoe-horn route declarations into an existing syntax, +Yesod’s approach is to use a simplified syntax designed just for routes. This +has the advantage of making the code not only easy to write, but simple enough +for someone with no Yesod experience to read and understand the sitemap of your +application.

+

A basic example of this syntax is:

+
/             HomeR     GET
+/blog         BlogR     GET POST
+/blog/#BlogId BlogPostR GET POST
+
+/static       StaticR   Static getStatic
+

The next few sections will explain the full details of what goes on in the +route declaration.

+
+

Pieces

+

One of the first thing Yesod does when it gets a request is split up the +requested path into pieces. The pieces are tokenized at all forward slashes. +For example:

+
toPieces "/" = []
+toPieces "/foo/bar/baz/" = ["foo", "bar", "baz", ""]
+

You may notice that there are some funny things going on with trailing slashes, +or double slashes ("/foo//bar//"), or a few other things. Yesod believes in +having canonical URLs; if users request a URL with a trailing slash, or with a +double slash, they are automatically redirected to the canonical version. This +ensures you have one URL for one resource, and can help with your search +rankings.

+

What this means for you is that you needn’t concern yourself with the exact +structure of your URLs: you can safely think about pieces of a path, and Yesod +automatically handles intercalating the slashes and escaping problematic +characters.

+

If, by the way, you want more fine-tuned control of how paths are split into +pieces and joined together again, you’ll want to look at the cleanPath and +joinPath methods in the Yesod typeclass chapter.

+
+

Types of Pieces

+

When you are declaring your routes, you have three types of pieces at your +disposal:

+
+
+Static +
+

+This is a plain string that must be matched against precisely in the URL. +

+
+
+Dynamic single +
+

+This is a single piece (ie, between two forward slashes), but +represents a user-submitted value. This is the primary method of receiving +extra user input on a page request. These pieces begin with a hash (#) and are +followed by a data type. The datatype must be an instance of PathPiece. +

+
+
+Dynamic multi +
+

+The same as before, but can receive multiple pieces of the URL. +This must always be the last piece in a resource pattern. It is specified by an +asterisk (*) followed by a datatype, which must be an instance of +PathMultiPiece. Multi pieces are not as common as the other two, though they +are very important for implementing features like static trees representing +file structure or wikis with arbitrary hierarchies. +

+
+
+ +

Let us take a look at some standard kinds of resource patterns you may want to +write. Starting simply, the root of an application will just be /. Similarly, +you may want to place your FAQ at /page/faq.

+

Now let’s say you are going to write a Fibonacci website. You may construct +your URLs like /fib/#Int. But there’s a slight problem with this: we do not +want to allow negative numbers or zero to be passed into our application. +Fortunately, the type system can protect us:

+
newtype Natural = Natural Int
+instance PathPiece Natural where
+    toPathPiece (Natural i) = T.pack $ show i
+    fromPathPiece s =
+        case reads $ T.unpack s of
+            (i, ""):_
+                | i < 1 -> Nothing
+                | otherwise -> Just $ Natural i
+            [] -> Nothing
+

On line 1 we define a simple newtype wrapper around Int to protect ourselves +from invalid input. We can see that PathPiece is a typeclass with two +methods. toPathPiece does nothing more than convert to a Text. +fromPathPiece attempts to convert a Text to our datatype, returning +Nothing when this conversion is impossible. By using this datatype, we can +ensure that our handler function is only ever given natural numbers, allowing +us to once again use the type system to battle the boundary issue.

+ +

Defining a PathMultiPiece is just as simple. Let’s say we want to have a Wiki +with at least two levels of hierarchy; we might define a datatype such as:

+
data Page = Page Text Text [Text] -- 2 or more
+instance PathMultiPiece Page where
+    toPathMultiPiece (Page x y z) = x : y : z
+    fromPathMultiPiece (x:y:z) = Just $ Page x y z
+    fromPathMultiPiece _ = Nothing
+
+
+

Overlap checking

+

By default, Yesod will ensure that no two routes have the potential to overlap +with each other. So, for example, consider the following routes:

+
/foo/bar   Foo1R GET
+/foo/#Text Foo2R GET
+

This route declaration will be rejected as overlapping, since /foo/bar will +match both routes. However, there are two cases where we may wish to allow +overlapping:

+
    +
  1. +

    +We know by the definition of our datatype that the overlap can never happen. For example, if you replace Text with Int above, it’s easy to convince yourself that there’s no route that exists that will overlap. Yesod is currently not capable of performing such an analysis. +

    +
  2. +
  3. +

    +You have some extra knowledge about how your application operates, and know that such a situation should never be allowed. For example, if the Foo2R route should never be allowed to receive the parameter bar. +

    +
  4. +
+

You can turn off overlap checking by using the exclamation mark at the +beginning of your route. For example, the following will be accepted by Yesod:

+
/foo/bar    Foo1R GET
+!/foo/#Int  Foo2R GET
+!/foo/#Text Foo3R GET
+ +

One issue that overlapping routes introduce is ambiguity. In the example above, +should /foo/bar route to Foo1R or Foo3R? And should /foo/42 route to +Foo2R or Foo3R? Yesod’s rule for this is simple: first route wins.

+
+
+
+

Resource name

+

Each resource pattern also has a name associated with it. That name will become +the constructor for the type safe URL datatype associated with your +application. Therefore, it has to start with a capital letter. By convention, +these resource names all end with a capital R. There is nothing forcing you to +do this, it is just common practice.

+

The exact definition of our constructor depends upon the resource pattern it is +attached to. Whatever datatypes are used as single-pieces or multi-pieces of the +pattern become arguments to the datatype. This gives us a 1-to-1 correspondence +between our type-safe URL values and valid URLs in our application.

+ +

Let’s get some real examples going here. If you had the resource patterns +/person/#Text named PersonR, /year/#Int named YearR and /page/faq +named FaqR, you would end up with a route data type roughly looking like:

+
data MyRoute = PersonR Text
+             | YearR Int
+             | FaqR
+

If a user requests /year/2009, Yesod will convert it into the value YearR +2009. /person/Michael becomes PersonR "Michael" and /page/faq becomes +FaqR. On the other hand, /year/two-thousand-nine, /person/michael/snoyman +and /page/FAQ would all result in 404 errors without ever seeing your code.

+
+
+

Handler specification

+

The last piece of the puzzle when declaring your resources is how they will be +handled. There are three options in Yesod:

+
    +
  • +

    +A single handler function for all request methods on a given route. +

    +
  • +
  • +

    +A separate handler function for each request method on a given route. Any + other request method will generate a 405 Method Not Allowed response. +

    +
  • +
  • +

    +You want to pass off to a subsite. +

    +
  • +
+

The first two can be easily specified. A single handler function will be a line +with just a resource pattern and the resource name, such as /page/faq FaqR. +In this case, the handler function must be named handleFaqR.

+

A separate handler for each request method will be the same, plus a list of +request methods. The request methods must be all capital letters. For example, +/person/#String PersonR GET POST DELETE. In this case, you would need to +define three handler functions: getPersonR, postPersonR and +deletePersonR.

+

Subsites are a very useful— but more complicated— topic in Yesod. We will cover +writing subsites later, but using them is not too difficult. The most commonly +used subsite is the static subsite, which serves static files for your +application. In order to serve static files from /static, you would need a +resource line like:

+
/static StaticR Static getStatic
+

In this line, /static just says where in your URL structure to serve the +static files from. There is nothing magical about the word static, you could +easily replace it with /my/non-dynamic/files.

+

The next word, StaticR, gives the resource name. The next two words +specify that we are using a subsite. Static is the name of the subsite +foundation datatype, and getStatic is a function that gets a Static value +from a value of your master foundation datatype.

+

Let’s not get too caught up in the details of subsites now. We will look more +closely at the static subsite in the scaffolded site chapter.

+
+
+
+

Dispatch

+

Once you have specified your routes, Yesod will take care of all the pesky +details of URL dispatch for you. You just need to make sure to provide the +appropriate handler functions. For subsite routes, you do not need to write any +handler functions, but you do for the other two. We mentioned the naming rules +above (MyHandlerR GET becomes getMyHandlerR, MyOtherHandlerR becomes +handleMyOtherHandlerR).

+

Now that we know which functions we need to write, let’s figure out what their +type signatures should be.

+
+

Return Type

+

Let’s look at a simple handler function:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

There are two components to this return type: Handler and Html. Let’s +analyze each in more depth.

+
+

Handler monad

+

Like the Widget type, the Handler data type is not defined anywhere in the +Yesod libraries. Instead, the libraries provide the data type:

+
data HandlerT site m a
+

And like WidgetT, this has three arguments: a base monad m, a monadic value +a, and the foundation data type site. Each application defines a Handler +synonym which constrains site to that application’s foundation data type, and +sets m to IO. If your foundation is MyApp, in other words, you’d have the +synonym:

+
type Handler = HandlerT MyApp IO
+

We need to be able to modify the underlying monad when writing subsites, but +otherwise we’ll use IO.

+

The HandlerT monad provides access to information about the user request +(e.g. query-string parameters), allows modifying the response (e.g., response +headers), and more. This is the monad that most of your Yesod code will live +in.

+

In addition, there’s a type class called MonadHandler. Both HandlerT and +WidgetT are instances of this type class, allowing many common functions to +be used in both monads. If you see MonadHandler in any API documentation, you +should remember that the function can be used in your Handler functions.

+
+
+

Html

+

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 +can be returned?

+

In order to generate a response, we need to know two pieces of information: +the content type (e.g., text/html, image/png) and how to serialize it to a +stream of bytes. This is represented by the TypedContent data type:

+
data TypedContent = TypedContent !ContentType !Content
+

We also have a type class for all data types which can be converted to a +TypedContent:

+
class ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

Many common data types are instances of this type class, including Html, +Value (from the aeson package, representing JSON), Text, and even () (for +representing an empty response).

+
+
+
+

Arguments

+

Let’s return to our simple example from above:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

Not every route is as simple as this HomeR. Take for instance our PersonR +route from earlier. The name of the person needs to be passed to the handler +function. This translation is very straight-forward, and hopefully intuitive. +For example:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Yesod
+
+data App = App
+instance Yesod App
+
+mkYesod "App" [parseRoutes|
+/person/#Text PersonR GET
+/year/#Integer/month/#Text/day/#Int DateR
+/wiki/*Texts WikiR GET
+|]
+
+getPersonR :: Text -> Handler Html
+getPersonR name = defaultLayout [whamlet|<h1>Hello #{name}!|]
+
+handleDateR :: Integer -> Text -> Int -> Handler Text -- text/plain
+handleDateR year month day =
+    return $
+        T.concat [month, " ", T.pack $ show day, ", ", T.pack $ show year]
+
+getWikiR :: [Text] -> Handler Text
+getWikiR = return . T.unwords
+
+main :: IO ()
+main = warp 3000 App
+

The arguments have the types of the dynamic pieces for each route, in the order +specified. Also, notice how we are able to use both Html and Text return +values.

+
+
+
+

The Handler functions

+

Since the majority of your code will live in the Handler monad, it’s +important to invest some time in understanding it better. The remainder of this +chapter will give a brief introduction to some of the most common functions +living in the Handler monad. I am specifically not covering any of the +session functions; that will be addressed in the sessions chapter.

+
+

Application Information

+

There are a number of functions that return information about your application +as a whole, and give no information about individual requests. Some of these +are:

+
+
+getYesod +
+

+Returns your application foundation value. If you store configuration +values in your foundation, you will probably end up using this function a lot. +(If you’re so inclined, you can also use ask from Control.Monad.Reader; +getYesod is simply a type-constrained synonym for it.) +

+
+
+getUrlRender +
+

+Returns the URL rendering function, which converts a type-safe +URL into a Text. Most of the time- like with Hamlet- Yesod calls this +function for you, but you may occasionally need to call it directly. +

+
+
+getUrlRenderParams +
+

+A variant of getUrlRender that converts both a type-safe +URL and a list of query-string parameters. This function handles all +percent-encoding necessary. +

+
+
+
+
+

Request Information

+

The most common information you will want to get about the current request is +the requested path, the query string parameters and POSTed form data. The +first of those is dealt with in the routing, as described above. The other two +are best dealt with using the forms module.

+

That said, you will sometimes need to get the data in a more raw format. For +this purpose, Yesod exposes the YesodRequest datatype along with the +getRequest function to retrieve it. This gives you access to the full list of +GET parameters, cookies, and preferred languages. There are some convenient +functions to make these lookups easier, such as lookupGetParam, +lookupCookie and languages. For raw access to the POST parameters, you +should use runRequestBody.

+

If you need even more raw data, like request headers, you can use waiRequest +to access the Web Application Interface (WAI) request value. See the WAI +appendix for more details.

+
+
+

Short Circuiting

+

The following functions immediately end execution of a handler function and +return a result to the user.

+
+
+redirect +
+

+Sends a redirect response to the user (a 303 response). If you want to use a different response code (e.g., a permanent 301 redirect), you can use redirectWith. +

+
+
+ +
+
+notFound +
+

+Return a 404 response. This can be useful if a user requests a +database value that doesn’t exist. +

+
+
+permissionDenied +
+

+Return a 403 response with a specific error message. +

+
+
+invalidArgs +
+

+A 400 response with a list of invalid arguments. +

+
+
+sendFile +
+

+Sends a file from the filesystem with a specified content type. This +is the preferred way to send static files, since the underlying WAI handler may +be able to optimize this to a sendfile system call. Using readFile for +sending static files should not be necessary. +

+
+
+sendResponse +
+

+Send a normal response with a 200 status code. This is really +just a convenience for when you need to break out of some deeply nested code +with an immediate response. Any instance of ToTypedContent may be used. +

+
+
+sendWaiResponse +
+

+When you need to get low-level and send out a raw WAI +response. This can be especially useful for creating streaming responses or a +technique like server-sent events. +

+
+
+
+
+

Response Headers

+
+
+setCookie +
+

+Set a cookie on the client. Instead of taking an expiration date, +this function takes a cookie duration in minutes. Remember, you won’t see this +cookie using lookupCookie until the following request. +

+
+
+deleteCookie +
+

+Tells the client to remove a cookie. Once again, lookupCookie +will not reflect this change until the next request. +

+
+
+setHeader +
+

+Set an arbitrary response header. +

+
+
+setLanguage +
+

+Set the preferred user language, which will show up in the result +of the languages function. +

+
+
+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 +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. +

+
+
+alreadyExpired +
+

+Sets the Expires header to the past. +

+
+
+expiresAt +
+

+Sets the Expires header to the specified date/time. +

+
+
+
+
+
+

I/O and debugging

+

The HandlerT and WidgetT monad transformers are both instances of a number +of typeclasses. For this section, the important typeclasses are MonadIO and +MonadLogger. The former allows you to perform arbitrary IO actions inside +your handler, such as reading from a file. In order to achieve this, you just +need to prepend liftIO to the call.

+

MonadLogger provides a built-in logging system. There are many ways you can +customize this system, including what messages get logged and where logs are +sent. By default, logs are sent to standard output, in development all messages +are logged, and in production, warnings and errors are logged.

+

Often times when logging, we want to know where in the source code the logging +occured. For this, MonadLogger provides a number of convenience Template +Haskell functions which will automatically insert source code location into the +log messages. These functions are $logDebug, $logInfo, $logWarn, and +$logError. Let’s look at a short example of some of these functions.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Exception (IOException, try)
+import           Control.Monad     (when)
+import           Yesod
+
+data App = App
+instance Yesod App where
+    -- This function controls which messages are logged
+    shouldLog App src level =
+        True -- good for development
+        -- level == LevelWarn || level == LevelError -- good for production
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = do
+    $logDebug "Trying to read data file"
+    edata <- liftIO $ try $ readFile "datafile.txt"
+    case edata :: Either IOException String of
+        Left e -> do
+            $logError $ "Could not read datafile.txt"
+            defaultLayout [whamlet|An error occurred|]
+        Right str -> do
+            $logInfo "Reading of data file succeeded"
+            let ls = lines str
+            when (length ls < 5) $ $logWarn "Less than 5 lines of data"
+            defaultLayout
+                [whamlet|
+                    <ol>
+                        $forall l <- ls
+                            <li>#{l}
+                |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Query string and hash fragments

+

We’ve looked at a number of functions which work on URL-like things, such as redirect. These functions all work with type-safe URLs, but what else do they work with? There’s a typeclass called RedirectUrl which contains the logic for converting some type into a textual URL. This includes type-safe URLs, textual URLs, and two special instances:

+
    +
  1. +

    +A tuple of a URL and a list of key/value pairs of query string parameters. +

    +
  2. +
  3. +

    +The Fragment datatype, used for adding a hash fragment to the end of a URL. +

    +
  4. +
+

Both of these instances allow you to "add on" extra information to a type-safe +URL. Let’s see some examples of how these can be used:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text        (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+/link1 Link1R GET
+/link2 Link2R GET
+/link3 Link3R GET
+/link4 Link4R GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Redirects"
+    [whamlet|
+        <p>
+            <a href=@{Link1R}>Click to start the redirect chain!
+    |]
+
+getLink1R, getLink2R, getLink3R :: Handler ()
+getLink1R = redirect Link2R -- /link2
+getLink2R = redirect (Link3R, [("foo", "bar")]) -- /link3?foo=bar
+getLink3R = redirect $ Link4R :#: ("baz" :: Text) -- /link4#baz
+
+getLink4R :: Handler Html
+getLink4R = defaultLayout
+    [whamlet|
+        <p>You made it!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Of course, inside a Hamlet template this is usually not necessary, as you can simply include the hash after the URL directly, e.g.:

+
<a href=@{Link1R}#somehash>Link to hash
+
+
+

Summary

+

Routing and dispatch is arguably the core of Yesod: it is from here that our +type-safe URLs are defined, and the majority of our code is written within the +Handler monad. This chapter covered some of the most important and central +concepts of Yesod, so it is important that you properly digest it.

+

This chapter also hinted at a number of more complex Yesod topics that we will +be covering later. But you should be able to write some very sophisticated web +applications with just the knowledge you have learned up until here.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/scaffolding-and-the-site-template.html b/public/book-1.4/scaffolding-and-the-site-template.html new file mode 100644 index 00000000..7f00aa35 --- /dev/null +++ b/public/book-1.4/scaffolding-and-the-site-template.html @@ -0,0 +1,505 @@ + Scaffolding and the Site Template :: Yesod Web Framework Book- Version 1.4 +
+

Scaffolding and the Site Template

+ + +

So you’re tired of running small examples, and ready to write a real site? Then +you’re at the right chapter. Even with the entire Yesod library at your +fingertips, there are still a lot of steps you need to go through to get a +production-quality site setup:

+
    +
  • +

    +Config file parsing +

    +
  • +
  • +

    +Signal handling (*nix) +

    +
  • +
  • +

    +More efficient static file serving +

    +
  • +
  • +

    +A good file layout +

    +
  • +
+

The scaffolded site is a combination of many Yesoders' best practices combined +together into a ready-to-use skeleton for your sites. It is highly recommended +for all sites. This chapter will explain the overall structure of the +scaffolding, how to use it, and some of its less-than-obvious features.

+

For the most part, this chapter will not contain code samples. It is +recommended that you follow along with an actual scaffolded site.

+ +
+

How to Scaffold

+

The yesod-bin package installs an executable (conveniently named +yesod as well). This executable provides a few commands (run stack +exec -- yesod --help to get a list). In order to generate a +scaffolding, the command is stack new my-project +yesod-postgres. This will generate a scaffolding site with a postgres +database backend in a directory named my-project. You can see the +other available templates using the command stack templates.

+

The key thing differing in various available templates (from the +stack templates command) is the database backend. You get a few +choices here, including SQL and MongoDB backends, as well as +"yesod-simple" template to include no database support. This last +option also turns off a few extra dependencies, giving you a leaner +overall site. The remainder of this chapter will focus on the +scaffoldings for one of the database backends. There will be minor +differences for the yesod-simple backend.

+

After creating your files, install the yesod command line tool inside +the project: stack build yesod-bin cabal-install. Then do a stack +build inside the directory. In particular, the commands provided +will ensure that any missing dependencies are built and installed.

+

Finally, to launch your development site, you would use stack exec -- yesod devel. +This site will automatically rebuild and reload whenever you change your code.

+
+
+

File Structure

+

The scaffolded site is built as a fully cabalized Haskell package. In addition +to source files, config files, templates, and static files are produced as +well.

+
+

Cabal file

+

Whether directly using stack, or indirectly using stack exec -- yesod devel, building +your code will always go through the cabal file. If you open the file, you’ll +see that there are both library and executable blocks. If the library-only +flag is turned on, then the executable block is not built. This is how yesod +devel calls your app. Otherwise, the executable is built.

+

The library-only flag should only be used by yesod devel; you should never +be explicitly passing it into cabal. There is an additional flag, dev, that +allows cabal to build an executable, but turns on some of the same features as +the library-only flag, i.e., no optimizations and reload versions of the +Shakespearean template functions.

+

In general, you will build as follows:

+
    +
  • +

    +When developing, use yesod devel exclusively. +

    +
  • +
  • +

    +When building a production build, perform stack clean && + stack build. This will produce an optimized executable in your + dist folder. (You can also use the yesod keter command for + this.) +

    +
  • +
+

You might be surprised to see the NoImplicitPrelude extension. We turn this +on since the site includes its own module, Import, with a few changes to the +Prelude that make working with Yesod a little more convenient.

+

The last thing to note is the exposed-modules list. If you add any modules to +your application, you must update this list to get yesod devel to work +correctly. Unfortunately, neither Cabal nor GHC will give you a warning if you +forgot to make this update, and instead you’ll get a very scary-looking error +message from yesod devel.

+
+
+

Routes and entities

+

Multiple times in this book, you’ve seen a comment like “We’re declaring our +routes/entities with quasiquotes for convenience. In a production site, you +should use an external file.” The scaffolding uses such an external file.

+

Routes are defined in config/routes, and entities in config/models. They +have the exact same syntax as the quasiquoting you’ve seen throughout the book, +and yesod devel knows to automatically recompile the appropriate modules when +these files change.

+

The models files is referenced by Model.hs. You are free to declare +whatever you like in this file, but here are some guidelines:

+
    +
  • +

    +Any data types used in entities must be imported/declared in Model.hs, + above the persistFileWith call. +

    +
  • +
  • +

    +Helper utilities should either be declared in Import.hs or, if very + model-centric, in a file within the Model folder and imported into + Import.hs. +

    +
  • +
+
+
+

Foundation and Application modules

+

The mkYesod function which we have used throughout the book declares a few +things:

+
    +
  • +

    +Route type +

    +
  • +
  • +

    +Route render function +

    +
  • +
  • +

    +Dispatch function +

    +
  • +
+

The dispatch function refers to all of the handler functions, and therefore all +of those must either be defined in the same file as the dispatch function, or +be imported into the module containing the dispatch function.

+

Meanwhile, the handler functions will almost certainly refer to the route type. +Therefore, they must be either in the same file where the route type is +defined, or must import that file. If you follow the logic here, your entire +application must essentially live in a single file!

+

Clearly this isn’t what we want. So instead of using mkYesod, the scaffolding +site uses a decomposed version of the function. Foundation calls +mkYesodData, which declares the route type and render function. Since it does +not declare the dispatch function, the handler functions need not be in scope. +Import.hs imports Foundation.hs, and all the handler modules import +Import.hs.

+

In Application.hs, we call mkYesodDispatch, which creates our dispatch +function. For this to work, all handler functions must be in scope, so be sure +to add an import statement for any new handler modules you create.

+

Other than that, Application.hs is pretty simple. It provides two primary +functions: getApplicationDev is used by yesod devel to launch your app, and +makeApplication is used by the executable to launch.

+

Foundation.hs is much more exciting. It:

+
    +
  • +

    +Declares your foundation datatype +

    +
  • +
  • +

    +Declares a number of instances, such as Yesod, YesodAuth, and + YesodPersist +

    +
  • +
  • +

    +Imports the messages files. If you look for the line starting with + mkMessage, you will see that it specifies the folder containing the + messages (messages/) and the default language (en, for English). +

    +
  • +
+

This is the right file for adding extra instances for your foundation, such as +YesodAuthEmail or YesodBreadcrumbs.

+

We’ll be referring back to this file later, as we discussed some of the special +implementations of Yesod typeclass methods.

+
+
+

Import

+

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’m always adding the same five import statements (Data.Text, + Control.Applicative, etc) to every handler module. +

    +
  • +
  • +

    +I want to make sure I never use some evil function (head, readFile, …) from Prelude. +

    +
  • +
+ +

The solution is to turn on the NoImplicitPrelude language extension, +re-export the parts of Prelude we want, add in all the other stuff we want, +define our own functions as well, and then import this file in all handlers.

+ +
+
+

Handler modules

+

Handler modules should go inside the Handler folder. The site template +includes one module: Handler/Home.hs. How you split up your handler functions +into individual modules is your decision, but a good rule of thumb is:

+
    +
  • +

    +Different methods for the same route should go in the same file, e.g. + getBlogR and postBlogR. +

    +
  • +
  • +

    +Related routes can also usually go in the same file, e.g., getPeopleR and + getPersonR. +

    +
  • +
+

Of course, it’s entirely up to you. When you add a new handler file, make sure +you do the following:

+
    +
  • +

    +Add it to version control (you are using version control, right?). +

    +
  • +
  • +

    +Add it to the cabal file. +

    +
  • +
  • +

    +Add it to the Application.hs file. +

    +
  • +
  • +

    +Put a module statement at the top, and an import Import line below it. +

    +
  • +
+

You can use the stack exec — yesod add-handler command to automate the last three steps.

+
+
+
+

widgetFile

+

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.

+

If you have a handler function:

+
getHomeR = defaultLayout $(widgetFile "homepage")
+

, Yesod will look for the following files:

+
    +
  • +

    +templates/homepage.hamlet +

    +
  • +
  • +

    +templates/homepage.lucius +

    +
  • +
  • +

    +templates/homepage.cassius +

    +
  • +
  • +

    +templates/homepage.julius +

    +
  • +
+

If any of those files are present, they will be automatically included in the +output.

+ +
+
+

defaultLayout

+

One of the first things you’re going to want to customize is the look of your +site. The layout is actually broken up into two files:

+
    +
  • +

    +templates/default-layout-wrapper.hamlet contains just the basic shell of a + page. This file is interpreted as plain Hamlet, not as a Widget, and + therefore cannot refer to other widgets, embed i18n strings, or add extra + CSS/JS. +

    +
  • +
  • +

    +templates/default-layout.hamlet is where you would put the bulk of your + page. You must remember to include the widget value in the page, as that + contains the per-page contents. This file is interpreted as a Widget. +

    +
  • +
+

Also, since default-layout is included via the widgetFile function, any +Lucius, Cassius, or Julius files named default-layout.* will automatically be +included as well.

+
+
+

Static files

+

The scaffolded site automatically includes the static file subsite, optimized +for serving files that will not change over the lifetime of the current build. +What this means is that:

+
    +
  • +

    +When your static file identifiers are generated (e.g., static/mylogo.png + becomes mylogo_png), a query-string parameter is added to it with a hash of + the contents of the file. All of this happens at compile time. +

    +
  • +
  • +

    +When yesod-static serves your static files, it sets expiration headers far + in the future, and includes an etag based on a hash of your content. +

    +
  • +
  • +

    +Whenever you embed a link to mylogo_png, the rendering includes the + query-string parameter. If you change the logo, recompile, and launch your + new app, the query string will have changed, causing users to ignore the + cached copy and download a new version. +

    +
  • +
+

Additionally, you can set a specific static root in your Settings.hs file to +serve from a different domain name. This has the advantage of not requiring +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 +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:

+
    +
  1. +

    +Caching works properly. +

    +
  2. +
  3. +

    +Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with the same hash already exists. +

    +
  4. +
+

Finally, all of your Javascript is automatically minified via hjsmin.

+
+
+

Conclusion

+

The purpose of this chapter was not to explain every line that exists in the +scaffolded site, but instead to give a general overview to how it works. The +best way to become more familiar with it is to jump right in and start writing +a Yesod site with it.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/sessions.html b/public/book-1.4/sessions.html new file mode 100644 index 00000000..eae733f6 --- /dev/null +++ b/public/book-1.4/sessions.html @@ -0,0 +1,488 @@ + Sessions :: Yesod Web Framework Book- Version 1.4 +
+

Sessions

+ + +

HTTP is a stateless protocol. While some view this as a disadvantage, advocates +of RESTful web development laud this as a plus. When state is removed from the +picture, we get some automatic benefits, such as easier scalability and +caching. You can draw many parallels with the non-mutable nature of Haskell in +general.

+

As much as possible, RESTful applications should avoid storing state about an +interaction with a client. However, it is sometimes unavoidable. Features like +shopping carts are the classic example, but other more mundane interactions +like proper login handling can be greatly enhanced by correct usage of sessions.

+

This chapter will describe how Yesod stores session data, how you can access +this data, and some special functions to help you make the most of sessions.

+
+

Clientsession

+

One of the earliest packages spun off from Yesod was clientsession. This +package uses encryption and signatures to store data in a client-side cookie. +The encryption prevents the user from inspecting the data, and the signature +ensures that the session cannot be tampered with.

+

It might sound like a bad idea from an efficiency standpoint to store data in a +cookie. After all, this means that the data must be sent on every request. +However, in practice, clientsession can be a great boon for performance.

+
    +
  • +

    +No server side database lookup is required to service a request. +

    +
  • +
  • +

    +We can easily scale horizontally: each request contains all the information + we need to send a response. +

    +
  • +
  • +

    +To avoid undue bandwidth overhead, production sites can serve their static + content from a separate domain name, thereby skipping transmission of the + session cookie for each request. +

    +
  • +
+

Storing megabytes of information in the session will be a bad idea. But for +that matter, most session implementations recommend against such practices. If +you really need massive storage for a user, it is best to store a lookup key in +the session, and put the actual data in a database.

+

All of the interaction with clientsession is handled by Yesod internally, but +there are a few spots where you can tweak the behavior just a bit.

+
+
+

Controlling sessions

+

By default, your Yesod application will use clientsession for its session +storage, getting the encryption key from the client client-session-key.aes +and giving a session a two hour timeout. (Note: timeout is measured from the +last time the client sent a request to the site, not from when then session +was first created.) However, all of those points can be modified by overriding +the makeSessionBackend method in the Yesod typeclass.

+

One simple way to override this method is to simply turn off session handling; +to do so, return Nothing. If your app has absolutely no session needs, +disabling them can give a bit of a performance increase. But be careful about +disabling sessions: this will also disable such features as Cross-Site Request +Forgery protection.

+
instance Yesod App where
+    makeSessionBackend _ = return Nothing
+

Another common approach is to modify the filepath or timeout value, but +continue using client-session. To do so, use the defaultClientSessionBackend +helper function:

+
instance Yesod App where
+    makeSessionBackend _ =
+        fmap Just $ defaultClientSessionBackend minutes filepath
+      where minutes = 24 * 60 -- 1 day
+            filepath = "mykey.aes"
+

There are a few other functions to grant you more fine-grained control of +client-session, but they will rarely be necessary. Please see Yesod.Core's +documentation if you are interested. It’s also possible to implement some other +form of session, such as a server side session. To my knowledge, at the time of +writing, no other such implementations exist.

+ +
+
+

Hardening via SSL

+

Client sessions over HTTP have an inherent hijacking vulnerability: an attacker +can read the unencrypted traffic, obtain the session cookie from it, and then +make requests to the site with that same cookie to impersonate the user. This +vulnerability is particularly severe if the sessions include any personally +identifiable information or authentication material.

+

The only sure way to defeat this threat is to run your entire site over SSL, +and prevent browsers from attempting to access it over HTTP. You can achieve +the first part of this at the webserver level, either via an SSL solution in +Haskell such as warp-tls, or by using an SSL-enabled load balancer like +Amazon Elastic Load Balancer.

+

To prevent your site from sending cookies over insecure connections, you should +augment your application’s sessions as well as the default yesodMiddleware +implementation with some additional behavior: Apply the sslOnlySessions +transformation to your makeSessionBackend, and compose the +sslOnlyMiddleware transformation with your yesodMiddleware implementation.

+
instance Yesod App where
+    makeSessionBackend _ = sslOnlySessions $
+        fmap Just $ defaultClientSessionBackend 120 "mykey.aes"
+    yesodMiddleware = (sslOnlyMiddleware 120) . defaultYesodMiddleware
+

sslOnlySessions causes all session cookies to be set with the Secure bit on, +so that browsers will not transmit them over HTTP. sslOnlyMiddleware adds a +Strict-Transport-Security header to all responses, which instructs browsers not +to make HTTP requests to your domain or its subdomains for the specified number +of minutes. Be sure to set the timeout for the sslOnlyMiddleware to be at +least as long as your session timeout. Used together, these measures will +ensure that session cookies are not transmitted in the clear.

+
+
+

Session Operations

+

Like most frameworks, a session in Yesod is a key-value store. The base session +API boils down to four functions: lookupSession gets a value for a key (if +available), getSession returns all of the key/value pairs, setSession sets +a value for a key, and deleteSession clears a value for a key.

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+import           Control.Applicative ((<$>), (<*>))
+import qualified Web.ClientSession   as CS
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+getHomeR :: Handler Html
+getHomeR = do
+    sess <- getSession
+    defaultLayout
+        [whamlet|
+            <form method=post>
+                <input type=text name=key>
+                <input type=text name=val>
+                <input type=submit>
+            <h1>#{show sess}
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    (key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
+    case mval of
+        Nothing -> deleteSession key
+        Just val -> setSession key val
+    liftIO $ print (key, mval)
+    redirect HomeR
+
+instance Yesod App where
+    -- Make the session timeout 1 minute so that it's easier to play with
+    makeSessionBackend _ = do
+        backend <- defaultClientSessionBackend 1 "keyfile.aes"
+        return $ Just backend
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Messages

+

One usage of sessions previously alluded to is messages. They come to solve a +common problem in web development: the user performs a POST request, the web +app makes a change, and then the web app wants to simultaneously redirect the +user to a new page and send the user a success message. (This is known as +Post/Redirect/Get.)

+

Yesod provides a pair of functions to enable this workflow: setMessage stores +a value in the session, and getMessage both reads the value most recently put +into the session, and clears the old value so it is not displayed twice.

+

It is recommended to have a call to getMessage in defaultLayout so that any +available message is shown to a user immediately, without having to add +getMessage calls to every handler.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/            HomeR       GET
+/set-message SetMessageR POST
+|]
+
+instance Yesod App where
+    defaultLayout widget = do
+        pc <- widgetToPageContent widget
+        mmsg <- getMessage
+        withUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <p>Your message was: #{msg}
+                        ^{pageBody pc}
+            |]
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{SetMessageR}>
+            My message is: #
+            <input type=text name=message>
+            <button>Go
+    |]
+
+postSetMessageR :: Handler ()
+postSetMessageR = do
+    msg <- runInputPost $ ireq textField "message"
+    setMessage $ toHtml msg
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+

Initial page load, no message

+ + + + + + +
+

New message entered in text box

+ + + + + + +
+

After form submit, message appears at top of page

+ + + + + + +
+

After refresh, the message is cleared

+ + + + + + +
+
+
+

Ultimate Destination

+

Not to be confused with a horror film, ultimate destination is a technique +originally developed for Yesod’s authentication framework, but which has more +general usefulness. Suppose a user requests a page that requires +authentication. If the user is not yet logged in, you need to send him/her to +the login page. A well-designed web app will then send them back to the first +page they requested. That’s what we call the ultimate destination.

+

redirectUltDest sends the user to the ultimate destination set in his/her +session, clearing that value from the session. It takes a default destination +as well, in case there is no destination set. For setting the session, there +are three options:

+
    +
  • +

    +setUltDest sets the destination to the given URL, which can be given + either as a textual URL or a type-safe URL. +

    +
  • +
  • +

    +setUltDestCurrent sets the destination to the currently requested URL. +

    +
  • +
  • +

    +setUltDestReferer sets the destination based on the Referer header (the + page that led the user to the current page). +

    +
  • +
+

Additionally, there is the clearUltDest function, to drop the ultimate +destination value from the session if present.

+

Let’s look at a small sample app. It will allow the user to set his/her name in +the session, and then tell the user his/her name from another route. If the +name hasn’t been set yet, the user will be redirected to the set name page, +with an ultimate destination set to come back to the current page.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/         HomeR     GET
+/setname  SetNameR  GET POST
+/sayhello SayHelloR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{SetNameR}>Set your name
+        <p>
+            <a href=@{SayHelloR}>Say hello
+    |]
+
+-- Display the set name form
+getSetNameR :: Handler Html
+getSetNameR = defaultLayout
+    [whamlet|
+        <form method=post>
+            My name is #
+            <input type=text name=name>
+            . #
+            <input type=submit value="Set name">
+    |]
+
+-- Retreive the submitted name from the user
+postSetNameR :: Handler ()
+postSetNameR = do
+    -- Get the submitted name and set it in the session
+    name <- runInputPost $ ireq textField "name"
+    setSession "name" name
+
+    -- After we get a name, redirect to the ultimate destination.
+    -- If no destination is set, default to the homepage
+    redirectUltDest HomeR
+
+getSayHelloR :: Handler Html
+getSayHelloR = do
+    -- Lookup the name value set in the session
+    mname <- lookupSession "name"
+    case mname of
+        Nothing -> do
+            -- No name in the session, set the current page as
+            -- the ultimate destination and redirect to the
+            -- SetName page
+            setUltDestCurrent
+            setMessage "Please tell me your name"
+            redirect SetNameR
+        Just name -> defaultLayout [whamlet|<p>Welcome #{name}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Summary

+

Sessions are the primary means by which we bypass the statelessness imposed by +HTTP. We shouldn’t consider this an escape hatch to perform whatever actions we +want: statelessness in web applications is a virtue, and we should respect it +whenever possible. However, there are specific cases where it is vital to +retain some state.

+

The session API in Yesod is very simple. It provides a key-value store, and a +few convenience functions built on top for common use cases. If used properly, +with small payloads, sessions should be an unobtrusive part of your web +development.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/settings-types.html b/public/book-1.4/settings-types.html new file mode 100644 index 00000000..6269b148 --- /dev/null +++ b/public/book-1.4/settings-types.html @@ -0,0 +1,196 @@ + Settings Types :: Yesod Web Framework Book- Version 1.4 +
+

Settings Types

+ + +

Let’s say you’re writing a webserver. You want the server to take a port to +listen on, and an application to run. So you create the following function:

+
run :: Int -> Application -> IO ()
+

But suddenly you realize that some people will want to customize their timeout +durations. So you modify your API:

+
run :: Int -> Int -> Application -> IO ()
+

So, which Int is the timeout, and which is the port? Well, you could create +some type aliases, or comment your code. But there’s another problem creeping +into our code: this run function is getting unmanageable. Soon we’ll need to +take an extra parameter to indicate how exceptions should be handled, and then +another one to control which host to bind to, and so on.

+

A more extensible solution is to introduce a settings datatype:

+
data Settings = Settings
+    { settingsPort :: Int
+    , settingsHost :: String
+    , settingsTimeout :: Int
+    }
+

And this makes the calling code almost self-documenting:

+
run Settings
+    { settingsPort = 8080
+    , settingsHost = "127.0.0.1"
+    , settingsTimeout = 30
+    } myApp
+

Great, couldn’t be clearer, right? True, but what happens when you have 50 +settings to your webserver. Do you really want to have to specify all of those +each time? Of course not. So instead, the webserver should provide a set of +defaults:

+
defaultSettings = Settings 3000 "127.0.0.1" 30
+

And now, instead of needing to write that long bit of code above, we can get +away with:

+
run defaultSettings { settingsPort = 8080 } myApp -- (1)
+

This is great, except for one minor hitch. Let’s say we now decide to add an +extra record to Settings. Any code out in the wild looking like this:

+
run (Settings 8080 "127.0.0.1" 30) myApp -- (2)
+

will be broken, since the Settings constructor now takes 4 arguments. The +proper thing to do would be to bump the major version number so that dependent +packages don’t get broken. But having to change major versions for every minor +setting you add is a nuisance. The solution? Don’t export the Settings +constructor:

+
module MyServer
+    ( Settings
+    , settingsPort
+    , settingsHost
+    , settingsTimeout
+    , run
+    , defaultSettings
+    ) where
+

With this approach, no one can write code like (2), so you can freely add new +records without any fear of code breaking.

+

The one downside of this approach is that it’s not immediately obvious from the +Haddocks that you can actually change the settings via record syntax. That’s +the point of this chapter: to clarify what’s going on in the libraries that use +this technique.

+

I personally use this technique in a few places, feel free to have a look at +the Haddocks to see what I mean.

+
    +
  • +

    +Warp: Settings +

    +
  • +
  • +

    +http-conduit: Request and ManagerSettings +

    +
  • +
  • +

    +xml-conduit +

    +
  • +
  • +

    +Parsing: ParseSettings +

    +
  • +
  • +

    +Rendering: RenderSettings +

    +
  • +
+

As a tangential issue, http-conduit and xml-conduit actually create +instances of the Default typeclass instead of declaring a brand new +identifier. This means you can just type def instead of +defaultParserSettings.

+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/shakespearean-templates.html b/public/book-1.4/shakespearean-templates.html new file mode 100644 index 00000000..d95f2ebe --- /dev/null +++ b/public/book-1.4/shakespearean-templates.html @@ -0,0 +1,1060 @@ + Shakespearean Templates :: Yesod Web Framework Book- Version 1.4 +
+

Shakespearean Templates

+ + +

Yesod uses the Shakespearean family of template languages as its standard +approach to HTML, CSS and Javascript creation. This language family shares some +common syntax, as well as overarching principles:

+
    +
  • +

    +As little interference to the underlying language as possible, while +providing conveniences where unobtrusive. +

    +
  • +
  • +

    +Compile-time guarantees on well-formed content. +

    +
  • +
  • +

    +Static type safety, greatly helping the prevention of + XSS (cross-site + scripting) attacks. +

    +
  • +
  • +

    +Automatic validation of interpolated links, whenever possible, through type-safe + URLs. +

    +
  • +
+

There is nothing inherently tying Yesod to these languages, or the other way +around: each can be used independently of the other. This chapter will address +these template languages on their own, while the remainder of the book will use +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 +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 +Javascript; the only added feature is variable interpolation.

+ +
+

Hamlet (HTML)

+
$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle} - My Site
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1 .page-title>#{pageTitle}
+        <p>Here is a list of your friends:
+        $if null friends
+            <p>Sorry, I lied, you don't have any friends.
+        $else
+            <ul>
+                $forall Friend name age <- friends
+                    <li>#{name} (#{age} years old)
+        <footer>^{copyright}
+
+
+

Lucius (CSS)

+
section.blog {
+    padding: 1em;
+    border: 1px solid #000;
+    h1 {
+        color: #{headingColor};
+        background-image: url(@{MyBackgroundR});
+    }
+}
+
+
+

Cassius (CSS)

+

The following is equivalent to the Lucius example above.

+
section.blog
+    padding: 1em
+    border: 1px solid #000
+    h1
+        color: #{headingColor}
+        background-image: url(@{MyBackgroundR})
+
+
+

Julius (Javascript)

+
$(function(){
+    $("section.#{sectionClass}").hide();
+    $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
+    ^{addBling}
+});
+
+
+
+

Types

+

Before we jump into syntax, let’s take a look at the various types involved. We +mentioned in the introduction that types help protect us from XSS attacks. For +example, let’s say that we have an HTML template that should display someone’s +name. It might look like this:

+
<p>Hello, my name is #{name}
+ +

What should happen to name, and what should its datatype be? A naive approach +would be to use a Text value, and insert it verbatim. But that would give us +quite a problem when name is equal to something like:

+
<script src='http://nefarious.com/evil.js'></script>
+

What we want is to be able to entity-encode the name, so that < becomes &lt;.

+

An equally naive approach is to simply entity-encode every piece of text that +gets embedded. What happens when you have some preexisting HTML generated from +another process? For example, on the Yesod website, all Haskell code snippets +are run through a colorizing function that wraps up words in appropriate span +tags. If we entity escaped everything, code snippets would be completely +unreadable!

+

Instead, we have an Html datatype. In order to generate an Html value, we +have two options for APIs: the ToMarkup typeclass provides a way to convert +String and Text values into Html, via its toHtml function, +automatically escaping entities along the way. This would be the approach we’d +want for the name above. For the code snippet example, we would use the +preEscapedToMarkup function.

+

When you use variable interpolation in Hamlet (the HTML Shakespeare language), +it automatically applies a toHtml call to the value inside. So if you +interpolate a String, it will be entity-escaped. But if you provide an Html +value, it will appear unmodified. In the code snippet example, we might +interpolate with something like #{preEscapedToMarkup myHaskellHtml}.

+ +

Similarly, we have Css/ToCss, as well as Javascript/ToJavascript. These +provide some compile-time sanity checks that we haven’t accidentally stuck some +HTML in our CSS.

+ +
+

Type-safe URLs

+

Possibly the most unique feature in Yesod is type-safe URLs, and the ability to +use them conveniently is provided directly by Shakespeare. Usage is nearly +identical to variable interpolation; we just use the at-sign (@) instead of the +hash (#). We’ll cover the syntax later; first, let’s clarify the intuition.

+

Suppose we have an application with two routes: +http://example.com/profile/home is the homepage, and +http://example.com/display/time displays the current time. And let’s say we +want to link from the homepage to the time. I can think of three different ways +of constructing the URL:

+
    +
  1. +

    +As a relative link: ../display/time +

    +
  2. +
  3. +

    +As an absolute link, without a domain: /display/time +

    +
  4. +
  5. +

    +As an absolute link, with a domain: http://example.com/display/time +

    +
  6. +
+

There are problems with each approach: the first will break if either URL +changes. Also, it’s not suitable for all use cases; RSS and Atom feeds, for +instance, require absolute URLs. The second is more resilient to change than +the first, but still won’t be acceptable for RSS and Atom. And while the third +works fine for all use cases, you’ll need to update every single URL in your +application whenever your domain name changes. You think that doesn’t happen +often? Just wait till you move from your development to staging and finally +production server.

+

But more importantly, there is one huge problem with all approaches: if you +change your routes at all, the compiler won’t warn you about the broken links. +Not to mention that typos can wreak havoc as well.

+

The goal of type-safe URLs is to let the compiler check things for us as much +as possible. In order to facilitate this, our first step must be to move away +from plain old text, which the compiler doesn’t understand, to some well +defined datatypes. For our simple application, let’s model our routes with a +sum type:

+
data MyRoute = Home | Time
+

Instead of placing a link like /display/time in our template, we can use the +Time constructor. But at the end of the day, HTML is made up of text, not +data types, so we need some way to convert these values to text. We call this a +URL rendering function, and a simple one is:

+
renderMyRoute :: MyRoute -> Text
+renderMyRoute Home = "http://example.com/profile/home"
+renderMyRoute Time = "http://example.com/display/time"
+ +

OK, we have our render function, and we have type-safe URLs embedded in the +templates. How does this fit together exactly? Instead of generating an Html +(or Css or Javascript) value directly, Shakespearean templates actually +produce a function, which takes this render function and produces HTML. To see +this better, let’s have a quick (fake) peek at how Hamlet would work under the +surface. Supposing we had a template:

+
<a href=@{Time}>The time
+

this would translate roughly into the Haskell code:

+
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
+
+
+
+

Syntax

+

All Shakespearean languages share the same interpolation syntax, and are able +to utilize type-safe URLs. They differ in the syntax specific for their target +language (HTML, CSS, or Javascript). Let’s explore each language in turn.

+
+

Hamlet Syntax

+

Hamlet is the most sophisticated of the languages. Not only does it provide +syntax for generating HTML, it also allows for basic control structures: +conditionals, looping, and maybes.

+
+

Tags

+

Obviously tags will play an important part of any HTML template language. In +Hamlet, we try to stick very close to existing HTML syntax to make the language +more comfortable. However, instead of using closing tags to denote nesting, we +use indentation. So something like this in HTML:

+
<body>
+<p>Some paragraph.</p>
+<ul>
+<li>Item 1</li>
+<li>Item 2</li>
+</ul>
+</body>
+

would be

+
<body>
+    <p>Some paragraph.
+    <ul>
+        <li>Item 1
+        <li>Item 2
+

In general, we find this to be easier to follow than HTML once you get +accustomed to it. The only tricky part comes with dealing with whitespace +before and after tags. For example, let’s say you want to create the HTML

+
<p>Paragraph <i>italic</i> end.</p>
+

We want to make sure that whitespace is preserved after the word +"Paragraph" and before the word "end". To do so, we use two simple escape +characters:

+
<p>
+    Paragraph #
+    <i>italic
+    \ end.
+

The whitespace escape rules are actually quite simple:

+
    +
  1. +

    +If the first non-space character in a line is a backslash, the backslash is ignored. (Note: this will also cause any tag on this line to be treated as plain text.) +

    +
  2. +
  3. +

    +If the last character in a line is a hash, it is ignored. +

    +
  4. +
+

One other thing. Hamlet does not escape entities within its content. This is +done on purpose to allow existing HTML to be more easily copied in. So the +example above could also be written as:

+
<p>Paragraph <i>italic</i> end.
+

Notice that the first tag will be automatically closed by Hamlet, while the +inner "i" tag will not. You are free to use whichever approach you want, there +is no penalty for either choice. Be aware, however, that the only time you +use closing tags in Hamlet is for such inline tags; normal tags are not closed.

+

Another outcome of this is that any tags after the first tag do not have +special treatment for IDs and classes. For example, the Hamlet snippet:

+
<p #firstid>Paragraph <i #secondid>italic end.
+

generates the HTML:

+
<p id="firstid">Paragraph <i #secondid>italic</i> end.</p>
+

Notice how the p tag is automatically closed, and its attributes get special +treatment, whereas the i tag is treated as plain text.

+
+
+

Interpolation

+

What we have so far is a nice, simplified HTML, but it doesn’t let us interact +with our Haskell code at all. How do we pass in variables? Simple: with +interpolation:

+
<head>
+    <title>#{title}
+

The hash followed by a pair of braces denotes variable interpolation. In the +case above, the title variable from the scope in which the template was +called will be used. Let me state that again: Hamlet automatically has access +to the variables in scope when it’s called. There is no need to specifically +pass variables in.

+

You can apply functions within an interpolation. You can use string and numeric +literals in an interpolation. You can use qualified modules. Both parentheses +and the dollar sign can be used to group statements together. And at the end, +the toHtml function is applied to the result, meaning any instance of +ToHtml can be interpolated. Take, for instance, the following code.

+
-- Just ignore the quasiquote stuff for now, and that shamlet thing.
+-- It will be explained later.
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (shamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Char (toLower)
+import Data.List (sort)
+
+data Person = Person
+    { name :: String
+    , age  :: Int
+    }
+
+main :: IO ()
+main = putStrLn $ renderHtml [shamlet|
+<p>Hello, my name is #{name person} and I am #{show $ age person}.
+<p>
+    Let's do some funny stuff with my name: #
+    <b>#{sort $ map toLower (name person)}
+<p>Oh, and in 5 years I'll be #{show ((+) 5 (age person))} years old.
+|]
+  where
+    person = Person "Michael" 26
+

What about our much-touted type-safe URLs? They are almost identical to +variable interpolation in every way, except they start with an at-sign (@) +instead. In addition, there is embedding via a caret (^) which allows you to +embed another template of the same type. The next code sample demonstrates both +of these.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text)
+
+data MyRoute = Home
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+
+footer :: HtmlUrl MyRoute
+footer = [hamlet|
+<footer>
+    Return to #
+    <a href=@{Home}>Homepage
+    .
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ [hamlet|
+<body>
+    <p>This is my page.
+    ^{footer}
+|] render
+

Additionally, there is a variant of URL interpolation which allows you to embed +query string parameters. This can be useful, for example, for creating +paginated responses. Instead of using @{…}, you add a question mark +(@?{…}) to indicate the presence of a query string. The value you provide +must be a two-tuple with the first value being a type-safe URL and the second +being a list of query string parameter pairs. See the next code snippet for an +example.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text, append, pack)
+import Control.Arrow (second)
+import Network.HTTP.Types (renderQueryText)
+import Data.Text.Encoding (decodeUtf8)
+import Blaze.ByteString.Builder (toByteString)
+
+data MyRoute = SomePage
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render SomePage params = "/home" `append`
+    decodeUtf8 (toByteString $ renderQueryText True (map (second Just) params))
+
+main :: IO ()
+main = do
+    let currPage = 2 :: Int
+    putStrLn $ renderHtml $ [hamlet|
+<p>
+    You are currently on page #{currPage}.
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage - 1)])}>Previous
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage + 1)])}>Next
+|] render
+

This generates the expected HTML:

+
<p>You are currently on page 2.
+<a href="/home?page=1">Previous</a>
+<a href="/home?page=3">Next</a>
+</p>
+
+
+

Attributes

+

In that last example, we put an href attribute on the "a" tag. Let’s elaborate on the syntax:

+
    +
  • +

    +You can have interpolations within the attribute value. +

    +
  • +
  • +

    +The equals sign and value for an attribute are optional, just like in HTML. + So <input type=checkbox checked> is perfectly valid. +

    +
  • +
  • +

    +There are two convenience attributes: for id, you can use the hash, and for + classes, the period. In other words, <p #paragraphid .class1 .class2>. +

    +
  • +
  • +

    +While quotes around the attribute value are optional, they are required if + you want to embed spaces. +

    +
  • +
  • +

    +You can add an attribute optionally by using colons. To make a checkbox only + checked if the variable isChecked is True, you would write + <input type=checkbox :isChecked:checked>. To have a paragraph be optionally red, + you could use <p :isRed:style="color:red">. (This also works for class names, e.g., + <p :isCurrent:.current> will set the class current if isCurrent is True.) +

    +
  • +
  • +

    +Arbitrary key-value pairs can also be interpolated using the *{…} + syntax. The interpolated variable must be a tuple, or list of + tuples, of Text or String. For example: if we have a variable + attrs = [("foo", "bar")], we could interpolate that into an + element like: <p *{attrs}> to get <p foo="bar">. +

    +
  • +
+
+
+

Conditionals

+

Eventually, you’ll want to put in some logic in your page. The goal of Hamlet +is to make the logic as minimalistic as possible, pushing the heavy lifting +into Haskell. As such, our logical statements are very basic… so basic, that +it’s if, elseif, and else.

+
$if isAdmin
+    <p>Welcome to the admin section.
+$elseif isLoggedIn
+    <p>You are not the administrator.
+$else
+    <p>I don't know who you are. Please log in so I can decide if you get access.
+

All the same rules of normal interpolation apply to the content of the conditionals.

+
+
+

Maybe

+

Similarly, we have a special construct for dealing with Maybe values. This +could technically be dealt with using if, isJust and fromJust, but this +is more convenient and avoids partial functions.

+
$maybe name <- maybeName
+    <p>Your name is #{name}
+$nothing
+    <p>I don't know your name.
+

In addition to simple identifiers, you can use a few other, more complicated +values on the left hand side, such as constructors and tuples.

+
$maybe Person firstName lastName <- maybePerson
+    <p>Your name is #{firstName} #{lastName}
+

The right-hand-side follows the same rules as interpolations, allow variables, +function application, and so on.

+
+
+

Forall

+

And what about looping over lists? We have you covered there too:

+
$if null people
+    <p>No people.
+$else
+    <ul>
+        $forall person <- people
+            <li>#{person}
+
+
+

Case

+

Pattern matching is one of the great strengths of Haskell. Sum types let you +cleanly model many real-world types, and case statements let you safely +match, letting the compiler warn you if you missed a case. Hamlet gives you the +same power.

+
$case foo
+    $of Left bar
+        <p>It was left: #{bar}
+    $of Right baz
+        <p>It was right: #{baz}
+
+
+

With

+

Rounding out our statements, we have with. It’s basically just a convenience +for declaring a synonym for a long expression.

+
$with foo <- some very (long ugly) expression that $ should only $ happen once
+    <p>But I'm going to use #{foo} multiple times. #{foo}
+
+
+

Doctype

+

Last bit of syntactic sugar: the doctype statement. We have support for a +number of different versions of a doctype, though we recommend $doctype 5 +for modern web applications, which generates <!DOCTYPE html>.

+
$doctype 5
+<html>
+    <head>
+        <title>Hamlet is Awesome
+    <body>
+        <p>All done.
+ +
+
+
+

Lucius Syntax

+

Lucius is one of two CSS templating languages in the Shakespeare family. It is +intended to be a superset of CSS, leveraging the existing syntax while adding +in a few more features.

+
    +
  • +

    +Like Hamlet, we allow both variable and URL interpolation. +

    +
  • +
  • +

    +CSS blocks are allowed to nest. +

    +
  • +
  • +

    +You can declare variables in your templates. +

    +
  • +
  • +

    +A set of CSS properties can be created as a mixin, and reused in multiple + declarations. +

    +
  • +
+

Starting with the second point: let’s say you want to have some special styling +for some tags within your article. In plain ol' CSS, you’d have to write:

+
article code { background-color: grey; }
+article p { text-indent: 2em; }
+article a { text-decoration: none; }
+

In this case, there aren’t that many clauses, but having to type out article +each time is still a bit of a nuisance. Imagine if you had a dozen or so of +these. Not the worst thing in the world, but a bit of an annoyance. Lucius +helps you out here:

+
article {
+    code { background-color: grey; }
+    p { text-indent: 2em; }
+    a { text-decoration: none; }
+    > h1 { color: green; }
+}
+

Having Lucius variables allows you to avoid repeating yourself. A simple +example would be to define a commonly used color:

+
@textcolor: #ccc; /* just because we hate our users */
+body { color: #{textcolor} }
+a:link, a:visited { color: #{textcolor} }
+

Mixins are a relatively new addition to Lucius. The idea is to declare a mixin +providing a collection of properties, and then embed that mixin in a template +using caret interpolation (^). The following example demonstrates how we +could use a mixin to deal with vendor prefixes.

+
{-# LANGUAGE QuasiQuotes #-}
+import Text.Lucius
+import qualified Data.Text.Lazy.IO as TLIO
+
+-- Dummy render function.
+render = undefined
+
+-- Our mixin, which provides a number of vendor prefixes for transitions.
+transition val =
+    [luciusMixin|
+        -webkit-transition: #{val};
+        -moz-transition: #{val};
+        -ms-transition: #{val};
+        -o-transition: #{val};
+        transition: #{val};
+    |]
+
+-- Our actual Lucius template, which uses the mixin.
+myCSS =
+    [lucius|
+        .some-class {
+            ^{transition "all 4s ease"}
+        }
+    |]
+
+main = TLIO.putStrLn $ renderCss $ myCSS render
+
+
+

Cassius Syntax

+

Cassius is a whitespace-sensitive alternative to Lucius. As mentioned in the +synopsis, it uses the same processing engine as Lucius, but preprocesses all +input to insert braces to enclose subblocks and semicolons to terminate lines. +This means you can leverage all features of Lucius when writing Cassius. As a +simple example:

+
#banner
+    border: 1px solid #{bannerColor}
+    background-image: url(@{BannerImageR})
+
+
+

Julius Syntax

+

Julius is the simplest of the languages discussed here. In fact, some might +even say it’s really just Javascript. Julius allows the three forms of +interpolation we’ve mentioned so far, and otherwise applies no transformations +to your content.

+ +
+
+
+

Calling Shakespeare

+

The question of course arises at some point: how do I actually use this stuff? +There are three different ways to call out to Shakespeare from your Haskell +code:

+
+
+Quasiquotes +
+

+Quasiquotes allow you to embed arbitrary content within your Haskell, and for it to be converted into Haskell code at compile time. +

+
+
+External file +
+

+In this case, the template code is in a separate file which is referenced via Template Haskell. +

+
+
+Reload mode +
+

+Both of the above modes require a full recompile to see any changes. In reload mode, your template is kept in a separate file and referenced via Template Haskell. But at runtime, the external file is reparsed from scratch each time. +

+
+
+ +

One of the first two approaches should be used in production. They both embed +the entirety of the template in the final executable, simplifying deployment +and increasing performance. The advantage of the quasiquoter is the simplicity: +everything stays in a single file. For short templates, this can be a very good +fit. However, in general, the external file approach is recommended because:

+
    +
  • +

    +It follows nicely in the tradition of separating logic from presentation. +

    +
  • +
  • +

    +You can easily switch between external file and debug mode with some simple + CPP macros, meaning you can keep rapid development and still achieve high + performance in production. +

    +
  • +
+

Since these are special QuasiQuoters and Template Haskell functions, you need +to be sure to enable the appropriate language extensions and use correct +syntax. You can see a simple example of each in the following code snippets.

+

Quasiquoter

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Data.Text (Text)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: Text -> HtmlUrl MyRoute
+template title = [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{title}
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1>#{title}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ template "My Title" render
+

+

External file

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-} -- to control production versus debug
+import Text.Lucius (CssUrl, luciusFile, luciusFileReload, renderCss)
+import Data.Text (Text)
+import qualified Data.Text.Lazy.IO as TLIO
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: CssUrl MyRoute
+#if PRODUCTION
+template = $(luciusFile "template.lucius")
+#else
+template = $(luciusFileReload "template.lucius")
+#endif
+
+main :: IO ()
+main = TLIO.putStrLn $ renderCss $ template render
+

+
/* @template.lucius */
+foo { bar: baz }
+

The naming scheme for the functions is very consistent.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageQuasiquoterExternal fileReload

Hamlet

hamlet

hamletFile

N/A

Cassius

cassius

cassiusFile

cassiusFileReload

Lucius

lucius

luciusFile

luciusFileReload

Julius

julius

juliusFile

juliusFileReload

+
+

Alternate Hamlet Types

+

So far, we’ve seen how to generate an HtmlUrl value from Hamlet, which is a +piece of HTML with embedded type-safe URLs. There are currently three other +values we can generate using Hamlet: plain HTML, HTML with URLs and +internationalized messages, and widgets. That last one will be covered in more +detail in the widgets chapter.

+

To generate plain HTML without any embedded URLs, we use "simplified Hamlet". +There are a few changes:

+
    +
  • +

    +We use a different set of functions, prefixed with an "s". So the quasiquoter + is shamlet and the external file function is shamletFile. How we + pronounce those is still up for debate. +

    +
  • +
  • +

    +No URL interpolation is allowed. Doing so will result in a compile-time + error. +

    +
  • +
  • +

    +Embedding (the caret-interpolator) no longer allows arbitrary HtmlUrl + values. The rule is that the embedded value must have the same type as the + template itself, so in this case it must be Html. That means that for + shamlet, embedding can be completely replaced with normal variable + interpolation (with a hash). +

    +
  • +
+

Dealing with internationalization (i18n) in Hamlet is a bit complicated. Hamlet +supports i18n via a message datatype, very similar in concept and +implementation to a type-safe URL. As a motivating example, let’s say we want +to have an application that tells you hello and how many apples you have eaten. +We could represent those messages with a datatype.

+
data Msg = Hello | Apples Int
+

Next, we would want to be able to convert that into something human-readable, +so we define some render functions:

+
renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+

Now we want to interpolate those Msg values directly in the template. For that, we use underscore interpolation.

+
$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+

This kind of a template now needs some way to turn those values into HTML. So +just like type-safe URLs, we pass in a render function. To represent this, we +define a new type synonym:

+
type Render url = url -> [(Text, Text)] -> Text
+type Translate msg = msg -> Html
+type HtmlUrlI18n msg url = Translate msg -> Render url -> Html
+

At this point, you can pass renderEnglish, renderSpanish, or +renderKlingon to this template, and it will generate nicely translated output +(depending, of course, on the quality of your translators). The complete +program is:

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Data.Text (Text)
+import qualified Data.Text as T
+import Text.Hamlet (HtmlUrlI18n, ihamlet)
+import Text.Blaze.Html (toHtml)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+renderUrl :: MyRoute -> [(Text, Text)] -> Text
+renderUrl Home _ = "/home"
+renderUrl Time _ = "/time"
+renderUrl Stylesheet _ = "/style.css"
+
+data Msg = Hello | Apples Int
+
+renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+
+template :: Int -> HtmlUrlI18n Msg MyRoute
+template count = [ihamlet|
+$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml
+     $ (template 5) (toHtml . renderEnglish) renderUrl
+
+
+
+

Other Shakespeare

+

In addition to HTML, CSS and Javascript helpers, there is also some more +general-purpose Shakespeare available. shakespeare-text provides a simple way +to create interpolated strings, much like people are accustomed to in scripting +languages like Ruby and Python. This package’s utility is definitely not +limited to Yesod.

+
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
+import Text.Shakespeare.Text
+import qualified Data.Text.Lazy.IO as TLIO
+import Data.Text (Text)
+import Control.Monad (forM_)
+
+data Item = Item
+    { itemName :: Text
+    , itemQty :: Int
+    }
+
+items :: [Item]
+items =
+    [ Item "apples" 5
+    , Item "bananas" 10
+    ]
+
+main :: IO ()
+main = forM_ items $ \item -> TLIO.putStrLn
+    [lt|You have #{show $ itemQty item} #{itemName item}.|]
+

Some quick points about this simple example:

+
    +
  • +

    +Notice that we have three different textual datatypes involved (String, + strict Text and lazy Text). They all play together well. +

    +
  • +
  • +

    +We use a quasiquoter named lt, which generates lazy text. There is also + st. +

    +
  • +
  • +

    +Also, there are longer names for these quasiquoters (ltext and stext). +

    +
  • +
  • +

    +The syntax for variable interpolation for Text.Shakespeare.Text is the same + as described above. Note that ^{..} and @{..} are also recognized in + lt and st. If the output of a template should contain ^{..}, a + backslash can be used to prevent the interpolation, + e.g. [lt|2^{23}|]. The backslash is removed from the resulting text. +

    +
  • +
+
+
+

General Recommendations

+

Here are some general hints from the Yesod community on how to get the most out +of Shakespeare.

+
    +
  • +

    +For actual sites, use external files. For libraries, it’s OK to use + quasiquoters, assuming they aren’t too long. +

    +
  • +
  • +

    +Patrick Brisbin has put together a + Vim code + highlighter that can help out immensely. +

    +
  • +
  • +

    +You should almost always start Hamlet tags on their own line instead of + embedding start/end tags after an existing tag. The only exception to this is + the occasional <i> or <b> tag inside a large block of text. +

    +
  • +
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/single-process-pubsub.html b/public/book-1.4/single-process-pubsub.html new file mode 100644 index 00000000..cf31cb6f --- /dev/null +++ b/public/book-1.4/single-process-pubsub.html @@ -0,0 +1,327 @@ + Single process pub-sub :: Yesod Web Framework Book- Version 1.4 +
+

Single process pub-sub

+ + +

The previous example was admittedly quite simple. Let’s build on that +foundation (pun intended) to do something a bit more interesting. Suppose we +have a workflow on our site like the following:

+
    +
  1. +

    +Enter some information on page X, and submit. +

    +
  2. +
  3. +

    +Submission starts a background job, and the user is redirected to a page to view status of that job. +

    +
  4. +
  5. +

    +That second page will subscribe to updates from the background job and display them to the user. +

    +
  6. +
+

The core principle here is the ability to let one thread publish updates, and +have another thread subscribe to receive those updates. This is known generally +as pub/sub, and fortunately is very easy to achieve in Haskell via STM.

+

Like the previous chapter, let me start off with the caveat: this technique +only works properly if you have a single web application process. If you have +two different servers and a load balancer, you’d either need sticky sessions or +some other solution to make sure that the requests from a single user are going +to the same machine. In those situations, you may want to consider using an +external pubsub solution, such as Redis.

+

With that caveat out of the way, let’s get started.

+
+

Foundation datatype

+

We’ll need two different mutable references in our foundation. The first will +keep track of the next "job id" we’ll hand out. Each of these background jobs +will be represented by a unique identifier, which will be used in our URLs. +The second piece of data will be a map from the job ID to the broadcast channel +used for publishing updates. In code:

+
data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+

Notice that our TChan contains Maybe Text values. The reason for the +Maybe wrapper is so that we can indicate that the channel is complete, by +providing a Nothing value.

+
+
+

Allocating a job

+

In order to allocate a job, we need to:

+
    +
  1. +

    +Get a job ID. +

    +
  2. +
  3. +

    +Create a new broadcast channel. +

    +
  4. +
  5. +

    +Add the channel to the channel map. +

    +
  6. +
+

Due to the beauty of STM, this is pretty easy.

+
(jobId, chan) <- liftIO $ atomically $ do
+    jobId <- readTVar nextJob
+    writeTVar nextJob $! jobId + 1
+    chan <- newBroadcastTChan
+    m <- readTVar jobs
+    writeTVar jobs $ IntMap.insert jobId chan m
+    return (jobId, chan)
+
+
+

Fork our background job

+

There are many different ways we could go about this, and they depend entirely +on what the background job is going to be. Here’s a minimal example of a +background job that prints out a few messages, with a 1 second delay between +each message. Note how after our final message, we broadcast a Nothing value +and remove our channel from the map of channels.

+
liftIO $ forkIO $ do
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something\n"
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something else\n"
+    threadDelay 1000000
+    atomically $ do
+        writeTChan chan $ Just "All done\n"
+        writeTChan chan Nothing
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.delete jobId m
+
+
+

View progress

+

For this demonstration, I’ve elected for a very simple progress viewing: a +plain text page with stream response. There are a few other possibilities here: +an HTML page that auto-refreshes every X seconds or using eventsource or +websockets. I encourage you to give those a shot also, but here’s the simplest +implementation I can think of:

+
getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+

We start off by looking up the channel in the map. If we can’t find it, it +means the job either never existed, or has already been completed. In either +event, we return a 404. (Another possible enhancement would be to store some +information on all previously completed jobs and let the user know if they’re +done.)

+

Assuming the channel exists, we use respondSource to start a streaming +response. We then repeatedly call readTChan until we get a Nothing value, +at which point we exit (via return ()). Notice that on each iteration, we +call both sendChunkText and sendFlush. Without that second call, the user +won’t receive any updates until the output buffer completely fills up, which is +not what we want for a real-time update system.

+
+
+

Complete application

+

For completeness, here’s the full source code for this application:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Control.Concurrent     (forkIO, threadDelay)
+import           Control.Concurrent.STM
+import           Data.IntMap            (IntMap)
+import qualified Data.IntMap            as IntMap
+import           Data.Text              (Text)
+import           Yesod
+
+data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+/view-progress/#Int ViewProgressR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "PubSub example"
+    [whamlet|
+        <form method=post>
+            <button>Start new background job
+    |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    App {..} <- getYesod
+    (jobId, chan) <- liftIO $ atomically $ do
+        jobId <- readTVar nextJob
+        writeTVar nextJob $! jobId + 1
+        chan <- newBroadcastTChan
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.insert jobId chan m
+        return (jobId, chan)
+    liftIO $ forkIO $ do
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something\n"
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something else\n"
+        threadDelay 1000000
+        atomically $ do
+            writeTChan chan $ Just "All done\n"
+            writeTChan chan Nothing
+            m <- readTVar jobs
+            writeTVar jobs $ IntMap.delete jobId m
+    redirect $ ViewProgressR jobId
+
+getViewProgressR :: Int -> Handler TypedContent
+getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+
+main :: IO ()
+main = do
+    jobs <- newTVarIO IntMap.empty
+    nextJob <- newTVarIO 1
+    warp 3000 App {..}
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/sql-joins.html b/public/book-1.4/sql-joins.html new file mode 100644 index 00000000..7bfe8315 --- /dev/null +++ b/public/book-1.4/sql-joins.html @@ -0,0 +1,556 @@ + SQL Joins :: Yesod Web Framework Book- Version 1.4 +
+

SQL Joins

+ + +

Persistent touts itself as a database-agnostic interface. How, then, are you +supposed to do things which are inherently backend-specific? This most often +comes up in Yesod when you want to join two tables together. There are some +pure-Haskell solutions that are completely backend-agonistic, but there are +also more efficient methods at our disposal. In this chapter, we’ll introduce a +common problem you might want to solve, and then build up more sophisticated +solutions.

+
+

Multi-author blog

+

Since blogs are a well understood problem domain, we’ll use that for our +problem setup. Consider a blog engine that allows you to have multiple authors +in the database, and each blog post will have a single author. In Persistent, +we may model this as:

+
Author
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+

Let’s set up our initial Yesod application to show a blog post index indicating +the blog title and the author:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import           Control.Monad.Logger
+import           Data.Text               (Text)
+import           Database.Persist.Sqlite
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Author
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+|]
+
+data App = App
+    { persistConfig :: SqliteConf
+    , connPool      :: ConnectionPool
+    }
+instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB = defaultRunDB persistConfig connPool
+instance YesodPersistRunner App where
+    getDBRunner = defaultGetDBRunner connPool
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/blog/#BlogId BlogR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ selectList [] []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall Entity blogid blog <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>
+                            #{blogTitle blog} by #{show $ blogAuthor blog}
+        |]
+
+getBlogR :: BlogId -> Handler Html
+getBlogR _ = error "Implementation left as exercise to reader"
+
+main :: IO ()
+main = do
+    -- Use an in-memory database with 1 connection. Terrible for production,
+    -- but useful for testing.
+    let conf = SqliteConf ":memory:" 1
+    pool <- createPoolConfig conf
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        -- Fill in some testing data
+        alice <- insert $ Author "Alice"
+        bob   <- insert $ Author "Bob"
+
+        insert_ $ Blog alice "Alice's first post" "Hello World!"
+        insert_ $ Blog bob "Bob's first post" "Hello World!!!"
+        insert_ $ Blog alice "Alice's second post" "Goodbye World!"
+
+    warp 3000 App
+        { persistConfig = conf
+        , connPool      = pool
+        }
+

That’s all well and good, but let’s look at the output:

+

Authors appear as numeric identifiers

+ + + + + + +
+

All we’re doing is displaying the numeric identifier of each author, instead of +the author’s name. In order to fix this, we need to pull extra information from +the Author table as well. Let’s dive in to getting that done.

+
+
+

Database queries in Widgets

+

I’ll address this one right off the bat, since it catches many users by +surprise. You might think that you can solve this problem in the Hamlet +template itself, e.g.:

+
<ul>
+    $forall Entity blogid blog <- blogs
+        $with author <- runDB $ get404 $ blogAuthor
+            <li>
+                <a href=@{BlogR blogid}>
+                    #{blogTitle blog} by #{authorName author}
+

However, this isn’t allowed, because Hamlet will not allow you to run +database actions inside of it. One of the goals of Shakespearean templates is +to help you keep your pure and impure code separated, with the idea being that +all impure code needs to stay in Haskell.

+

But we can actually tweak the above code to work in Yesod. The idea is to +separate out the code for each blog entry into a Widget function, and then +perform the database action in the Haskell portion of the function:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ selectList [] []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall blogEntity <- blogs
+                    ^{showBlogLink blogEntity}
+        |]
+
+showBlogLink :: Entity Blog -> Widget
+showBlogLink (Entity blogid blog) = do
+    author <- handlerToWidget $ runDB $ get404 $ blogAuthor blog
+    [whamlet|
+        <li>
+            <a href=@{BlogR blogid}>
+                #{blogTitle blog} by #{authorName author}
+    |]
+

We need to use handlerToWidget to turn our Handler action into a Widget +action, but otherwise the code is straightforward. And furthermore, we now get +exactly the output we wanted:

+

Authors appear as names

+ + + + + + +
+
+
+

Joins

+

If we have the exact result we’re looking for, why isn’t this chapter over? The +problem is that this technique is highly inefficient. We’re performing one +database query to load up all of the blog posts, then a separate query for each +blog post to get the author names. This is far less efficient than simply using +a SQL join. The question is: how do we do a join in Persistent? We’ll start off +by writing some raw SQL:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ rawSql
+        "SELECT ??, ?? \
+        \FROM blog INNER JOIN author \
+        \ON blog.author=author.id"
+        []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall (Entity blogid blog, Entity _ author) <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>
+                            #{blogTitle blog} by #{authorName author}
+        |]
+

We pass the rawSql function two parameters: a SQL query, and a list of +additional parameters to replace placeholders in the query. That list is empty, +since we’re not using any placeholders. However, note that we’re using ?? in +our SELECT statement. This is a form of type inspection: rawSql will detect +the type of entities being demanded, and automatically fill in the fields that +are necessary to make the query.

+

rawSql is certainly powerful, but it’s also unsafe. There’s no syntax +checking on your SQL query string, so you can get runtime errors. Also, it’s +easy to end up querying for the wrong type and end up with very confusing +runtime error messages.

+
+
+

Esqueleto

+

Persistent has a companion library- Esqueleto- which provides an expressive, +type safe DSL for writing SQL queries. It takes advantage of the Persistent +types to ensure it generates valid SQL queries and produces the results +requested by the program. In order to use Esqueleto, we’re going to add some +imports:

+
import qualified Database.Esqueleto      as E
+import           Database.Esqueleto      ((^.))
+

And then write our query using Esqueleto:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB
+           $ E.select
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall (E.Value blogid, E.Value title, E.Value name) <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>#{title} by #{name}
+        |]
+

Notice how similar the query looks to the SQL we wrote previously. One thing of +particular interest is the ^. operator, which is a projection. blog ^. +BlogAuthor, for example, means "take the author column of the blog table." +And thanks to the type safety of Esqueleto, you could never accidentally +project AuthorName from blog: the type system will stop you!

+

In addition to safety, there’s also a performance advantage to Esqueleto. +Notice the returned tuple; it explicitly lists the three columns that we +need to generate our listing. This can provide a huge performance boost: unlike +all other examples we’ve had, this one does not require transferring the +(potentially quite large) content column of the blog post to generate the +listing.

+ +

Esqueleto is really the gold standard in writing SQL queries in Persistent. The +rule of thumb should be: if you’re doing something that fits naturally into +Persistent’s query syntax, use Persistent, as it’s database agnostic and a bit +easier to use. But if you’re doing something that would be more efficient with +a SQL-specific feature, you should strongly consider Esqueleto.

+
+
+

Streaming

+

There’s still a problem with our Esqueleto approach. If there are thousands of +blog posts, then the workflow will be:

+
    +
  1. +

    +Read thousands of blog posts into memory on the server. +

    +
  2. +
  3. +

    +Render out the entire HTML page. +

    +
  4. +
  5. +

    +Send the HTML page to the client. +

    +
  6. +
+

This has two downsides: it uses a lot of memory, and it gives high latency for the user. If this is a bad approach, why does Yesod gear you towards it out of the box, instead of tending towards a streaming approach? Two reasons:

+
    +
  • +

    +Correctness: imagine if there was an error reading the 243rd record from the database. By doing a non-streaming response, Yesod can catch the exception and send a meaningful 500 error response. If we were already streaming, the streaming body would simply stop in the middle of a misleading 200 OK respond. +

    +
  • +
  • +

    +Ease of use: it’s usually easier to work with non-streaming bodies. +

    +
  • +
+

The standard recommendation I’d give someone who wants to generate listings +that may be large is to use pagination. This allows you to do less work on the +server, write simple code, get the correctness guarantees Yesod provides out of +the box, and reduce user latency. However, there are times when you’ll really +want to do a streaming response, so let’s cover that here.

+

Switching Esqueleto to a streaming response is easy: replace select with +selectSource. The Esqueleto query itself remains unchanged. Then we’ll use +the respondSourceDB function to generate a streaming database response, and +manually construct our HTML to wrap up the listing.

+
getHomeR :: Handler TypedContent
+getHomeR = do
+    let blogsSrc =
+             E.selectSource
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    render <- getUrlRenderParams
+    respondSourceDB typeHtml $ do
+        sendChunkText "<html><head><title>Blog posts</title></head><body><ul>"
+        blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) ->
+            toFlushBuilder $
+            [hamlet|
+                <li>
+                    <a href=@{BlogR blogid}>#{title} by #{name}
+            |] render
+            )
+        sendChunkText "</ul></body></html>"
+

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 +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 +Flush Builder value, as needed by Yesod’s streaming framework.

+

Unfortunately, we’re no longer able to take advantage of Hamlet to do our +overall page layout, since we need to explicit generate start and end tags +separately. This introduces another point for possible bugs, if we accidentally +create unbalanced tags. We also lose the ability to use defaultLayout, for +exactly the same reason.

+

Streaming HTML responses are a powerful tool, and are sometimes necessary. But +generally speaking, I’d recommend sticking to safer options.

+
+
+

Conclusion

+

This chapter covered a number of ways of doing a SQL join:

+
    +
  • +

    +Avoid the join entirely, and manually grab the associated data in Haskell. This is also known as an application level join. +

    +
  • +
  • +

    +Write the SQL explicitly with rawSql. While somewhat convenient, this loses a lot of Persistent’s type safety. +

    +
  • +
  • +

    +Use Esqueleto’s DSL functionality to create a type-safe SQL query. +

    +
  • +
  • +

    +And if you need it, you can even generate a streaming response from Esqueleto. +

    +
  • +
+

For completeness, here’s the entire body of the final, streaming example:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import           Control.Monad.Logger
+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
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+|]
+
+data App = App
+    { persistConfig :: SqliteConf
+    , connPool      :: ConnectionPool
+    }
+instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB = defaultRunDB persistConfig connPool
+instance YesodPersistRunner App where
+    getDBRunner = defaultGetDBRunner connPool
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/blog/#BlogId BlogR GET
+|]
+
+getHomeR :: Handler TypedContent
+getHomeR = do
+    let blogsSrc =
+             E.selectSource
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    render <- getUrlRenderParams
+    respondSourceDB typeHtml $ do
+        sendChunkText "<html><head><title>Blog posts</title></head><body><ul>"
+        blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) ->
+            toFlushBuilder $
+            [hamlet|
+                <li>
+                    <a href=@{BlogR blogid}>#{title} by #{name}
+            |] render
+            )
+        sendChunkText "</ul></body></html>"
+
+getBlogR :: BlogId -> Handler Html
+getBlogR _ = error "Implementation left as exercise to reader"
+
+main :: IO ()
+main = do
+    -- Use an in-memory database with 1 connection. Terrible for production,
+    -- but useful for testing.
+    let conf = SqliteConf ":memory:" 1
+    pool <- createPoolConfig conf
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        -- Fill in some testing data
+        alice <- insert $ Author "Alice"
+        bob   <- insert $ Author "Bob"
+
+        insert_ $ Blog alice "Alice's first post" "Hello World!"
+        insert_ $ Blog bob "Bob's first post" "Hello World!!!"
+        insert_ $ Blog alice "Alice's second post" "Goodbye World!"
+
+    warp 3000 App
+        { persistConfig = conf
+        , connPool      = pool
+        }
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/understanding-request.html b/public/book-1.4/understanding-request.html new file mode 100644 index 00000000..18188adb --- /dev/null +++ b/public/book-1.4/understanding-request.html @@ -0,0 +1,667 @@ + Understanding a Request :: Yesod Web Framework Book- Version 1.4 +
+

Understanding a Request

+ + +

You can often times get away with using Yesod for quite a while without needing +to understand its internal workings. However, such an understanding is often +times advantageous. This chapter will walk you through the request handling +process for a fairly typical Yesod application. Note that a fair amount of this +discussion involves code changes in Yesod 1.2. Most of the concepts are the +same in previous versions, though the data types involved were a bit messier.

+

Yesod’s usage of Template Haskell to bypass boilerplate code can make it +a bit difficult to understand this process sometimes. If beyond the information +in this chapter you wish to further analyze things, it can be useful to view +GHC’s generated code using -ddump-splices.

+ +
+

Handlers

+

When trying to understand Yesod request handling, we need to look at two +components: how a request is dispatched to the appropriate handler code, and +how handler functions are processed. We’ll start off with the latter, and +then circle back to understanding the dispatch process itself.

+
+

Layers

+

Yesod builds itself on top of WAI, which provides a protocol for web servers +(or, more generally, handlers) and applications to communicate with each +other. This is expressed through two datatypes: Request and Response. Then, an +Application is defined as type:

+
type Application = Request
+                -> (Response -> IO ResponseReceived)
+                -> IO ResponseReceived
+

A WAI handler will take an application and run it.

+ +

Request and Response are both very low-level, trying to represent the HTTP +protocol without too much embellishment. This keeps WAI as a generic tool, but +also leaves out a lot of the information we need in order to implement a web +framework. For example, WAI will provide us with the raw data for all request +headers. But Yesod needs to parse that to get cookie information, and then +parse the cookies in order to extract session information.

+

To deal with this dichotomy, Yesod introduces two new data types: +YesodRequest and YesodResponse. YesodRequest contains a WAI Request, and +also adds in such request information as cookies and session variables, and on +the response side can either be a standard WAI Response, or be a higher-level +representation of such a response including such things as updated session +information and extra response headers. To parallel WAI’s Application, we +have:

+
type YesodApp = YesodRequest -> ResourceT IO YesodResponse
+ +

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 +handler functions, you need to have access to three different things:

+
    +
  • +

    +The YesodRequest value for the current request. +

    +
  • +
  • +

    +Some basic environment information, like how to log messages or handle error conditions. This is provided by the datatype RunHandlerEnv. +

    +
  • +
  • +

    +A mutable variable to keep track of updateable information, such as the headers to be returned and the user session state. This is called GHState. (I know that’s not a great name, but it’s there for historical reasons.) +

    +
  • +
+

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 +a step further and converts all the way to a WAI Application.

+
+
+

Content

+

Our example above, and many others you’ve already seen, give a handler +with a type of Handler Html. We’ve just described what the Handler means, +but how does Yesod know how to deal with Html? The answer lies in the +ToTypedContent typeclass. The relevants bit of code are:

+
data Content = ContentBuilder !BBuilder.Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush BBuilder.Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+data TypedContent = TypedContent !ContentType !Content
+
+class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

The Content datatype represents the different ways you can provide a response +body. The first three mirror WAI’s representation directly. The fourth +(ContentDontEvaluate) is used to indicate to Yesod whether response bodies +should be fully evaluated before being returned to users. The advantage to +fully evaluating is that we can provide meaningful error messages if an +exception is thrown from pure code. The downside is possibly increased time and +memory usage.

+

In any event, Yesod knows how to turn a Content into a response body. The +ToContent typeclass provides a way to allow many different datatypes to be +converted into response bodies. Many commonly used types are already instances +of ToContent, including strict and lazy ByteString and Text, and of course +Html.

+

TypedContent adds an extra piece of information: the content type of the value. +As you might expect, there are ToTypedContent instances for a number of common +datatypes, including Html, aeson’s Value (for JSON), and Text (treated as plain text).

+
instance ToTypedContent J.Value where
+    toTypedContent v = TypedContent typeJson (toContent v)
+instance ToTypedContent Html where
+    toTypedContent h = TypedContent typeHtml (toContent h)
+instance ToTypedContent T.Text where
+    toTypedContent t = TypedContent typePlain (toContent t)
+

Putting this all together: a Handler is able to return any value which is an +instance of ToTypedContent, and Yesod will handle turning it into an +appropriate representation and setting the Content-Type response header.

+
+
+

Short-circuit responses

+

One other oddity is how short-circuiting works. For example, you can call +redirect in the middle of a handler function, and the rest of the function will +not be called. The mechanism we use is standard Haskell exceptions. Calling +redirect just throws an exception of type HandlerContents. The runHandler +function will catch any exceptions thrown and produce an appropriate response. +For HandlerContents, each constructor gives a clear action to perform, be it +redirecting or sending a file. For all other exception types, an error message +is displayed to the user.

+
+
+
+

Dispatch

+

Dispatch is the act of taking an incoming request and generating an appropriate +response. We have a few different constraints regarding how we want to handle +dispatch:

+
    +
  • +

    +Dispatch based on path segments (or pieces). +

    +
  • +
  • +

    +Optionally dispatch on request method. +

    +
  • +
  • +

    +Support subsites: packaged collections of functionality providing multiple routes under a specific URL prefix. +

    +
  • +
  • +

    +Support using WAI applications as subsites, while introducing as little + runtime overhead to the process as possible. In particular, we want to avoid + performing any unnecessary parsing to generate a YesodRequest if it + won’t be used. +

    +
  • +
+

The lowest common denominator for this would be to simply use a WAI +Application. However, this doesn’t provide quite enough information: we +need access to the foundation datatype, the logger, and for subsites how a +subsite route is converted to a parent site route. To address this, we have two +helper data types- YesodRunnerEnv and YesodSubRunnerEnv- providing this extra +information for normal sites and subsites.

+

With those types, dispatch now becomes a relatively simple matter: give me an +environment and a request, and I’ll give you a response. This is +represented by the typeclasses YesodDispatch and YesodSubDispatch:

+
class Yesod site => YesodDispatch site where
+    yesodDispatch :: YesodRunnerEnv site -> W.Application
+
+class YesodSubDispatch sub m where
+    yesodSubDispatch :: YesodSubRunnerEnv sub (HandlerSite m) m
+                     -> W.Application
+

We’ll see a bit later how YesodSubDispatch is used. Let’s first +understand how YesodDispatch comes into play.

+
+

toWaiApp, toWaiAppPlain, and warp

+

Let’s assume for the moment that you have a datatype which is an instance +of YesodDispatch. You’ll want to now actually run this thing somehow. To +do this, we need to convert it into a WAI Application and pass it to some kind +of a WAI handler/server. To start this journey, we use toWaiAppPlain. It +performs any appwide initialization necessary. At the time of writing, this +means allocating a logger and setting up the session backend, but more +functionality may be added in the future. Using this data, we can now create a +YesodRunnerEnv. And when that value is passed to yesodDispatch, we get a WAI +Application.

+

We’re almost done. The final remaining modification is path segment +cleanup. The Yesod typeclass includes a member function named cleanPath which +can be used to create canonical URLs. For example, the default implementation +would remove double slashes and redirect a user from /foo//bar to /foo/bar. +toWaiAppPlain adds in some pre-processing to the normal WAI request by +analyzing the requested path and performing cleanup/redirect as necessary.

+

At this point, we have a fully functional WAI Application. There are two other +helper functions included. toWaiApp wraps toWaiAppPlain and additionally +includes some commonly used WAI middlewares, including request logging and GZIP +compression. (Please see the Haddocks for an up-to-date list.) We finally have +the warp function which, as you might guess, runs your application with Warp.

+ +
+
+

Generated code

+

The last remaining black box is the Template Haskell generated code. This +generated code is responsible for handling some of the tedious, error-prone +pieces of your site. If you want to, you can write these all by hand instead. +We’ll demonstrate what that translation would look like, and in the +process elucidate how YesodDispatch and YesodSubDispatch work. Let’s +start with a fairly typical Yesod application.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+
+instance Yesod App
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req sendResponse = sendResponse $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+

For completeness we’ve provided a full listing, but let’s focus on +just the Template Haskell portion:

+
mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+

While this generates a few pieces of code, we only need to replicate three +components to make our site work. Let’s start with the simplest: the +Handler type synonym:

+
type Handler = HandlerT App IO
+

Next is the type-safe URL and its rendering function. The rendering function is +allowed to generate both path segments and query string parameters. Standard +Yesod sites never generate query-string parameters, but it is technically +possible. And in the case of subsites, this often does happen. Notice how we +handle the qs parameter for the MySubsiteR case:

+
instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+

You can see that there’s a fairly simple mapping from the higher-level +route syntax and the RenderRoute instance. Each route becomes a constructor, +each URL parameter becomes an argument to its constructor, we embed a route for +the subsite, and use toPathPiece to render parameters to text.

+

The final component is the YesodDispatch instance. Let’s look at this in +a few pieces.

+
instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+

As described above, yesodDispatch is handed both an environment and a WAI +Request value. We can now perform dispatch based on the requested path, or in +WAI terms, the pathInfo. Referring back to our original high-level route +syntax, we can see that our first route is going to be the single piece +only-get, which we pattern match for.

+

Once that match has succeeded, we additionally pattern match on the request +method; if it’s GET, we use the handler function getOnlyGetR. +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 +to the handler function, and then convert the resulting YesodResponse into a +WAI Response.

+

Wonderful; one down, three to go. The next one is even easier.

+
            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+

Unlike OnlyGetR, AnyMethodR will work for any request method, so we don’t +need to perform any further pattern matching.

+
            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+

We add in one extra complication here: a dynamic parameter. While we used +toPathPiece to render to a textual value above, we now use fromPathPiece to +perform the parsing. Assuming the parse succeeds, we then follow a very similar +dispatch system as was used for OnlyGetR. The prime difference is that our +parameter needs to be passed to both the handler function and the route data +constructor.

+

Next we’ll look at the subsite, which is quite different.

+
            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+

Unlike the other pattern matches, here we just look to see if our pattern +prefix matches. Any route beginning with /my-subsite should be passed off to +the subsite for processing. This is where we finally get to use +yesodSubDispatch. This function closely mirrors yesodDispatch. We need to +construct a new environment to be passed to it. Let’s discuss the four +fields:

+
    +
  • +

    +ysreGetSub demonstrates how to get the subsite foundation type from the + master site. We provide getMySubsite, which is the function we provided in + the high-level route syntax. +

    +
  • +
  • +

    +ysreParentRunner provides a means of running a handler function. It may seem + a bit boring to just provide yesodRunner, but by having a separate parameter + we allow the construction of deeply nested subsites, which will wrap and + unwrap many layers of interleaving subsites. (This is a more advanced concept + which we won’t be covering in this chapter.) +

    +
  • +
  • +

    +ysreToParentRoute will convert a route for the subsite into a route for the + parent site. This is the purpose of the MySubsiteR constructor. This allows + subsites to use functions such as getRouteToParent. +

    +
  • +
  • +

    +ysreParentEnv simply passes on the initial environment, which contains a + number of things the subsite may need (such as logger). +

    +
  • +
+

The other interesting thing is how we modify the pathInfo. This allows subsites +to continue dispatching from where the parent site left off. To demonstrate +how this works, see some screenshots of various requests in the following +figure.

+

Path info in subsite

+ + + + + + +
+

And finally, not all requests will be valid routes. For those cases, we just +want to respond with a 404 not found.

+
            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+
+

Complete code

+

Following is the full code for the non-Template Haskell approach.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+import           Yesod.Core.Types           (YesodSubRunnerEnv (..))
+
+data App = App
+
+instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+
+type Handler = HandlerT App IO
+
+instance Yesod App
+
+instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req sendResponse = sendResponse $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+
+
+
+

Conclusion

+

Yesod abstracts away quite a bit of the plumbing from you as a developer. Most +of this is boilerplate code that you’ll be happy to ignore. But it can be +empowering to understand exactly what’s going on under the surface. At +this point, you should hopefully be able- with help from the Haddocks- to write +a site without any of the autogenerated Template Haskell code. Not that +I’d recommend it; I think using the generated code is easier and safer.

+

One particular advantage of understanding this material is seeing where Yesod +sits in the world of WAI. This makes it easier to see how Yesod will interact +with WAI middleware, or how to include code from other WAI framework in a Yesod +application (or vice-versa!).

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/visitor-counter.html b/public/book-1.4/visitor-counter.html new file mode 100644 index 00000000..e23225d4 --- /dev/null +++ b/public/book-1.4/visitor-counter.html @@ -0,0 +1,164 @@ + Visitor counter :: Yesod Web Framework Book- Version 1.4 +
+

Visitor counter

+ + +

Remember back in the good ol' days of the internet, where no website was +complete without a little "you are visitor number 32" thingy? Ahh, those were +the good times! Let’s recreate that wonderful experience in Yesod!

+

Now, if we wanted to do this properly, we’d store this information in some kind +of persistent storage layer, like a database, so that the information could be +shared across multiple horizontally-scaled web servers, and so that the +information would survive an app restart.

+

But our goal here isn’t to demonstrate good practice (after all, if it was +about good practice, I wouldn’t be demonstrating a visitor counter, right?). +Instead, this is meant to provide a simple example of sharing some state among +multiple handlers. A real-world use case would be caching information across +requests. Just remember that when you use the technique we’ll be showing, you +need to be careful about multiple app servers and app restarts.

+

The technique is simple: we create a new field in the foundation datatype for a +mutable reference to some data, and then access it in each handler. The +technique is so simple, it’s worth just diving into the code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import           Yesod
+
+data App = App
+    { visitors :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    visitorsRef <- fmap visitors getYesod
+    visitors <-
+        liftIO $ atomicModifyIORef visitorsRef $ \i ->
+        (i + 1, i + 1)
+    defaultLayout
+        [whamlet|
+            <p>Welcome, you are visitor number #{visitors}.
+        |]
+
+main :: IO ()
+main = do
+    visitorsRef <- newIORef 0
+    warp 3000 App
+        { visitors = visitorsRef
+        }
+

I used IORef here, since we didn’t need anything more than it provided, but +you’re free to use MVars or TVars as well. In fact, a good exercise for +the reader is to modify the above program to store the visitor count in a +TVar instead.

+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/web-application-interface.html b/public/book-1.4/web-application-interface.html new file mode 100644 index 00000000..cbe94699 --- /dev/null +++ b/public/book-1.4/web-application-interface.html @@ -0,0 +1,346 @@ + Web Application Interface :: Yesod Web Framework Book- Version 1.4 +
+

Web Application Interface

+ + + +

It is a problem almost every language used for web development has dealt with: +the low level interface between the web server and the application. The +earliest example of a solution is the venerable and battle-worn Common Gateway Interface (CGI), +providing a language-agnostic interface using only standard input, standard +output and environment variables.

+

Back when Perl was becoming the de facto web programming language, a major +shortcoming of CGI became apparent: the process needed to be started anew for +each request. When dealing with an interpretted language and application +requiring database connection, this overhead became unbearable. FastCGI (and +later SCGI) arose as a successor to CGI, but it seems that much of the +programming world went in a different direction.

+

Each language began creating its own standard for interfacing with servers. +mod_perl. mod_python. mod_php. mod_ruby. Within the same language, multiple +interfaces arose. In some cases, we even had interfaces on top of interfaces. +And all of this led to much duplicated effort: a Python application designed to +work with FastCGI wouldn’t work with mod_python; mod_python only exists for +certain webservers; and these programming language specific web server +extensions need to be written for each programming language.

+

Haskell has its own history. We originally had the cgi package, which provided +a monadic interface. The fastcgi package then provided the same interface. +Meanwhile, it seemed that the majority of Haskell web development focused on +the standalone server. The problem is that each server comes with its own +interface, meaning that you need to target a specific backend. This means that +it is impossible to share common features, like GZIP encoding, development +servers, and testing frameworks.

+

WAI attempts to solve this, by providing a generic and efficient interface +between web servers and applications. Any handler supporting the interface +can serve any WAI application, while any application using the interface can +run on any handler.

+

At the time of writing, there are various backends, including Warp, FastCGI, +and development server. There are even more esoteric backends like +wai-handler-webkit for creating desktop apps. wai-extra provides many common +middleware components like GZIP, JSON-P and virtual hosting. wai-test makes it +easy to write unit tests, and wai-handler-devel lets you develop your +applications without worrying about stopping to compile. Yesod targets WAI, as +well as other Haskell web frameworks such as Scotty and MFlow. It’s also used +by some applications that skip the framework entirely, including Hoogle.

+ +
+

The Interface

+

The interface itself is very straight-forward: an application takes a request +and returns a response. A response is an HTTP status, a list of headers and a +response body. A request contains various information: the requested path, +query string, request body, HTTP version, and so on.

+

In order to handle resource management in an exception-safe manner, we use +continuation passing style for returning the response, similar to how the +bracket function works. This makes our definition of an application look +like:

+
type Application =
+    Request ->
+    (Response -> IO ResponseReceived) ->
+    IO ResponseReceived
+

The first argument is a Request, which shouldn’t be too surprising. The +second argument is the continuation, or what we should do with a Response. +Generally speaking, this will just be sending it to the client. We use the +special ResponseReceived type to ensure that the application does in fact +call the continuation.

+

This may seem a little strange, but usage is pretty straight-forward, as we’ll +demonstrate below.

+
+

Response Body

+

Haskell has a datatype known as a lazy bytestring. By utilizing laziness, you +can create large values without exhausting memory. Using lazy I/O, you can do +such tricks as having a value which represents the entire contents of a file, +yet only occupies a small memory footprint. In theory, a lazy bytestring is the +only representation necessary for a response body.

+

In practice, while lazy byte strings are wonderful for generating "pure" +values, the lazy I/O necessary to read a file introduces some non-determinism +into our programs. When serving thousands of small files a second, the limiting +factor is not memory, but file handles. Using lazy I/O, file handles may not be +freed immediately, leading to resource exhaustion. To deal with this, WAI +provides its own streaming data interface.

+

The core of this streaming interface is the Builder. A Builder represents +an action to fill up a buffer with bytes of data. This is more efficient than +simply passing ByteStrings around, as it can avoid multiple copies of data. +In many cases, an application needs only to provide a single Builder value. +And for that simple case, we have the ResponseBuilder constructor.

+

However, there are times when an Application will need to interleave IO +actions with yielding of data to the client. For that case, we have +ResponseStream. With ResponseStream, you provide a function. This +function in turn takes two actions: a "yield more data" action, and a "flush +the buffer" action. This allows you to yield data, perform IO actions, and +flush, as many times as you need, and with any interleaving desired.

+

There is one further optimization: many operating systems provide a sendfile +system call, which sends a file directly to a socket, bypassing a lot of the +memory copying inherent in more general I/O system calls. For that case, we +have a ResponseFile.

+

Finally, there are some cases where we need to break out of the HTTP mode +entirely. Two examples are WebSockets, where we need to upgrade a half-duplex +HTTP connection to a full-duplex connection, and HTTPS proxying, which requires +our proxy server to establish a connection, and then become a dumb data +transport agent. For these cases, we have the ResponseRaw constructor. Note +that not all WAI handlers can in fact support ResponseRaw, though the most +commonly used handler, Warp, does provide this support.

+
+
+

Request Body

+

Like response bodies, we could theoretically use a lazy ByteString for request +bodies, but in practice we want to avoid lazy I/O. Instead, the request body is +represented with a IO ByteString action (ByteString here being a strict +ByteString). Note that this does not return the entire request body, but +rather just the next chunk of data. Once you’ve consumed the entire request +body, further calls to this action will return an empty ByteString.

+

Note that, unlike response bodies, we have no need for using Builders on +the request side, since our purpose is purely for reading.

+

The request body could in theory contain any type of data, but the most common +are URL encoded and multipart form data. The wai-extra package contains +built-in support for parsing these in a memory-efficient manner.

+
+
+
+

Hello World

+

To demonstrate the simplicity of WAI, let’s look at a hello world example. In +this example, we’re going to use the OverloadedStrings language extension to +avoid explicitly packing string values into bytestrings.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.HTTP.Types (status200)
+import Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $
+  responseLBS status200 [("Content-Type", "text/plain")] "Hello World"
+
+main = run 3000 application
+

Lines 2 through 4 perform our imports. Warp is provided by the warp package, +and is the premiere WAI backend. WAI is also built on top of the http-types +package, which provides a number of datatypes and convenience values, including +status200.

+

First we define our application. Since we don’t care about the specific request +parameters, we ignore the first argument to the function, which contains the +request value. The second argument is our "send a response" function, which we +immediately use. The response value we send is built from a lazy ByteString +(thus responseLBS), with status code 200 ("OK"), a text/plain content type, +and a body containing the words "Hello World". Pretty straight-forward.

+
+
+

Resource allocation

+

Let’s make this a little more interesting, and try to allocate a resource for +our response. We’ll create an MVar in our main function to track the number +of requests, and then hold that MVar while sending each response.

+
{-# LANGUAGE OverloadedStrings #-}
+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)
+
+application countRef _ respond = do
+    modifyMVar countRef $ \count -> do
+        let count' = count + 1
+            msg = fromByteString "You are visitor number: " <>
+                  fromShow count'
+        responseReceived <- respond $ responseBuilder
+            status200
+            [("Content-Type", "text/plain")]
+            msg
+        return (count', responseReceived)
+
+main = do
+    visitorCount <- newMVar 0
+    run 3000 $ application visitorCount
+

This is where WAI’s continuation interface shines. We’re able to use the +standard modifyMVar function to acquire the MVar lock and send our +response. Note how we thread the responseReceived value through, though we +never actually use the value for anything. It is merely witness to the fact +that we have, in fact, sent a response.

+

Notice also how we take advantage of Builders in constructing our msg +value. Instead of concatenating two ByteStrings together directly, we +monoidally append two different Builder values. The advantage to this is that +the results will end up being copied directly into the final output buffer, +instead of first being copied into a temporary ByteString buffer to only +later be copied into the final buffer.

+
+
+

Streaming response

+

Let’s give our streaming interface a test as well:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (fromByteString)
+import           Control.Concurrent       (threadDelay)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai
+import           Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $ responseStream status200 [("Content-Type", "text/plain")]
+    $ \send flush -> do
+        send $ fromByteString "Starting the response...\n"
+        flush
+        threadDelay 1000000
+        send $ fromByteString "All done!\n"
+
+main = run 3000 application
+

We use responseStream, and our third argument is a function which takes our +"send a builder" and "flush the buffer" functions. Notice how we flush after +our first chunk of data, to make sure the client sees the data immediately. +However, there’s no need to flush at the end of a response. WAI requires that +the handler automatically flush at the end of a stream.

+
+
+

Middleware

+

In addition to allowing our applications to run on multiple backends without +code changes, the WAI allows us another benefits: middleware. Middleware is +essentially an application transformer, taking one application and returning +another one.

+

Middleware components can be used to provide lots of services: cleaning up +URLs, authentication, caching, JSON-P requests. But perhaps the most useful and +most intuitive middleware is gzip compression. The middleware works very +simply: it parses the request headers to determine if a client supports +compression, and if so compresses the response body and adds the appropriate +response header.

+

The great thing about middlewares is that they are unobtrusive. Let’s see how +we would apply the gzip middleware to our hello world application.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.Wai.Handler.Warp (run)
+import Network.Wai.Middleware.Gzip (gzip, def)
+import Network.HTTP.Types (status200)
+
+application _ respond = respond $ responseLBS status200 [("Content-Type", "text/plain")]
+                       "Hello World"
+
+main = run 3000 $ gzip def application
+

We added an import line to actually have access to the middleware, and then +simply applied gzip to our application. You can also chain together multiple +middlewares: a line such as gzip False $ jsonp $ othermiddleware $ +myapplication is perfectly valid. One word of warning: the order the +middleware is applied can be important. For example, jsonp needs to work on +uncompressed data, so if you apply it after you apply gzip, you’ll have +trouble.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/widgets.html b/public/book-1.4/widgets.html new file mode 100644 index 00000000..3258cd6e --- /dev/null +++ b/public/book-1.4/widgets.html @@ -0,0 +1,636 @@ + Widgets :: Yesod Web Framework Book- Version 1.4 +
+

Widgets

+ + +

One of the challenges in web development is that we have to coordinate three +different client-side technologies: HTML, CSS and Javascript. Worse still, we +have to place these components in different locations on the page: CSS in a +style tag in the head, Javascript in a script tag before the closing body tag, and HTML in the +body. And never mind if you want to put your CSS and Javascript in separate +files!

+

In practice, this works out fairly nicely when building a single page, because +we can separate our structure (HTML), style (CSS) and logic (Javascript). But +when we want to build modular pieces of code that can be easily composed, it +can be a headache to coordinate all three pieces separately. Widgets are +Yesod’s solution to the problem. They also help with the issue of including +libraries, such as jQuery, one time only.

+

Our four template languages- Hamlet, Cassius, Lucius and Julius- provide the +raw tools for constructing your output. Widgets provide the glue that allows +them to work together seamlessly.

+
+

Synopsis

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR = defaultLayout $ do
+    setTitle "My Page Title"
+    toWidget [lucius| h1 { color: green; } |]
+    addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
+    toWidget
+        [julius|
+            $(function() {
+                $("h1").click(function(){
+                    alert("You clicked on the heading!");
+                });
+            });
+        |]
+    toWidgetHead
+        [hamlet|
+            <meta name=keywords content="some sample keywords">
+        |]
+    toWidget
+        [hamlet|
+            <h1>Here's one way of including content
+        |]
+    [whamlet|<h2>Here's another |]
+    toWidgetBody
+        [julius|
+            alert("This is included in the body itself");
+        |]
+
+main = warp 3000 App
+

This produces the following HTML (indentation added):

+
<!DOCTYPE html>
+<html>
+  <head>
+    <title>My Page Title</title>
+    <meta name="keywords" content="some sample keywords">
+    <style>h1{color:green}</style>
+  </head>
+  <body>
+    <h1>Here's one way of including content</h1>
+    <h2>Here's another</h2>
+    <script>
+      alert("This is included in the body itself");
+    </script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">
+    </script><script>
+      $(function() {
+        $('h1').click(function() {
+          alert("You clicked on the heading!");
+        });
+      });
+    </script>
+  </body>
+</html>
+
+
+

What’s in a Widget?

+

At a very superficial level, an HTML document is just a bunch of nested tags. +This is the approach most HTML generation tools take: you define hierarchies of +tags and are done with it. But let’s imagine that I want to write a component +of a page for displaying the navbar. I want this to be "plug and play": I call +the function at the right time, and the navbar is inserted at the correct point +in the hierarchy.

+

This is where our superficial HTML generation breaks down. Our navbar likely +consists of some CSS and JavaScript in addition to HTML. By the time we call +the navbar function, we have already rendered the <head> tag, so it is too +late to add a new <style> tag for our CSS declarations. Under normal +strategies, we would need to break up our navbar function into three parts: +HTML, CSS and JavaScript, and make sure that we always call all three pieces.

+

Widgets take a different approach. Instead of viewing an HTML document as a +monolithic tree of tags, widgets see a number of distinct components in the +page. In particular:

+
    +
  • +

    +The title +

    +
  • +
  • +

    +External stylesheets +

    +
  • +
  • +

    +External Javascript +

    +
  • +
  • +

    +CSS declarations +

    +
  • +
  • +

    +Javascript code +

    +
  • +
  • +

    +Arbitrary <head> content +

    +
  • +
  • +

    +Arbitrary <body> content +

    +
  • +
+

Different components have different semantics. For example, there can only be +one title, but there can be multiple external scripts and stylesheets. However, +those external scripts and stylesheets should only be included once. Arbitrary +head and body content, on the other hand, has no limitation (someone may want +to have five lorem ipsum blocks after all).

+

The job of a widget is to hold onto these disparate components and apply proper +logic for combining different widgets together. This consists of things like +taking the last title set and ignoring others, filtering duplicates from the +list of external scripts and stylesheets, and concatenating head and body +content.

+
+
+

Constructing Widgets

+

In order to use widgets, you’ll obviously need to be able to get your hands on +them. The most common way will be via the ToWidget typeclass, and its +toWidget method. This allows you to convert your Shakespearean templates +directly to a Widget: Hamlet code will appear in the body, Julius scripts +inside a <script>, and Cassius and Lucius in a <style> tag.

+ +

But what if you want to add some <meta> tags, which need to appear in +the head? Or if you want some Javascript to appear in the body instead of the +head? For these purposes, Yesod provides two additional type classes: +ToWidgetHead and ToWidgetBody. These work exactly as they seem they should. One example use case for this is to have fine-grained control of where your <script> tags end up getting inserted.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "toWidgetHead and toWidgetBody"
+    toWidgetBody
+        [hamlet|<script src=/included-in-body.js>|]
+    toWidgetHead
+        [hamlet|<script src=/included-in-head.js>|]
+
+main :: IO ()
+main = warp 3001 App
+

Note that even though toWidgetHead was called after toWidgetBody, the +latter <script> tag appears first in the generated HTML.

+

In addition, there are a number of other functions for creating specific kinds +of Widgets:

+
+
+setTitle +
+

+Turns some HTML into the page title. +

+
+
+toWidgetMedia +
+

+Works the same as toWidget, but takes an +additional parameter to indicate what kind of media this applies to. Useful for +creating print stylesheets, for instance. +

+
+
+addStylesheet +
+

+Adds a reference, via a <link> tag, to an external +stylesheet. Takes a type-safe URL. +

+
+
+addStylesheetRemote +
+

+Same as addStylesheet, but takes a normal URL. Useful +for referring to files hosted on a CDN, like Google’s jQuery UI CSS files. +

+
+
+addScript +
+

+Adds a reference, via a <script> tag, to an external script. +Takes a type-safe URL. +

+
+
+addScriptRemote +
+

+Same as addScript, but takes a normal URL. Useful for +referring to files hosted on a CDN, like Google’s jQuery. +

+
+
+
+
+

Combining Widgets

+

The whole idea of widgets is to increase composability. You can take these +individual pieces of HTML, CSS and Javascript, combine them together into +something more complicated, and then combine these larger entities into +complete pages. This all works naturally through the Monad instance of +Widget, meaning you can use do-notation to compose pieces together.

+
myWidget1 = do
+    toWidget [hamlet|<h1>My Title|]
+    toWidget [lucius|h1 { color: green } |]
+
+myWidget2 = do
+    setTitle "My Page Title"
+    addScriptRemote "http://www.example.com/script.js"
+
+myWidget = do
+    myWidget1
+    myWidget2
+
+-- or, if you want
+myWidget' = myWidget1 >> myWidget2
+ +
+
+

Generate IDs

+

If we’re really going for true code reuse here, we’re eventually going to run +into name conflicts. Let’s say that there are two helper libraries that both +use the class name “foo” to affect styling. We want to avoid such a +possibility. Therefore, we have the newIdent function. This function +automatically generates a word that is unique for this handler.

+
getRootR = defaultLayout $ do
+    headerClass <- newIdent
+    toWidget [hamlet|<h1 .#{headerClass}>My Header|]
+    toWidget [lucius| .#{headerClass} { color: green; } |]
+
+
+

whamlet

+

Let’s say you’ve got a fairly standard Hamlet template, that embeds another +Hamlet template to represent the footer:

+
page =
+    [hamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+
+footer =
+    [hamlet|
+        <footer>
+            <p>That's all folks!
+    |]
+

That works fine if the footer is plain old HTML, but what if we want to add +some style? Well, we can easily spice up the footer by turning it into a +Widget:

+
footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+

But now we’ve got a problem: a Hamlet template can only embed another Hamlet +template; it knows nothing about a Widget. This is where whamlet comes in. It +takes exactly the same syntax as normal Hamlet, and variable (#{…}) and URL +(@{…}) interpolation are unchanged. But embedding (^{…}) takes a Widget, +and the final result is a Widget. To use it, we can just do:

+
page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

There is also whamletFile, if you would prefer to keep your template in a +separate file.

+ +
+

Types

+

You may have noticed that I’ve been avoiding type signatures so far. The simple +answer is that each widget is a value of type Widget. But if you look through +the Yesod libraries, you’ll find no definition of the Widget type. What +gives?

+

Yesod defines a very similar type: data WidgetT site m a. This data type is a +monad transformer. The last two arguments are the underlying monad and the +monadic value, respectively. The site parameter is the specific foundation +type for your individual application. Since this type varies for each and every +site, it’s impossible for the libraries to define a single Widget datatype +which would work for every application.

+

Instead, the mkYesod Template Haskell function generates this type synonym +for you. Assuming your foundation data type is called MyApp, your Widget +synonym is defined as:

+
type Widget = WidgetT MyApp IO ()
+

We set the monadic value to be (), since a widget’s value will ultimately be +thrown away. IO is the standard base monad, and will be used in almost all +cases. The only exception is when writing a subsite. Subsites are a more +advanced topic, and will be covered later in their own chapter.

+

Once we know about our Widget type synonym, it’s easy to add signatures to +our previous code samples:

+
footer :: Widget
+footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+
+page :: Widget
+page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

When we start digging into handler functions some more, we’ll encounter a +similar situation with the HandlerT and Handler types.

+
+
+
+

Using Widgets

+

It’s all well and good that we have these beautiful Widget datatypes, but how +exactly do we turn them into something the user can interact with? The most +commonly used function is defaultLayout, which essentially has the type +signature Widget → Handler Html.

+

defaultLayout is actually a typeclass method, which can be overridden for +each application. This is how Yesod apps are themed. So we’re still left with +the question: when we’re inside defaultLayout, how do we unwrap a Widget? +The answer is widgetToPageContent. Let’s look at some (simplified) types:

+
data PageContent url = PageContent
+    { pageTitle :: Html
+    , pageHead :: HtmlUrl url
+    , pageBody :: HtmlUrl url
+    }
+widgetToPageContent :: Widget -> Handler (PageContent url)
+

This is getting closer to what we need. We now have direct access to the HTML +making up the head and body, as well as the title. At this point, we can use +Hamlet to combine them all together into a single document, along with our site +layout, and we use withUrlRenderer to convert that Hamlet result into actual +HTML that’s ready to be shown to the user. The next example demonstrates this +process.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent widget
+    withUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    <style>body { font-family: verdana }
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

There’s still one thing that bothers me: that style tag. There are a few +problems with it:

+
    +
  • +

    +Unlike Lucius or Cassius, it doesn’t get compile-time checked for + correctness. +

    +
  • +
  • +

    +Granted that the current example is very simple, but in something more + complicated we could get into character escaping issues. +

    +
  • +
  • +

    +We’ll now have two style tags instead of one: the one produced by myLayout, + and the one generated in the pageHead based on the styles set in the + widget. +

    +
  • +
+

We have one more trick in our bag to address this: we apply some last-minute +adjustments to the widget itself before calling widgetToPageContent. It’s +actually very easy to do: we just use do-notation again.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent $ do
+        widget
+        toWidget [lucius| body { font-family: verdana } |]
+    withUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Using handler functions

+

We haven’t covered too much of the handler functionality yet, but once we do, +the question arises: how do we use those functions in a widget? For example, +what if your widget needs to look up a query string parameter using +lookupGetParam?

+

The first answer is the function handlerToWidget, which can convert a +Handler action into a Widget answer. However, in many cases, this won’t be +necessary. Consider the type signature of lookupGetParam:

+
lookupGetParam :: MonadHandler m => Text -> m (Maybe Text)
+

This function will live in any instance of MonadHandler. And conveniently, +Widget is also a MonadHandler instance. This means that most code can be +run in either Handler or Widget. And if you need to explicitly convert from +Handler to Widget, you can always use handlerToWidget.

+ +
+
+

Summary

+

The basic building block of each page is a widget. Individual snippets of HTML, +CSS, and Javascript can be turned into widgets via the polymorphic toWidget +function. Using do-notation, you can combine these individual widgets into +larger widgets, eventually containing all the content of your page.

+

Unwrapping these widgets is usually performed within the defaultLayout +function, which can be used to apply a unified look-and-feel to all your pages.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/wiki-chat-example.html b/public/book-1.4/wiki-chat-example.html new file mode 100644 index 00000000..41e6edf1 --- /dev/null +++ b/public/book-1.4/wiki-chat-example.html @@ -0,0 +1,597 @@ + Wiki: markdown, chat subsite, event source :: Yesod Web Framework Book- Version 1.4 +
+

Wiki: markdown, chat subsite, event source

+ + +

This example will tie together a few different ideas. We’ll start with a chat +subsite, which allows us to embed a chat widget on any page. We’ll use the HTML +5 event source API to handle sending events from the server to the client.

+
+

Subsite: data

+

In order to define a subsite, we first need to create a foundation type for the +subsite, the same as we would do for a normal Yesod application. In our case, +we want to keep a channel of all the events to be sent to the individual +participants of a chat. This ends up looking like:

+
-- @Chat/Data.hs
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+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
+import           Yesod
+
+-- | Our subsite foundation. We keep a channel of events that all connections
+-- will share.
+data Chat = Chat (Chan ServerEvent)
+

We also need to define our subsite routes in the same module. We need to have +two commands: one to send a new message to all users, and another to receive +the stream of messages.

+
-- @Chat/Data.hs
+mkYesodSubData "Chat" [parseRoutes|
+/send SendR POST
+/recv ReceiveR GET
+|]
+
+
+

Subsite: handlers

+

Now that we’ve defined our foundation and routes, we need to create a separate +module for providing the subsite dispatch functionality. We’ll call this +module Chat, and it’s where we’ll start to see how a subsite functions.

+

A subsite always sits as a layer on top of some master site, which will be +provided by the user. In many cases, a subsite will require specific +functionality to be present in the master site. In the case of our chat +subsite, we want user authentication to be provided by the master site. The +subsite needs to be able to query whether the current user is logged into the +site, and to get the user’s name.

+

The way we represent this concept is to define a typeclass that encapsulates +the necessary functionality. Let’s have a look at our YesodChat typeclass:

+
-- @Chat/Data.hs
+class (Yesod master, RenderMessage master FormMessage)
+        => YesodChat master where
+    getUserName :: HandlerT master IO Text
+    isLoggedIn :: HandlerT master IO Bool
+

Any master site which wants to use the chat subsite will need to provide a +YesodChat instance. (We’ll see in a bit how this requirement is enforced.) +There are a few interesting things to note:

+
    +
  • +

    +We can put further constraints on the master site, such as providing a + Yesod instance and allowing rendering of form messages. The former allows + us to use defaultLayout, while the latter allows us to use standard form + widgets. +

    +
  • +
  • +

    +Previously in the book, we’ve used the Handler monad quite a bit. Remember + that Handler is just an application-specific type synonym around + HandlerT. Since this code is intended to work with many different + applications, we use the full HandlerT form of the transformer. +

    +
  • +
+

Speaking of the Handler type synonym, we’re going to want to have something +similar for our subsite. The question is: what does this monad look like? In a +subsite situation, we end up with two layers of HandlerT transformers: one +for the subsite, and one for the master site. We want to have a synonym that +works for any master site which is an instance of YesodChat, so we end up +with:

+
-- @Chat/Data.hs
+type ChatHandler a =
+    forall master. YesodChat master =>
+    HandlerT Chat (HandlerT master IO) a
+

Now that we have our machinery out of the way, it’s time to write our subsite +handler functions. We had two routes: one for sending messages, and one for +receiving messages. Let’s start with sending. We need to:

+
    +
  1. +

    +Get the username for the person sending the message. +

    +
  2. +
  3. +

    +Parse the message from the incoming parameters. (Note that we’re going to use GET parameters for simplicity of the client-side Ajax code.) +

    +
  4. +
  5. +

    +Write the message to the Chan. +

    +
  6. +
+

The trickiest bit of all this code is to know when to use lift. Let’s look at +the implementation, and then discuss those lift usages:

+
-- @Chat/Data.hs
+postSendR :: ChatHandler ()
+postSendR = do
+    from <- lift getUserName
+    body <- lift $ runInputGet $ ireq textField "message"
+    Chat chan <- getYesod
+    liftIO $ writeChan chan $ ServerEvent Nothing Nothing $ return $
+        fromText from <> fromText ": " <> fromText body
+

getUserName is the function we defined in our YesodChat typeclass earlier. +If we look at that type signature, we see that it lives in the master site’s +Handler monad. Therefore, we need to lift that call out of the subsite.

+

The call to runInputGet is a little more subtle. Theoretically, we could run +this in either the subsite or the master site. However, we use lift here as +well for one specific reason: message translations. By using the master site, +we can take advantage of whatever RenderMessage instance the master site +defines. This also explains why we have a RenderMessage constraint on the +YesodChat typeclass.

+

The next call to getYesod is not lifted. The reasoning here is simple: +we want to get the subsite’s foundation type in order to access the message +channel. If we instead lifted that call, we’d get the master site’s +foundation type instead, which is not what we want in this case.

+

The final line puts the new message into the channel. Since this is an IO +action, we use liftIO. ServerEvent is part of the wai-eventsource +package, and is the means by which we’re providing server-sent events in this +example.

+

The receiving side is similarly simple:

+
-- @Chat/Data.hs
+getReceiveR :: ChatHandler ()
+getReceiveR = do
+    Chat chan0 <- getYesod
+    chan <- liftIO $ dupChan chan0
+    sendWaiApplication $ eventSourceAppChan chan
+

We use dupChan so that each new connection receives its own copy of newly +generated messages. This is a standard method in concurrent Haskell of creating +broadcast channels. The last line in our function exposes the underlying +wai-eventsource application as a Yesod handler, using the +sendWaiApplication function to promote a WAI application to a Yesod handler.

+

Now that we’ve defined our handler functions, we can set up our dispatch. In a +normal application, dispatching is handled by calling mkYesod, which creates +the appropriate YesodDispatch instance. In subsites, things are a little bit +more complicated, since you’ll often want to place constraints on the master +site. The formula we use is the following:

+
-- @Chat.hs
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+module Chat where
+
+import           Chat.Data
+import           Yesod
+
+instance YesodChat master => YesodSubDispatch Chat (HandlerT master IO) where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesChat)
+

We’re stating that our Chat subsite can live on top of any master site which +is an instance of YesodChat. We then use the mkYesodSubDispatch Template +Haskell function to generate all of our dispatching logic. While this is a bit +more difficult to write than mkYesod, it provides necessary flexibility, and +is mostly identical for any subsite you’ll write.

+
+
+

Subsite: widget

+

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.

+

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 +URLs. Let’s start off our widget:

+
-- @Chat.hs
+chatWidget :: YesodChat master
+           => (Route Chat -> Route master)
+           -> WidgetT master IO ()
+chatWidget toMaster = do
+

Next, we’re going to generate some identifiers to be used by our widget. It’s +always good practice to let Yesod generate unique identifiers for you instead +of creating them manually to avoid name collisions.

+
-- @Chat.hs
+    chat <- newIdent   -- the containing div
+    output <- newIdent -- the box containing the messages
+    input <- newIdent  -- input field from the user
+

And next we need to check if the user is logged in, using the isLoggedIn +function in our YesodChat typeclass. Since we’re in a Widget and that +function lives in the Handler monad, we need to use handlerToWidget:

+
-- @Chat.hs
+    ili <- handlerToWidget isLoggedIn  -- check if we're already logged in
+

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 +client-side code wrapped in a Widget:

+
-- @Chat.hs
+    if ili
+        then do
+            -- Logged in: show the widget
+            [whamlet|
+                <div ##{chat}>
+                    <h2>Chat
+                    <div ##{output}>
+                    <input ##{input} type=text placeholder="Enter Message">
+            |]
+            -- Just some CSS
+            toWidget [lucius|
+                ##{chat} {
+                    position: absolute;
+                    top: 2em;
+                    right: 2em;
+                }
+                ##{output} {
+                    width: 200px;
+                    height: 300px;
+                    border: 1px solid #999;
+                    overflow: auto;
+                }
+            |]
+            -- And now that Javascript
+            toWidgetBody [julius|
+                // Set up the receiving end
+                var output = document.getElementById(#{toJSON output});
+                var src = new EventSource("@{toMaster ReceiveR}");
+                src.onmessage = function(msg) {
+                    // This function will be called for each new message.
+                    var p = document.createElement("p");
+                    p.appendChild(document.createTextNode(msg.data));
+                    output.appendChild(p);
+
+                    // And now scroll down within the output div so the most recent message
+                    // is displayed.
+                    output.scrollTop = output.scrollHeight;
+                };
+
+                // Set up the sending end: send a message via Ajax whenever the user hits
+                // enter.
+                var input = document.getElementById(#{toJSON input});
+                input.onkeyup = function(event) {
+                    var keycode = (event.keyCode ? event.keyCode : event.which);
+                    if (keycode == '13') {
+                        var xhr = new XMLHttpRequest();
+                        var val = input.value;
+                        input.value = "";
+                        var params = "?message=" + encodeURI(val);
+                        xhr.open("POST", "@{toMaster SendR}" + params);
+                        xhr.send(null);
+                    }
+                }
+            |]
+

And finally, if the user isn’t logged in, we’ll ask them to log in to use the +chat app.

+
-- @Chat.hs
+        else do
+            -- User isn't logged in, give a not-logged-in message.
+            master <- getYesod
+            [whamlet|
+                <p>
+                    You must be #
+                    $maybe ar <- authRoute master
+                        <a href=@{ar}>logged in
+                    $nothing
+                        logged in
+                    \ to chat.
+            |]
+
+
+

Master site: data

+

Now we can proceed with writing our main application. This application will +include the chat subsite and a wiki. The first thing we need to consider is how +to store the wiki contents. Normally, we’d want to put this in some kind of a +Persistent database. For simplicity, we’ll just use an in-memory +representation. Each Wiki page is indicated by a list of names, and the contents of each page is going to be a piece of Text. So our full foundation datatype is:

+
-- @ChatMain.hs
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE ViewPatterns          #-}
+module ChatMain where
+
+import           Chat
+import           Chat.Data
+import           Control.Concurrent.Chan (newChan)
+import           Data.IORef
+import           Data.Map                (Map)
+import qualified Data.Map                as Map
+import           Data.Text               (Text)
+import qualified Data.Text.Lazy          as TL
+import           Text.Markdown
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+    { getChat     :: Chat
+    , wikiContent :: IORef (Map [Text] Text)
+    }
+

Next we want to set up our routes:

+
-- @ChatMain.hs
+mkYesod "App" [parseRoutes|
+/            HomeR GET      -- the homepage
+/wiki/*Texts WikiR GET POST -- note the multipiece for the wiki hierarchy
+
+/chat        ChatR Chat getChat    -- the chat subsite
+/auth        AuthR Auth getAuth    -- the auth subsite
+|]
+
+
+

Master site: instances

+

We need to make two modifications to the default Yesod instance. Firstly, we +want to provide an implementation of authRoute, so that our chat subsite +widget can provide a proper link to a login page. Secondly, we’ll provide a +override to the defaultLayout. Besides providing login/logout links, this +function will add in the chat widget on every page.

+
-- @ChatMain.hs
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR -- get a working login link
+
+    -- Our custom defaultLayout will add the chat widget to every page.
+    -- We'll also add login and logout links to the top.
+    defaultLayout widget = do
+        pc <- widgetToPageContent $ do
+            widget
+            chatWidget ChatR
+        mmsg <- getMessage
+        withUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <div .message>#{msg}
+                        <nav>
+                            <a href=@{AuthR LoginR}>Login
+                            \ | #
+                            <a href=@{AuthR LogoutR}>Logout
+                        ^{pageBody pc}
+            |]
+

Since we’re using the chat subsite, we have to provide an instance of +YesodChat.

+
-- @ChatMain.hs
+instance YesodChat App where
+    getUserName = do
+        muid <- maybeAuthId
+        case muid of
+            Nothing -> do
+                setMessage "Not logged in"
+                redirect $ AuthR LoginR
+            Just uid -> return uid
+    isLoggedIn = do
+        ma <- maybeAuthId
+        return $ maybe False (const True) ma
+

Our YesodAuth and RenderMessage instances, as well as the homepage handler, +are rather bland:

+
-- @ChatMain.hs
+-- Fairly standard YesodAuth instance. We'll use the dummy plugin so that you
+-- can create any name you want, and store the login name as the AuthId.
+instance YesodAuth App where
+    type AuthId App = Text
+    authPlugins _ = [authDummy]
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authHttpManager = error "authHttpManager" -- not used by authDummy
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Nothing special here, just giving a link to the root of the wiki.
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Welcome to the Wiki!
+        <p>
+            <a href=@{wikiRoot}>Wiki root
+    |]
+  where
+    wikiRoot = WikiR []
+
+
+

Master site: wiki handlers

+

Now it’s time to write our wiki handlers: a GET for displaying a page, and a +POST for updating a page. We’ll also define a wikiForm function to be used on +both handlers:

+
-- @ChatMain.hs
+-- A form for getting wiki content
+wikiForm :: Maybe Textarea -> Html -> MForm Handler (FormResult Textarea, Widget)
+wikiForm mtext = renderDivs $ areq textareaField "Page body" mtext
+
+-- Show a wiki page and an edit form
+getWikiR :: [Text] -> Handler Html
+getWikiR page = do
+    -- Get the reference to the contents map
+    icontent <- fmap wikiContent getYesod
+
+    -- And read the map from inside the reference
+    content <- liftIO $ readIORef icontent
+
+    -- Lookup the contents of the current page, if available
+    let mtext = Map.lookup page content
+
+    -- Generate a form with the current contents as the default value.
+    -- Note that we use the Textarea wrapper to get a <textarea>.
+    (form, _) <- generateFormPost $ wikiForm $ fmap Textarea mtext
+    defaultLayout $ do
+        case mtext of
+            -- We're treating the input as markdown. The markdown package
+            -- automatically handles XSS protection for us.
+            Just text -> toWidget $ markdown def $ TL.fromStrict text
+            Nothing -> [whamlet|<p>Page does not yet exist|]
+        [whamlet|
+            <h2>Edit page
+            <form method=post>
+                ^{form}
+                <div>
+                    <input type=submit>
+        |]
+
+-- Get a submitted wiki page and updated the contents.
+postWikiR :: [Text] -> Handler Html
+postWikiR page = do
+    icontent <- fmap wikiContent getYesod
+    content <- liftIO $ readIORef icontent
+    let mtext = Map.lookup page content
+    ((res, form), _) <- runFormPost $ wikiForm $ fmap Textarea mtext
+    case res of
+        FormSuccess (Textarea t) -> do
+            liftIO $ atomicModifyIORef icontent $
+                \m -> (Map.insert page t m, ())
+            setMessage "Page updated"
+            redirect $ WikiR page
+        _ -> defaultLayout
+                [whamlet|
+                    <form method=post>
+                        ^{form}
+                        <div>
+                            <input type=submit>
+                |]
+
+
+

Master site: running

+

Finally, we’re ready to run our application. Unlike many of our previous +examples in this book, we need to perform some real initialization in the +main function. The Chat subsite requires an empty Chan to be created, and +we need to create a mutable variable to hold the wiki contents. Once we have +those values, we can create an App value and pass it to the warp function.

+
-- @ChatMain.hs
+main :: IO ()
+main = do
+    -- Create our server event channel
+    chan <- newChan
+
+    -- Initially have a blank database of wiki pages
+    icontent <- newIORef Map.empty
+
+    -- Run our app
+    warpEnv App
+        { getChat = Chat chan
+        , wikiContent = icontent
+        }
+
+
+

Conclusion

+

This example demonstrated creation of a non-trivial subsite. Some important +points to notice were the usage of typeclasses to express constraints on the +master site, how data initialization was performed in the main function, and +how lifting allowed us to operate in either the subsite or master site +context.

+

If you’re looking for a way to test out your subsite skills, I’d recommend +modifying this example so that the Wiki code also lived in its own subsite.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/xml.html b/public/book-1.4/xml.html new file mode 100644 index 00000000..01546157 --- /dev/null +++ b/public/book-1.4/xml.html @@ -0,0 +1,834 @@ + xml-conduit :: Yesod Web Framework Book- Version 1.4 +
+

xml-conduit

+ + +

Many developers cringe at the thought of dealing with XML files. XML has the +reputation of having a complicated data model, with obfuscated libraries and +huge layers of complexity sitting between you and your goal. I’d like to posit +that a lot of that pain is actually a language and library issue, not inherent +to XML.

+

Once again, Haskell’s type system allows us to easily break down the problem to +its most basic form. The xml-types package neatly deconstructs the XML data +model (both a streaming and DOM-based approach) into some simple ADTs. +Haskell’s standard immutable data structures make it easier to apply transforms +to documents, and a simple set of functions makes parsing and rendering a +breeze.

+

We’re going to be covering the xml-conduit package. Under the surface, this +package uses a lot of the approaches Yesod in general does for high +performance: blaze-builder, text, conduit and attoparsec. But from a user +perspective, it provides everything from the simplest APIs +(readFile/writeFile) through full control of XML event streams.

+

In addition to xml-conduit, there are a few related packages that come into +play, like xml-hamlet and xml2html. We’ll cover both how to use all these +packages, and when they should be used.

+
+

Synopsis

+
<!-- Input XML file -->
+<document title="My Title">
+    <para>This is a paragraph. It has <em>emphasized</em> and <strong>strong</strong> words.</para>
+    <image href="myimage.png"/>
+</document>
+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import qualified Data.Map        as M
+import           Prelude         hiding (readFile, writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = do
+    -- readFile will throw any parse errors as runtime exceptions
+    -- def uses the default settings
+    Document prologue root epilogue <- readFile def "input.xml"
+
+    -- root is the root element of the document, let's modify it
+    let root' = transform root
+
+    -- And now we write out. Let's indent our output
+    writeFile def
+        { rsPretty = True
+        } "output.html" $ Document prologue root' epilogue
+
+-- We'll turn out <document> into an XHTML document
+transform :: Element -> Element
+transform (Element _name attrs children) = Element "html" M.empty
+    [xml|
+        <head>
+            <title>
+                $maybe title <- M.lookup "title" attrs
+                    \#{title}
+                $nothing
+                    Untitled Document
+        <body>
+            $forall child <- children
+                ^{goNode child}
+    |]
+
+goNode :: Node -> [Node]
+goNode (NodeElement e) = [NodeElement $ goElem e]
+goNode (NodeContent t) = [NodeContent t]
+goNode (NodeComment _) = [] -- hide comments
+goNode (NodeInstruction _) = [] -- and hide processing instructions too
+
+-- convert each source element to its XHTML equivalent
+goElem :: Element -> Element
+goElem (Element "para" attrs children) =
+    Element "p" attrs $ concatMap goNode children
+goElem (Element "em" attrs children) =
+    Element "i" attrs $ concatMap goNode children
+goElem (Element "strong" attrs children) =
+    Element "b" attrs $ concatMap goNode children
+goElem (Element "image" attrs _children) =
+    Element "img" (fixAttr attrs) [] -- images can't have children
+  where
+    fixAttr mattrs
+        | "href" `M.member` mattrs  = M.delete "href" $ M.insert "src" (mattrs M.! "href") mattrs
+        | otherwise                 = mattrs
+goElem (Element name attrs children) =
+    -- don't know what to do, just pass it through...
+    Element name attrs $ concatMap goNode children
+
<?xml version="1.0" encoding="UTF-8"?>
+<!-- Output XHTML -->
+<html>
+    <head>
+        <title>
+            My Title
+        </title>
+    </head>
+    <body>
+        <p>
+            This is a paragraph. It has
+            <i>
+                emphasized
+            </i>
+            and
+            <b>
+                strong
+            </b>
+            words.
+        </p>
+        <img src="myimage.png"/>
+    </body>
+</html>
+
+
+

Types

+

Let’s take a bottom-up approach to analyzing types. This section will also +serve as a primer on the XML data model itself, so don’t worry if you’re not +completely familiar with it.

+

I think the first place where Haskell really shows its strength is with the +Name datatype. Many languages (like Java) struggle with properly expressing +names. The issue is that there are in fact three components to a name: its +local name, its namespace (optional), and its prefix (also optional). Let’s +look at some XML to explain:

+
<no-namespace/>
+<no-prefix xmlns="first-namespace" first-attr="value1"/>
+<foo:with-prefix xmlns:foo="second-namespace" foo:second-attr="value2"/>
+

The first tag has a local name of no-namespace, and no namespace or prefix. +The second tag (local name: no-prefix) also has no prefix, but it does have +a namespace (first-namespace). first-attr, however, does not inherit that +namespace: attribute namespaces must always be explicitly set with a prefix.

+ +

The third tag has a local name of with-prefix, a prefix of foo and a +namespace of second-namespace. Its attribute has a second-attr local name +and the same prefix and namespace. The xmlns and xmlns:foo attributes are +part of the namespace specification, and are not considered attributes of their +respective elements.

+

So let’s review what we need from a name: every name has a local name, and it +can optionally have a prefix and namespace. Seems like a simple fit for a +record type:

+
data Name = Name
+    { nameLocalName :: Text
+    , nameNamespace :: Maybe Text
+    , namePrefix    :: Maybe Text
+    }
+

According the XML namespace standard, two names are considered equivalent +if they have the same localname and namespace. In other words, the prefix is +not important. Therefore, xml-types defines Eq and Ord instances that +ignore the prefix.

+

The last class instance worth mentioning is IsString. It would be very +tedious to have to manually type out Name "p" Nothing Nothing every time we +want a paragraph. If you turn on OverloadedStrings, "p" will resolve to +that all by itself! In addition, the IsString instance recognizes something +called Clark notation, which allows you to prefix the namespace surrounded in +curly brackets. In other words:

+
"{namespace}element" == Name "element" (Just "namespace") Nothing
+"element" == Name "element" Nothing Nothing
+
+

The Four Types of Nodes

+

XML documents are a tree of nested nodes. There are in fact four different +types of nodes allowed: elements, content (i.e., text), comments, and +processing instructions.

+ +

Since processing instructions have two pieces of text associated with them (the +target and the data), we have a simple data type:

+
data Instruction = Instruction
+    { instructionTarget :: Text
+    , instructionData :: Text
+    }
+

Comments have no special datatype, since they are just text. But content is an +interesting one: it could contain either plain text or unresolved entities +(e.g., &copyright-statement;). xml-types keeps those unresolved entities +in all the data types in order to completely match the spec. However, in +practice, it can be very tedious to program against those data types. And in +most use cases, an unresolved entity is going to end up as an error anyway.

+

Therefore, the Text.XML module defines its own set of datatypes for nodes, +elements and documents that removes all unresolved entities. If you need to +deal with unresolved entities instead, you should use the Text.XML.Unresolved +module. From now on, we’ll be focusing only on the Text.XML data types, +though they are almost identical to the xml-types versions.

+

Anyway, after that detour: content is just a piece of text, and therefore it +too does not have a special datatype. The last node type is an element, which +contains three pieces of information: a name, a map of attribute name/value +pairs, and a list of children nodes. (In xml-types, this value could contain +unresolved entities as well.) So our Element is defined as:

+
data Element = Element
+    { elementName :: Name
+    , elementAttributes :: Map Name Text
+    , elementNodes :: [Node]
+    }
+

Which of course begs the question: what does a Node look like? This is where +Haskell really shines: its sum types model the XML data model perfectly.

+
data Node
+    = NodeElement Element
+    | NodeInstruction Instruction
+    | NodeContent Text
+    | NodeComment Text
+
+
+

Documents

+

So now we have elements and nodes, but what about an entire document? Let’s +just lay out the datatypes:

+
data Document = Document
+    { documentPrologue :: Prologue
+    , documentRoot :: Element
+    , documentEpilogue :: [Miscellaneous]
+    }
+
+data Prologue = Prologue
+    { prologueBefore :: [Miscellaneous]
+    , prologueDoctype :: Maybe Doctype
+    , prologueAfter :: [Miscellaneous]
+    }
+
+data Miscellaneous
+    = MiscInstruction Instruction
+    | MiscComment Text
+
+data Doctype = Doctype
+    { doctypeName :: Text
+    , doctypeID :: Maybe ExternalID
+    }
+
+data ExternalID
+    = SystemID Text
+    | PublicID Text Text
+

The XML spec says that a document has a single root element (documentRoot). +It also has an optional doctype statement. Before and after both the doctype +and the root element, you are allowed to have comments and processing +instructions. (You can also have whitespace, but that is ignored in the +parsing.)

+

So what’s up with the doctype? Well, it specifies the root element of the +document, and then optional public and system identifiers. These are used to +refer to DTD files, which give more information about the file (e.g., +validation rules, default attributes, entity resolution). Let’s see some +examples:

+
<!DOCTYPE root> <!-- no external identifier -->
+<!DOCTYPE root SYSTEM "root.dtd"> <!-- a system identifier -->
+<!DOCTYPE root PUBLIC "My Root Public Identifier" "root.dtd"> <!-- public identifiers have a system ID as well -->
+

And that, my friends, is the entire XML data model. For many parsing purposes, +you’ll be able to simply ignore the entire Document datatype and go +immediately to the documentRoot.

+
+
+

Events

+

In addition to the document API, xml-types defines an Event datatype. This +can be used for constructing streaming tools, which can be much more memory +efficient for certain kinds of processing (eg, adding an extra attribute to all +elements). We will not be covering the streaming API currently, though it +should look very familiar after analyzing the document API.

+ +
+
+
+

Text.XML

+

The recommended entry point to xml-conduit is the Text.XML module. This module +exports all of the datatypes you’ll need to manipulate XML in a DOM fashion, as +well as a number of different approaches for parsing and rendering XML content. +Let’s start with the simple ones:

+
readFile  :: ParseSettings  -> FilePath -> IO Document
+writeFile :: RenderSettings -> FilePath -> Document -> IO ()
+

This introduces the ParseSettings and RenderSettings datatypes. You can use +these to modify the behavior of the parser and renderer, such as adding +character entities and turning on pretty (i.e., indented) output. Both these +types are instances of the Default typeclass, so you can simply use def when +these need to be supplied. That is how we will supply these values through the +rest of the chapter; please see the API docs for more information.

+

It’s worth pointing out that in addition to the file-based API, there is also a +text- and bytestring-based API. The bytestring-powered functions all perform +intelligent encoding detections, and support UTF-8, UTF-16 and UTF-32, in +either big or little endian, with and without a Byte-Order Marker (BOM). All +output is generated in UTF-8.

+

For complex data lookups, we recommend using the higher-level cursors API. The +standard Text.XML API not only forms the basis for that higher level, but is +also a great API for simple XML transformations and for XML generation. See the +synopsis for an example.

+
+

A note about file paths

+

In the type signature above, we have a type FilePath. However, this isn’t +Prelude.FilePath. The standard Prelude defines a type synonym type FilePath += [Char]. Unfortunately, there are many limitations to using such an +approach, including confusion of filename character encodings and differences +in path separators.

+

Instead, xml-conduit uses the system-filepath package, which defines an +abstract FilePath type. I’ve personally found this to be a much nicer +approach to work with. The package is fairly easy to follow, so I won’t go into +details here. But I do want to give a few quick explanations of how to use it:

+
    +
  • +

    +Since a FilePath is an instance of IsString, you can type in regular + strings and they will be treated properly, as long as the OverloadedStrings + extension is enabled. (I highly recommend enabling it anyway, as it makes + dealing with Text values much more pleasant.) +

    +
  • +
  • +

    +If you need to explicitly convert to or from Prelude's FilePath, you + should use the encodeString and decodeString, respectively. This takes into + account file path encodings. +

    +
  • +
  • +

    +Instead of manually splicing together directory names and file names with + extensions, use the operators in the Filesystem.Path.CurrentOS module, e.g. + myfolder </> filename <.> extension. +

    +
  • +
+
+
+
+

Cursor

+

Suppose you want to pull the title out of an XHTML document. You could do so +with the Text.XML interface we just described, using standard pattern +matching on the children of elements. But that would get very tedious, very +quickly. Probably the gold standard for these kinds of lookups is XPath, where +you would be able to write /html/head/title. And that’s exactly what inspired +the design of the Text.XML.Cursor combinators.

+

A cursor is an XML node that knows its location in the tree; it’s able to +traverse upwards, sideways, and downwards. (Under the surface, this is achieved +by tying the knot.) +There are two functions available for creating cursors from Text.XML types: +fromDocument and fromNode.

+

We also have the concept of an Axis, defined as type Axis = Cursor -> +[Cursor]. It’s easiest to get started by looking at example axes: child +returns zero or more cursors that are the child of the current one, parent +returns the single parent cursor of the input, or an empty list if the input is +the root element, and so on.

+

In addition, there are some axes that take predicates. element is a commonly +used function that filters down to only elements which match the given name. +For example, element "title" will return the input element if its name is +"title", or an empty list otherwise.

+

Another common function which isn’t quite an axis is content :: Cursor +-> [Text]. For all content nodes, it returns the contained text; +otherwise, it returns an empty list.

+

And thanks to the monad instance for lists, it’s easy to string all of these +together. For example, to do our title lookup, we would write the following +program:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+            child cursor >>= element "head" >>= child
+                         >>= element "title" >>= descendant >>= content
+

What this says is:

+
    +
  1. +

    +Get me all the child nodes of the root element +

    +
  2. +
  3. +

    +Filter down to only the elements named "head" +

    +
  4. +
  5. +

    +Get all the children of all those head elements +

    +
  6. +
  7. +

    +Filter down to only the elements named "title" +

    +
  8. +
  9. +

    +Get all the descendants of all those title elements. (A descendant is a + child, or a descendant of a child. Yes, that was a recursive definition.) +

    +
  10. +
  11. +

    +Get only the text nodes. +

    +
  12. +
+

So for the input document:

+
<html>
+    <head>
+        <title>My <b>Title</b></title>
+    </head>
+    <body>
+        <p>Foo bar baz</p>
+    </body>
+</html>
+

We end up with the output My Title. This is all well and good, but it’s much +more verbose than the XPath solution. To combat this verbosity, Aristid +Breitkreuz added a set of operators to the Cursor module to handle many common +cases. So we can rewrite our example as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $/ element "head" &/ element "title" &// content
+

$/ says to apply the axis on the right to the children of the cursor on the +left. &/ is almost identical, but is instead used to combine two axes +together. This is a general rule in Text.XML.Cursor: operators beginning with +$ directly apply an axis, while & will combine two together. &// is +used for applying an axis to all descendants.

+

Let’s go for a more complex, if more contrived, example. We have a document +that looks like:

+
<html>
+    <head>
+        <title>Headings</title>
+    </head>
+    <body>
+        <hgroup>
+            <h1>Heading 1 foo</h1>
+            <h2 class="foo">Heading 2 foo</h2>
+        </hgroup>
+        <hgroup>
+            <h1>Heading 1 bar</h1>
+            <h2 class="bar">Heading 2 bar</h2>
+        </hgroup>
+    </body>
+</html>
+

We want to get the content of all the h1 tags which precede an h2 tag with +a class attribute of "bar". To perform this convoluted lookup, we can write:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test2.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $// element "h2"
+               >=> attributeIs "class" "bar"
+               >=> precedingSibling
+               >=> element "h1"
+               &// content
+

Let’s step through that. First we get all h2 elements in the document. ($// +gets all descendants of the root element.) Then we filter out only those with +class=bar. That >=> operator is actually the standard operator from +Control.Monad; yet another advantage of the monad instance of lists. +precedingSibling finds all nodes that come before our node and share the +same parent. (There is also a preceding axis which takes all elements earlier +in the tree.) We then take just the h1 elements, and then grab their content.

+ +

While the cursor API isn’t quite as succinct as XPath, it has the advantages of +being standard Haskell code, and of type safety.

+
+
+

xml-hamlet

+

Thanks to the simplicity of Haskell’s data type system, creating XML content +with the Text.XML API is easy, if a bit verbose. The following code:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Data.Map (empty)
+import           Prelude  hiding (writeFile)
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty
+        [ NodeElement $ Element "head" empty
+            [ NodeElement $ Element "title" empty
+                [ NodeContent "My "
+                , NodeElement $ Element "b" empty
+                    [ NodeContent "Title"
+                    ]
+                ]
+            ]
+        , NodeElement $ Element "body" empty
+            [ NodeElement $ Element "p" empty
+                [ NodeContent "foo bar baz"
+                ]
+            ]
+        ]
+

produces

+
<?xml version="1.0" encoding="UTF-8"?>
+<html><head><title>My <b>Title</b></title></head><body><p>foo bar baz</p></body></html>
+

This is leaps and bounds easier than having to deal with an imperative, +mutable-value-based API (cough, Java, cough), but it’s far from pleasant, and +obscures what we’re really trying to achieve. To simplify things, we have the +xml-hamlet package, which using Quasi-Quotation to allow you to type in your +XML in a natural syntax. For example, the above could be rewritten as:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map        (empty)
+import           Prelude         hiding (writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>
+        My #
+        <b>Title
+<body>
+    <p>foo bar baz
+|]
+

Let’s make a few points:

+
    +
  • +

    +The syntax is almost identical to normal Hamlet, except URL-interpolation + (@{…}) has been removed. As such: +

    +
      +
    • +

      +No close tags. +

      +
    • +
    • +

      +Whitespace-sensitive. +

      +
    • +
    • +

      +If you want to have whitespace at the end of a line, use a # at the end. At + the beginning, use a backslash. +

      +
    • +
    +
  • +
  • +

    +An xml interpolation will return a list of Nodes. So you still need to + wrap up the output in all the normal Document and root Element + constructs. +

    +
  • +
  • +

    +There is no support for the special .class and #id attribute forms. +

    +
  • +
+

And like normal Hamlet, you can use variable interpolation and control +structures. So a slightly more complex example would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+import Data.Text (Text, pack)
+import Data.Map (empty)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Michael" 26
+    , Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Gavriella" 1
+    ]
+
+main :: IO ()
+main =
+    writeFile def "people.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>Some People
+<body>
+    <h1>Some People
+    $if null people
+        <p>There are no people.
+    $else
+        <dl>
+            $forall person <- people
+                ^{personNodes person}
+|]
+
+personNodes :: Person -> [Node]
+personNodes person = [xml|
+<dt>#{personName person}
+<dd>#{pack $ show $ personAge person}
+|]
+

A few more notes:

+
    +
  • +

    +The caret-interpolation (^{…}) takes a list of nodes, and so can easily + embed other xml-quotations. +

    +
  • +
  • +

    +Unlike Hamlet, hash-interpolations (#{…}) are not polymorphic, and can + only accept Text values. +

    +
  • +
+
+
+

xml2html

+

So far in this chapter, our examples have revolved around XHTML. I’ve done that +so far simply because it is likely to be the most familiar form of XML for most +of our readers. But there’s an ugly side to all this that we must acknowledge: +not all XHTML will be correct HTML. The following discrepancies exist:

+
    +
  • +

    +There are some void tags (e.g., img, br) in HTML which do not need to + have close tags, and in fact are not allowed to. +

    +
  • +
  • +

    +HTML does not understand self-closing tags, so + <script></script> and <script/> mean very different + things. +

    +
  • +
  • +

    +Combining the previous two points: you are free to self-close void tags, + though to a browser it won’t mean anything. +

    +
  • +
  • +

    +In order to avoid quirks mode, you should start your HTML documents with a + DOCTYPE statement. +

    +
  • +
  • +

    +We do not want the XML declaration <?xml …?> at the top of an HTML + page. +

    +
  • +
  • +

    +We do not want any namespaces used in HTML, while XHTML is fully namespaced. +

    +
  • +
  • +

    +The contents of <style> and <script> tags should not be + escaped. +

    +
  • +
+

Fortunately, xml-conduit provides ToHtml instances for Nodes, +Documents, and Elements which respect these discrepancies. So by just +using toHtml, we can get the correct output.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map                        (empty)
+import           Text.Blaze.Html                 (toHtml)
+import           Text.Blaze.Html.Renderer.String (renderHtml)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = putStr $ renderHtml $ toHtml $ Document (Prologue [] Nothing []) root []
+
+root :: Element
+root = Element "html" empty [xml|
+<head>
+    <title>Test
+    <script>if (5 < 6 || 8 > 9) alert("Hello World!");
+    <style>body > h1 { color: red }
+<body>
+    <h1>Hello World!
+|]
+

Outputs: (whitespace added)

+
<!DOCTYPE HTML>
+<html>
+    <head>
+        <title>Test</title>
+        <script>if (5 < 6 || 8 > 9) alert("Hello World!");</script>
+        <style>body > h1 { color: red }</style>
+    </head>
+    <body>
+        <h1>Hello World!</h1>
+    </body>
+</html>
+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/yesod-for-haskellers.html b/public/book-1.4/yesod-for-haskellers.html new file mode 100644 index 00000000..91e7d756 --- /dev/null +++ b/public/book-1.4/yesod-for-haskellers.html @@ -0,0 +1,1236 @@ + Yesod for Haskellers :: Yesod Web Framework Book- Version 1.4 +
+

Yesod for Haskellers

+ + +

The majority of this book is built around giving practical information on how +to get common tasks done, without drilling too much into the details of what’s +going on under the surface. While the book presumes knowledge of Haskell, it +does not follow the typical style of many Haskell libraries introductions. Many +seasoned Haskellers are put off by this hiding of implementation details. The +purpose of this chapter is to address those concerns.

+

In this chapter, we’ll start off from a bare minimum web application, and +build up to more complicated examples, explaining the components and their +types along the way.

+
+

Hello Warp

+

Let’s start off with the most bare minimum application we can think of:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseLBS
+    status200
+    [("Content-Type", "text/plain")]
+    "Hello Warp!"
+

Wait a minute, there’s no Yesod in there! Don’t worry, we’ll get there. +Remember, we’re building from the ground up, and in Yesod, the ground floor is +WAI, the Web Application Interface. WAI sits between a web handler, such as a +web server or a test framework, and a web application. In our case, the +handler is Warp, a high performance web server, and our application is the +app function.

+

What’s this mysterious Application type? It’s a type synonym defined as:

+
type Application = Request
+                -> (Response -> IO ResponseReceived)
+                -> IO ResponseReceived
+

The Request value contains information such as the requested path, query +string, request headers, request body, and the IP address of the client. The +second argument is the “send response” function. Instead of simply having the +application return an IO Response, WAI uses continuation passing style to +allow for full exception safety, similar to how the bracket function works.

+

We can use this to do some simple dispatching:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, pathInfo, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req sendResponse =
+    case pathInfo req of
+        ["foo", "bar"] -> sendResponse $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested /foo/bar"
+        _ -> sendResponse $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested something else"
+

WAI mandates that the path be split into individual fragments (the stuff +between forward slashes) and converted into text. This allows for easy pattern +matching. If you need the original, unmodified ByteString, you can use +rawPathInfo. For more information on the available fields, please see the WAI +Haddocks.

+

That addresses the request side; what about responses? We’ve already seen +responseLBS, which is a convenient way of creating a response from a lazy +ByteString. That function takes three arguments: the status code, a list of +response headers (as key/value pairs), and the body itself. But responseLBS +is just a convenience wrapper. Under the surface, WAI uses blaze-builder’s +Builder data type to represent the raw bytes. Let’s dig down another level +and use that directly:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (Builder, fromByteString)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseBuilder)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseBuilder
+    status200
+    [("Content-Type", "text/plain")]
+    (fromByteString "Hello from blaze-builder!" :: Builder)
+

This opens up some nice opportunities for efficiently building up response +bodies, since Builder allows for O(1) append operations. We’re also able to +take advantage of blaze-html, which sits on top of blaze-builder. Let’s see our +first HTML application.

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (Application, responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import           Text.Blaze.Html5              (Html, docTypeHtml)
+import qualified Text.Blaze.Html5              as H
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseBuilder
+    status200
+    [("Content-Type", "text/html")] -- yay!
+    (renderHtmlBuilder myPage)
+
+myPage :: Html
+myPage = docTypeHtml $ do
+    H.head $ do
+        H.title "Hello from blaze-html and Warp"
+    H.body $ do
+        H.h1 "Hello from blaze-html and Warp"
+

But there’s a limitation with using a pure Builder value: we need to create +the entire response body before returning the Response value. With lazy +evaluation, that’s not as bad as it sounds, since not all of the body will live in +memory at once. However, if we need to perform some I/O to generate our +response body (such as reading data from a database), we’ll be in trouble.

+

To deal with that situation, WAI provides a means for generating streaming +response bodies. It also allows explicit control of flushing the stream. Let’s +see how this works.

+
{-# LANGUAGE OverloadedStrings #-}
+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)
+import           Network.Wai.Handler.Warp           (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseStream
+    status200
+    [("Content-Type", "text/plain")]
+    myStream
+
+myStream :: (Builder -> IO ()) -> IO () -> IO ()
+myStream send flush = do
+    send $ fromByteString "Starting streaming response.\n"
+    send $ fromByteString "Performing some I/O.\n"
+    flush
+    -- pretend we're performing some I/O
+    threadDelay 1000000
+    send $ fromByteString "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        send $ fromByteString "Got the value: " <>
+               fromShow i <>
+               fromByteString "\n"
+ +

Another common requirement when dealing with a streaming response is safely +allocating a scarce resource- such as a file handle. By safely, I mean +ensuring that the resource will be released, even in the case of some +exception. This is where the continuation passing style mentioned above comes +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)
+import           System.IO                (IOMode (ReadMode), withFile)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = withFile "index.html" ReadMode $ \handle ->
+    sendResponse $ responseStream
+        status200
+        [("Content-Type", "text/html")]
+        $ \send _flush ->
+            let loop = do
+                    bs <- S.hGet handle 4096
+                    if S.null bs
+                        then return ()
+                        else send (fromByteString bs) >> loop
+             in loop
+

Notice how we’re able to take advantage of existing exception safe functions +like withFile to deal with exceptions properly.

+

But in the case of serving files, it’s more efficient to use responseFile, +which can use the sendfile system call to avoid unnecessary buffer copies:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseFile)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseFile
+    status200
+    [("Content-Type", "text/html")]
+    "index.html"
+    Nothing -- means "serve whole file"
+            -- you can also serve specific ranges in the file
+

There are many aspects of WAI we haven’t covered here. One important topic is WAI middlewares. We also haven’t inspected request bodies at all. But for the purposes of understanding Yesod, we’ve covered enough for the moment.

+
+
+

What about Yesod?

+

In all our excitement about WAI and Warp, we still haven’t seen anything about Yesod! Since we just learnt all about WAI, our first question should be: how does Yesod interact with WAI. The answer to that is one very important function:

+
toWaiApp :: YesodDispatch site => site -> IO Application
+ +

This function takes some site value, which must be an instance of +YesodDispatch, and creates an Application. This function lives in the IO +monad, since it will likely perform actions like allocating a shared logging +buffer. The more interesting question is what this site value is all about.

+

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. +

    +
  • +
  • +

    +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 +Yesod application the hard way, by writing everything manually. We’ll +progressively add more convenience helpers on top as we go along.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import qualified Text.Blaze.Html5              as H
+import           Yesod.Core                    (Html, RenderRoute (..), Yesod,
+                                                YesodDispatch (..), toWaiApp)
+import           Yesod.Core.Types              (YesodRunnerEnv (..))
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+instance YesodDispatch App where
+    yesodDispatch (YesodRunnerEnv _logger site _sessionBackend _ _) _req sendResponse =
+        sendResponse $ responseBuilder
+            status200
+            [("Content-Type", "text/html")]
+            (renderHtmlBuilder $ welcomeMessage site)
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

OK, we’ve added quite a few new pieces here, let’s attack them one at a time. +The first thing we’ve done is created a new datatype, App. This is commonly +used as the foundation data type name for each application, though you’re free +to use whatever name you want. We’ve added one field to this datatype, +welcomeMessage, which will hold the content for our homepage.

+

Next we declare our Yesod instance. We just use the default values for all of +the methods for this example. More interesting is the RenderRoute typeclass. +This is the heart of type-safe URLs. We create an associated data type for +App which lists all of our app’s accepted routes. In this case, we have just +one: the homepage, which we call HomeR. It’s yet another Yesod naming +convention to append R to all of the route data constructors.

+

We also need to create a renderRoute method, which converts each type-safe +route value into a tuple of path pieces and query string parameters. We’ll get +to more interesting examples later, but for now, our homepage has an empty list +for both of those.

+

YesodDispatch determines how our application behaves. It has one method, +yesodDispatch, of type:

+
yesodDispatch :: YesodRunnerEnv site -> Application
+

YesodRunnerEnv provides three 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.

+

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 +our implementation of yesodDispatch is able to use everything we learned +about WAI above. Notice also how we accessed the welcomeMessage from our +foundation data type.

+

Finally, we have the main function. The App value is easy to create and, as +you can see, you could just as easily have performed some I/O to acquire the +welcome message. We use toWaiApp to obtain a WAI application, and then pass +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

+

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.

+

There are many common things you’d want to do when handling a single request, +e.g.:

+
    +
  • +

    +Return some HTML. +

    +
  • +
  • +

    +Redirect to a different URL. +

    +
  • +
  • +

    +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:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.Wai              (pathInfo)
+import           Network.Wai.Handler.Warp (run)
+import qualified Text.Blaze.Html5         as H
+import           Yesod.Core               (HandlerT, Html, RenderRoute (..),
+                                           Yesod, YesodDispatch (..), getYesod,
+                                           notFound, toWaiApp, yesodRunner)
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO Html
+getHomeR = do
+    site <- getYesod
+    return $ welcomeMessage site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

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 +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.

+

Our function body is short. We use the getYesod function to get the +foundation data type value, and then return the welcomeMessage field. We’ll +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 Applications. Let’s look at its type signature:

+
yesodRunner :: (ToTypedContent res, Yesod site)
+            => HandlerT site IO res
+            -> YesodRunnerEnv site
+            -> Maybe (Route site)
+            -> Application
+

We’re already familiar with YesodRunnerEnv from our previous example. As you +can see in our call to yesodRunner above, we pass that value in unchanged. +The Maybe (Route site) is a bit interesting, and gives us more insight into +how type-safe URLs work. Until now, we only saw the rendering side of these +URLs. But just as important is the parsing side: converting a requested path +into a route value. In our example, this code is just a few lines, and we store +the result in maybeRoute.

+ +

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 +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 +template. That template can be overridden in the Yesod typeclass; out of the +box, it’s just a boring HTML page.

+

This almost all makes sense, except for one issue: what’s that ToTypedContent +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 +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, +and sending the response body.

+
+

(To)Content, (To)TypedContent

+

At the very core of Yesod’s content system are the following types:

+
data Content = ContentBuilder !Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+
+type ContentType = ByteString
+data TypedContent = TypedContent !ContentType !Content
+

Content should remind you a bit of the WAI response types. ContentBuilder +is similar to responseBuilder, ContentSource is like responseStream but specialized to conduit, and +ContentFile is like responseFile. Unlike their WAI counterparts, none of +these constructors contain information on the status code or response headers; +that’s handled orthogonally in Yesod.

+

The one completely new constructor is ContentDontEvaluate. By default, when +you create a response body in Yesod, Yesod fully evaluates the body before +generating the response. The reason for this is to ensure that there are no +impure exceptions in your value. Yesod wants to make sure to catch any such +exceptions before starting to send your response so that, if there is an +exception, Yesod can generate a proper 500 internal server error response +instead of simply dying in the middle of sending a non-error response. However, +performing this evaluation can cause more memory usage. Therefore, Yesod +provides a means of opting out of this protection.

+

TypedContent is then a minor addition to Content: it includes the +ContentType as well. Together with a convention that an application returns a +200 OK status unless otherwise specified, we have everything we need from the +TypedContent type to create a response.

+

Yesod could have taken the approach of requiring users to always return +TypedContent from a handler function, but that would have required manually +converting to that type. Instead, Yesod uses a pair of typeclasses for this, +appropriately named ToContent and ToTypedContent. They have exactly the +definitions you’d expect:

+
class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

And Yesod provides instances for many common data types, including Text, +Html, and aeson’s Value type (containing JSON data). That’s how the +getHomeR function was able to return Html: Yesod knows how to convert it to +TypedContent, and from there it can be converted into a WAI response.

+
+
+

HasContentType and representations

+

This typeclass approach allows for one other nice abstraction. For many types, the type system itself lets us know what the content-type for the content should be. For example, Html will always be served with a text/html content-type.

+ +

Some requests to a web application can be displayed with various representation. For example, a request for tabular data could be served with:

+
    +
  • +

    +An HTML table +

    +
  • +
  • +

    +A CSV file +

    +
  • +
  • +

    +XML +

    +
  • +
  • +

    +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 +selectRep/provideRep function combo to provide multiple representations, +and have the framework automatically choose the appropriate one based on the +client headers.

+

The last missing piece to make this all work is the HasContentType typeclass:

+
class ToTypedContent a => HasContentType a where
+    getContentType :: Monad m => m a -> ContentType
+

The parameter m a is just a poor man’s Proxy type. And, in hindsight, we +should have used Proxy, but that would now be a breaking change. There are +instances for this typeclass for most data types supported by ToTypedContent. +Below is our example from above, tweaked just a bit to provide multiple +representations of the data:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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 (..),
+                                           TypedContent, Value, Yesod,
+                                           YesodDispatch (..), getYesod,
+                                           notFound, object, provideRep,
+                                           selectRep, toWaiApp, yesodRunner,
+                                           (.=))
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessageHtml :: !Html
+    , welcomeMessageText :: !Text
+    , welcomeMessageJson :: !Value
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = do
+    site <- getYesod
+    selectRep $ do
+        provideRep $ return $ welcomeMessageHtml site
+        provideRep $ return $ welcomeMessageText site
+        provideRep $ return $ welcomeMessageJson site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+main = do
+    waiApp <- toWaiApp App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+    run 3000 waiApp
+
+
+

Convenience warp function

+

And one minor convenience you’ll see quite a bit in the Yesod world. It’s very +common to call toWaiApp to create a WAI Application, and then pass that to +Warp’s run function. So Yesod provides a convenience warp wrapper function. +We can replace our previous main function with the following:

+
main :: IO ()
+main =
+    warp 3000 App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+

There’s also a warpEnv function which reads the port number from the PORT +environment variable, which is useful for working with platforms such as FP +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.

+

In addition to that standard functionality, the following are some common +categories of functions. The only requirement Yesod places on your handler +functions is that, ultimately, they return a type which is an instance of +ToTypedContent.

+

This section is just a short overview of functionality. For more information, +you should either look through the Haddocks, or read the rest of this book.

+
+

Getting request parameters

+

There are a few pieces of information provided by the client in a request:

+
    +
  • +

    +The requested path. This is usually handled by Yesod’s routing framework, and is not directly queried in a handler function. +

    +
  • +
  • +

    +Query string parameters. This can be queried using lookupGetParam. +

    +
  • +
  • +

    +Request bodies. In the case of URL encoded and multipart bodies, you can use lookupPostParam to get the request parameter. For multipart bodies, there’s also lookupFile for file parameters. +

    +
  • +
  • +

    +Request headers can be queried via lookupHeader. (And response headers can be set with addHeader.) +

    +
  • +
  • +

    +Yesod parses cookies for you automatically, and they can be queried using lookupCookie. (Cookies can be set via the setCookie function.) +

    +
  • +
  • +

    +Finally, Yesod provides a user session framework, where data can be set in a cryptographically secure session and associated with each user. This can be queried and set using the functions lookupSession, setSession and deleteSession. +

    +
  • +
+

While you can use these functions directly for such purposes as processing +forms, you usually will want to use the yesod-form library, which provides a +higher level form abstraction based on applicative functors.

+
+
+

Short circuiting

+

In some cases, you’ll want to short circuit the handling of a request. Reasons +for doing this would be:

+
    +
  • +

    +Send an HTTP redirect, via the redirect function. This will default to using the 303 status code. You can use redirectWith to get more control over this. +

    +
  • +
  • +

    +Return a 404 not found with notFound, or a 405 bad method via badMethod. +

    +
  • +
  • +

    +Indicate some error in the request via notAuthenticated, permissionDenied, or invalidArgs. +

    +
  • +
  • +

    +Send a special response, such as with sendFile or sendResponseStatus (to override the status 200 response code) +

    +
  • +
  • +

    +sendWaiResponse to drop down a level of abstraction, bypass some Yesod abstractions, and use WAI itself. +

    +
  • +
+
+
+

Streaming

+

So far, the examples of ToTypedContent instances I gave all involved +non-streaming responses. Html, Text, and Value all get converted into a +ContentBuilder constructor. As such, they cannot interleave I/O with sending +data to the user. What happens if we want to perform such interleaving?

+

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 +Builder. Yesod also provides a number of convenience functions for creating +that Source, such as sendChunk, sendChunkBS, and sendChunkText.

+

Here’s an example, which just converts our initial responseSource example +from WAI to Yesod.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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,
+                                                     YesodDispatch (..), liftIO,
+                                                     notFound, respondSource,
+                                                     sendChunk, sendChunkBS,
+                                                     sendChunkText, sendFlush,
+                                                     warp, yesodRunner)
+
+-- | Our foundation datatype.
+data App = App
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = respondSource "text/plain" $ do
+    sendChunkBS "Starting streaming response.\n"
+    sendChunkText "Performing some I/O.\n"
+    sendFlush
+    -- pretend we're performing some I/O
+    liftIO $ threadDelay 1000000
+    sendChunkBS "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        sendChunk $ fromByteString "Got the value: " <>
+                    fromShow i <>
+                    fromByteString "\n"
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+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, +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 +Fibonacci number. And if you visit /, it will automatically redirect you to +/fib/1.

+

In the Yesod world, the first question to ask is: how do we model our route +data type? This is pretty straight-forward: data Route App = HomeR | FibR +Int. The question is: how do we want to define our RenderRoute instance? We +need to convert the Int to a Text. What function should we use?

+

Before you answer that, realize that we’ll also need to be able to parse back a Text into an Int for dispatch purposes. So we need to make sure that we have a pair of functions with the property fromText . toText == Just. Show/Read could be a candidate for this, except that:

+
    +
  1. +

    +We’d be required to convert through String. +

    +
  2. +
  3. +

    +The Show/Read instances for Text and String both involve extra escaping, which we don’t want to incur. +

    +
  4. +
+

Instead, the approach taken by Yesod is the path-pieces package, and in +particular the PathPiece typeclass, defined as:

+
class PathPiece s where
+    fromPathPiece :: Text -> Maybe s
+    toPathPiece   :: s    -> Text
+

Using this typeclass, we can write parse and render functions for our route datatype:

+
instance RenderRoute App where
+    data Route App = HomeR | FibR Int
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ([], [])
+    renderRoute (FibR i) = (["fib", toPathPiece i], [])
+
+parseRoute' [] = Just HomeR
+parseRoute' ["fib", i] = FibR <$> fromPathPiece i
+parseRoute' _ = Nothing
+

And then we can write our YesodDispatch typeclass instance:

+
instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute = parseRoute' (pathInfo req)
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+                    Just (FibR i) -> getFibR i
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR i = return $ show $ fibs !! i
+

Notice our call to redirect in getHomeR. We’re able to use the route +datatype as the parameter to redirect, and Yesod takes advantage of our +renderRoute function to create a textual link.

+
+
+

Routing with Template Haskell

+

Now let’s suppose we want to add a new route to our previous application. We’d +have to make the following changes:

+
    +
  1. +

    +Modify the Route datatype itself. +

    +
  2. +
  3. +

    +Add a clause to renderRoute. +

    +
  4. +
  5. +

    +Add a clause to parseRoute', and make sure it corresponds correctly to renderRoute. +

    +
  6. +
  7. +

    +Add a clause to the case statement in yesodDispatch to call our handler function. +

    +
  8. +
  9. +

    +Write our handler function. +

    +
  10. +
+

That’s a lot of changes! And lots of manual, boilerplate changes means lots of +potential for mistakes. Some of the mistakes can be caught by the compiler if +you turn on warnings (forgetting to add a clause in renderRoute or a match in +yesodDispatch's case statement), but others cannot (ensuring that +renderRoute and parseRoute have the same logic, or adding the parseRoute +clause).

+

This is where Template Haskell comes into the Yesod world. Instead of dealing +with all of these changes manually, Yesod declares a high level routing syntax. +This syntax lets you specify your route syntax, dynamic parameters, constructor +names, and accepted request methods, and automatically generates parse, render, +and dispatch functions.

+

To get an idea of how much manual coding this saves, have a look at our +previous example converted to the Template Haskell version:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Yesod.Core (RenderRoute (..), Yesod, mkYesod, parseRoutes,
+                             redirect, warp)
+
+-- | Our foundation datatype.
+data App = App
+
+instance Yesod App
+
+mkYesod "App" [parseRoutes|
+/         HomeR GET
+/fib/#Int FibR  GET
+|]
+
+getHomeR :: Handler ()
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> Handler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 App
+

What’s wonderful about this is, as the developer, you can now focus on the +important part of your application, and not get involved in the details of +writing parsers and renderers. There are of course some downsides to the usage +of Template Haskell:

+
    +
  • +

    +Compile times are a bit slower. +

    +
  • +
  • +

    +The details of what’s going on behind the scenes aren’t easily apparent. (Though you can use cabal haddock to see what identifiers have been generated for you.) +

    +
  • +
  • +

    +You don’t have as much fine-grained control. For example, in the Yesod route syntax, each dynamic parameter has to be a separate field in the route constructor, as opposed to bundling fields together. This is a conscious trade-off in Yesod between flexibility and complexity. +

    +
  • +
+

This usage of Template Haskell is likely the most controversial decision in +Yesod. I personally think the benefits definitely justify its usage. But if +you’d rather avoid Template Haskell, you’re free to do so. Every example so far +in this chapter has done so, and you can follow those techniques. We also have +another, simpler approach in the Yesod world: LiteApp.

+
+

LiteApp

+

LiteApp allows you to throw away type safe URLs and Template Haskell. It uses +a simple routing DSL in pure Haskell. Once again, as a simple comparison, let’s +rewrite our Fibonacci example to use it.

+
import           Data.Text  (pack)
+import           Yesod.Core (LiteHandler, dispatchTo, dispatchTo, liteApp,
+                             onStatic, redirect, warp, withDynamic)
+
+getHomeR :: LiteHandler ()
+getHomeR = redirect "/fib/1"
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> LiteHandler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "fib") $ withDynamic $ \i -> dispatchTo (getFibR i)
+

There you go, a simple Yesod app without any language extensions at all! +However, even this application still demonstrates some type safety. Yesod will +use fromPathPiece to convert the parameter for getFibR from Text to an +Int, so any invalid parameter will be got by Yesod itself. It’s just one less +piece of checking that you have to perform.

+
+
+
+

Shakespeare

+

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.

+

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.

+
import           Data.Text  (pack)
+import           Yesod.Core
+
+getHomeR :: LiteHandler TypedContent
+getHomeR = return $ TypedContent typeHtml $ toContent
+    "<html><head><title>Hi There!</title>\
+    \<link rel='stylesheet' href='/style.css'>\
+    \<script src='/script.js'></script></head>\
+    \<body><h1>Hello World!</h1></body></html>"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

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 +content as Strings, convert them to Content using toContent, then wrap +them with a TypedContent constructor to give them the appropriate +content-type headers.

+

But as usual, we can do better. Dealing with Strings is not very efficient, +and it’s tedious to have to manually put in the content type all the time. But +we already know the solution to those problems: use the Html datatype from +blaze-html. Let’s convert our getHomeR function to use it:

+
import           Data.Text                   (pack)
+import           Text.Blaze.Html5            (toValue, (!))
+import qualified Text.Blaze.Html5            as H
+import qualified Text.Blaze.Html5.Attributes as A
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = return $ H.docTypeHtml $ do
+    H.head $ do
+        H.title $ toHtml "Hi There!"
+        H.link ! A.rel (toValue "stylesheet") ! A.href (toValue "/style.css")
+        H.script ! A.src (toValue "/script.js") $ return ()
+    H.body $ do
+        H.h1 $ toHtml "Hello World!"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

Ahh, far nicer. blaze-html provides a convenient combinator library, and will +execute far faster in most cases than whatever String concatenation you might +attempt.

+

If you’re happy with blaze-html combinators, by all means use them. However, +many people like to use a more specialized templating language. Yesod’s +standard provider for this is the Shakespearean languages: Hamlet, Lucius, and +Julius. You are by all means welcome to use a different system if so desired, +the only requirement is that you can get a Content value from the template.

+

Since Shakespearean templates are compile-time checked, their usage requires +either quasiquotation or Template Haskell. We’ll go for the former approach +here. Please see the Shakespeare chapter in the book for more information.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Data.Text   (Text, pack)
+import           Text.Julius (Javascript)
+import           Text.Lucius (Css)
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = withUrlRenderer $
+    [hamlet|
+        $doctype 5
+        <html>
+            <head>
+                <title>Hi There!
+                <link rel=stylesheet href=/style.css>
+                <script src=/script.js>
+            <body>
+                <h1>Hello World!
+    |]
+
+getStyleR :: LiteHandler Css
+getStyleR = withUrlRenderer [lucius|h1 { color: red }|]
+
+getScriptR :: LiteHandler Javascript
+getScriptR = withUrlRenderer [julius|alert('Yay, Javascript works too!');|]
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+
+

URL rendering function

+

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 +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.

+

The recommended approach instead would be to use our type-safe URL datatype in +our template instead of including explicit strings. As mentioned above, +LiteApp doesn’t provide any meaningful type-safe URLs, so we don’t have that +option here. But if you use the Template Haskell generators, you get type-safe +URLs for free.

+

In any event, the Shakespearean templates all expect to receive a function to +handle the rendering of a type-safe URL. Since we don’t actually use any +type-safe URLs, just about any function would work here (the function will be +ignored entirely), but withUrlRenderer is a convenient way of doing this.

+

As we’ll see next, withUrlRenderer isn’t really needed most of the time, +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 +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 +using them.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = defaultLayout $ do
+    setTitle $ toHtml "Hi There!"
+    [whamlet|<h1>Hello World!|]
+    toWidget [lucius|h1 { color: red }|]
+    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 +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 +temporary static files and linking to them).

+

Widgets also have another advantage. The defaultLayout function is a member +of the Yesod typeclass, and can be modified to provide a customized +look-and-feel for your website. Many built-in pieces of Yesod, such as error +messages, take advantage of the widget system, so by using widgets, you get a +consistent feel throughout your site.

+
+
+

Details we won’t cover

+

Hopefully this chapter has pulled back enough of the “magic” in Yesod to let +you understand what’s going on under the surface. We could of course continue +using this approach for analyze the rest of the Yesod ecosystem, but that would +be mostly redundant with the rest of this book. Hopefully you can now feel more +informed as you read chapters on Persistent, forms, sessions, and subsites.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/yesod-typeclass.html b/public/book-1.4/yesod-typeclass.html new file mode 100644 index 00000000..b13d6d5e --- /dev/null +++ b/public/book-1.4/yesod-typeclass.html @@ -0,0 +1,674 @@ + Yesod Typeclass :: Yesod Web Framework Book- Version 1.4 +
+

Yesod Typeclass

+ + +

Every one of our Yesod applications requires an instance of the Yesod +typeclass. So far, we’ve just relied on default implementations of these +methods. In this chapter, we’ll explore the meaning of many of the methods of +the Yesod typeclass.

+

The Yesod typeclass gives us a central place for defining settings for our +application. Everything has a default definition which is often the +right thing. But in order to build a powerful, customized application, you’ll +usually end up wanting to override at least a few of these methods.

+ +
+

Rendering and Parsing URLs

+

We’ve already mentioned how Yesod is able to automatically render type-safe +URLs into a textual URL that can be inserted into an HTML page. Let’s say we +have a route definition that looks like:

+
mkYesod "MyApp" [parseRoutes|
+/some/path SomePathR GET
+]
+

If we place SomePathR into a hamlet template, how does Yesod render it? Yesod +always tries to construct absolute URLs. This is especially useful once we +start creating XML sitemaps and Atom feeds, or sending emails. But in order to +construct an absolute URL, we need to know the domain name of the application.

+

You might think we could get that information from the user’s request, but we +still need to deal with ports. And even if we get the port number from the +request, are we using HTTP or HTTPS? And even if you know that, such an +approach would mean that, depending on how the user submitted a request would +generate different URLs. For example, we would generate different URLs +depending if the user connected to "example.com" or "www.example.com". For +Search Engine Optimization, we want to be able to consolidate on a single +canonical URL.

+

And finally, Yesod doesn’t make any assumption about where you host your +application. For example, I may have a mostly static site +(http://static.example.com/), but I’d like to stick a Yesod-powered Wiki at +/wiki/. There is no reliable way for an application to determine what subpath +it is being hosted from. So instead of doing all of this guesswork, Yesod needs +you to tell it the application root.

+

Using the wiki example, you would write your Yesod instance as:

+
instance Yesod MyWiki where
+    approot = ApprootStatic "http://static.example.com/wiki"
+

Notice that there is no trailing slash there. Next, when Yesod wants to +construct a URL for SomePathR, it determines that the relative path for +SomePathR is /some/path, appends that to your approot and creates +http://static.example.com/wiki/some/path.

+

The default value of approot is ApprootRelative, which essentially means +“don’t add any prefix.” In that case, the generated URL would be +/some/path. This works fine for the common case of a link within your +application, and your application being hosted at the root of your domain. But +if you have any use cases which demand absolute URLs (such as sending an +email), it’s best to use ApprootStatic.

+

In addition to the ApprootStatic constructor demonstrated above, you can also +use the ApprootMaster and ApprootRequest constructors. The former allows +you to determine the approot from the foundation value, which would let you +load up the approot from a config file, for instance. The latter allows you to +additionally use the request value to determine the approot; using this, you +could for example provide a different domain name depending on how the user +requested the site in the first place.

+

The scaffolded site uses ApprootMaster by default, and pulls your approot +from either the APPROOT environment variable or a config file on launch. +Additionally, it loads different settings for testing and +production builds, so you can easily test on one domain- like localhost- and +serve from a different domain. You can modify these values from the config +file.

+
+

joinPath

+

In order to convert a type-safe URL into a text value, Yesod uses two helper +functions. The first is the renderRoute method of the RenderRoute +typeclass. Every type-safe URL is an instance of this typeclass. renderRoute +converts a value into a list of path pieces. For example, our SomePathR from +above would be converted into ["some", "path"].

+ +

The other function is the joinPath method of the Yesod typeclass. This function takes four arguments:

+
    +
  • +

    +The foundation value +

    +
  • +
  • +

    +The application root +

    +
  • +
  • +

    +A list of path segments +

    +
  • +
  • +

    +A list of query string parameters +

    +
  • +
+

It returns a textual URL. The default implementation does the “right thing”: +it separates the path pieces by forward slashes, prepends the application root, +and appends the query string.

+

If you are happy with default URL rendering, you should not need to modify it. +However, if you want to modify URL rendering to do things like append a +trailing slash, this would be the place to do it.

+
+
+

cleanPath

+

The flip side of joinPath is cleanPath. Let’s look at how it gets used in +the dispatch process:

+
    +
  1. +

    +The path info requested by the user is split into a series of path pieces. +

    +
  2. +
  3. +

    +We pass the path pieces to the cleanPath function. +

    +
  4. +
  5. +

    +If cleanPath indicates a redirect (a Left response), then a 301 response +is sent to the client. This is used to force canonical URLs (eg, remove extra +slashes). +

    +
  6. +
  7. +

    +Otherwise, we try to dispatch using the response from cleanPath (a +Right). If this works, we return a response. Otherwise, we return a 404. +

    +
  8. +
+

This combination allows subsites to retain full control of how their URLs +appear, yet allows master sites to have modified URLs. As a simple example, +let’s see how we could modify Yesod to always produce trailing slashes on URLs:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# 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)
+import           Yesod
+
+data Slash = Slash
+
+mkYesod "Slash" [parseRoutes|
+/ RootR GET
+/foo FooR GET
+|]
+
+instance Yesod Slash where
+    joinPath _ ar pieces' qs' =
+        fromText ar `mappend` encodePath pieces qs
+      where
+        qs = map (TE.encodeUtf8 *** go) qs'
+        go "" = Nothing
+        go x = Just $ TE.encodeUtf8 x
+        pieces = pieces' ++ [""]
+
+    -- We want to keep canonical URLs. Therefore, if the URL is missing a
+    -- trailing slash, redirect. But the empty set of pieces always stays the
+    -- same.
+    cleanPath _ [] = Right []
+    cleanPath _ s
+        | dropWhile (not . T.null) s == [""] = -- the only empty string is the last one
+            Right $ init s
+        -- Since joinPath will append the missing trailing slash, we simply
+        -- remove empty pieces.
+        | otherwise = Left $ filter (not . T.null) s
+
+getRootR :: Handler Html
+getRootR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{RootR}>RootR
+        <p>
+            <a href=@{FooR}>FooR
+    |]
+
+getFooR :: Handler Html
+getFooR = getRootR
+
+main :: IO ()
+main = warp 3000 Slash
+

First, let’s look at our joinPath implementation. This is copied almost +verbatim from the default Yesod implementation, with one difference: we append +an extra empty string to the end. When dealing with path pieces, an empty +string will append another slash. So adding an extra empty string will force a +trailing slash.

+

cleanPath is a little bit trickier. First, we check for the empty path like +before, and if so pass it through as-is. We use Right to indicate that a +redirect is not necessary. The next clause is actually checking for two +different possible URL issues:

+
    +
  • +

    +There is a double slash, which would show up as an empty string in the middle + of our paths. +

    +
  • +
  • +

    +There is a missing trailing slash, which would show up as the last piece not + being an empty string. +

    +
  • +
+

Assuming neither of those conditions hold, then only the last piece is empty, +and we should dispatch based on all but the last piece. However, if this is not +the case, we want to redirect to a canonical URL. In this case, we strip out +all empty pieces and do not bother appending a trailing slash, since joinPath +will do that for us.

+
+
+
+

defaultLayout

+

Most websites like to apply some general template to all of their pages. +defaultLayout is the recommended approach for this. While you could just as +easily define your own function and call that instead, when you override +defaultLayout all of the Yesod-generated pages (error pages, authentication +pages) automatically get this style.

+

Overriding is very straight-forward: we use widgetToPageContent to convert a +Widget to a title, head tags and body tags, and then use withUrlRenderer to +convert a Hamlet template into an Html value. We can even add extra widget +components, like a Lucius template, from within defaultLayout. For more +information, see the previous chapter on widgets.

+

If you are using the scaffolded site, you can modify the files +templates/default-layout.hamlet and +templates/default-layout-wrapper.hamlet. The former contains most of the +contents of the <body> tag, while the latter has the rest of the HTML, such +as doctype and <head> tag. See those files for more details.

+
+

getMessage

+

Even though we haven’t covered sessions yet, I’d like to mention getMessage +here. A common pattern in web development is setting a message in one handler +and displaying it in another. For example, if a user POSTs a form, you may +want to redirect him/her to another page along with a “Form submission +complete” message. This is commonly known as +Post/Redirect/Get.

+

To facilitate this, Yesod comes built in with a pair of functions: setMessage +sets a message in the user session, and getMessage retrieves the message (and +clears it, so it doesn’t appear a second time). It’s recommended that you put +the result of getMessage into your defaultLayout. For example:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+import Data.Time (getCurrentTime)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    defaultLayout contents = do
+        PageContent title headTags bodyTags <- widgetToPageContent contents
+        mmsg <- getMessage
+        withUrlRenderer [hamlet|
+            $doctype 5
+
+            <html>
+                <head>
+                    <title>#{title}
+                    ^{headTags}
+                <body>
+                    $maybe msg <- mmsg
+                        <div #message>#{msg}
+                    ^{bodyTags}
+        |]
+
+getHomeR :: Handler Html
+getHomeR = do
+    now <- liftIO getCurrentTime
+    setMessage $ toHtml $ "You previously visited at: " ++ show now
+    defaultLayout [whamlet|<p>Try refreshing|]
+
+main :: IO ()
+main = warp 3000 App
+

We’ll cover getMessage/setMessage in more detail when we discuss sessions.

+
+
+
+

Custom error pages

+

One of the marks of a professional web site is a properly designed error page. +Yesod gets you a long way there by automatically using your defaultLayout for +displaying error pages. But sometimes, you’ll want to go even further. For +this, you’ll want to override the errorHandler method:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/error ErrorR GET
+/not-found NotFoundR GET
+|]
+
+instance Yesod App where
+    errorHandler NotFound = fmap toTypedContent $ defaultLayout $ do
+        setTitle "Request page not located"
+        toWidget [hamlet|
+<h1>Not Found
+<p>We apologize for the inconvenience, but the requested page could not be located.
+|]
+    errorHandler other = defaultErrorHandler other
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{ErrorR}>Internal server error
+            <a href=@{NotFoundR}>Not found
+    |]
+
+getErrorR :: Handler ()
+getErrorR = error "This is an error"
+
+getNotFoundR :: Handler ()
+getNotFoundR = notFound
+
+main :: IO ()
+main = warp 3000 App
+

Here we specify a custom 404 error page. We can also use the +defaultErrorHandler when we don’t want to write a custom handler for each +error type. Due to type constraints, we need to start off our methods with +fmap toTypedContent, but otherwise you can write a typical handler function. +(We’ll learn more about TypedContent in the next chapter.)

+

In fact, you could even use special responses like redirects:

+
    errorHandler NotFound = redirect HomeR
+    errorHandler other = defaultErrorHandler other
+ +
+
+

External CSS and Javascript

+ +

One of the most powerful, and most intimidating, methods in the Yesod typeclass +is addStaticContent. Remember that a Widget consists of multiple components, +including CSS and Javascript. How exactly does that CSS/JS arrive in the user’s +browser? By default, they are served in the <head> of the page, inside +<style> and <script> tags, respectively.

+

That might be simple, but it’s far from efficient. Every page load will now +require loading up the CSS/JS from scratch, even if nothing changed! What we +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 +(text/css or text/javascript) and the content itself. It will then return +one of three possible results:

+
+
+Nothing +
+

+No static file saving occurred; embed this content directly in the +HTML. This is the default behavior. +

+
+
+Just (Left Text) +
+

+This content was saved in an external file, and use the +given textual link to refer to it. +

+
+
+Just (Right (Route a, Query)) +
+

+Same, but now use a type-safe URL along with +some query string parameters. +

+
+
+

The Left result is useful if you want to store your static files on an +external server, such as a CDN or memory-backed server. The Right result is +more commonly used, and ties in very well with the static subsite. This is the +recommended approach for most applications, and is provided by the scaffolded +site by default.

+ +

The scaffolded addStaticContent does a number of intelligent things to help +you out:

+
    +
  • +

    +It automatically minifies your Javascript using the hjsmin package. +

    +
  • +
  • +

    +It names the output files based on a hash of the file contents. This means + you can set your cache headers to far in the future without fears of stale + content. +

    +
  • +
  • +

    +Also, since filenames are based on hashes, you can be guaranteed that a file + doesn’t need to be written if a file with the same name already exists. The + scaffold code automatically checks for the existence of that file, and avoids + the costly disk I/O of a write if it’s not necessary. +

    +
  • +
+
+
+

Smarter Static Files

+

Google recommends an important optimization: +serve +static files from a separate domain. The advantage to this approach is that +cookies set on your main domain are not sent when retrieving static files, thus +saving on a bit of bandwidth.

+

To facilitate this, we have the urlRenderOverride method. This method +intercepts the normal URL rendering and sets a special value for some routes. +For example, the scaffolding defines this method as:

+
urlRenderOverride y (StaticR s) =
+    Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s
+
+urlRenderOverride _ _ = Nothing
+

This means that static routes are served from a special static root, which you +can configure to be a different domain. This is a great example of the power +and flexibility of type-safe URLs: with a single line of code you’re able to +change the rendering of static routes throughout all of your handlers.

+
+
+

Authentication/Authorization

+

For simple applications, checking permissions inside each handler function can +be a simple, convenient approach. However, it doesn’t scale well. Eventually, +you’re going to want to have a more declarative approach. Many systems out +there define ACLs, special config files, and a lot of other hocus-pocus. In +Yesod, it’s just plain old Haskell. There are three methods involved:

+
+
+isWriteRequest +
+

+Determine if the current request is a "read" or "write" operations. By default, Yesod follows RESTful principles, and assumes GET, HEAD, OPTIONS, and TRACE requests are read-only, while all others are writable. +

+
+
+isAuthorized +
+

+Takes a route (i.e., type-safe URL) and a boolean indicating whether or not the request is a write request. It returns an AuthResult, which can have one of three values: +

+
    +
  • +

    +Authorized +

    +
  • +
  • +

    +AuthenticationRequired +

    +
  • +
  • +

    +Unauthorized +

    +
  • +
+
+
+

By default, it returns Authorized for all requests.

+
+
+authRoute +
+

+If isAuthorized returns AuthenticationRequired, then redirect +to the given route. If no route is provided (the default), return a 401 +“authentication required” message. +

+
+
+

These methods tie in nicely with the yesod-auth package, which is used by the +scaffolded site to provide a number of authentication options, such as OpenID, +Mozilla Persona, email, username and Twitter. We’ll cover more concrete +examples in the auth chapter.

+
+
+

Some Simple Settings

+

Not everything in the Yesod typeclass is complicated. Some methods are simple +functions. Let’s just go through the list:

+
+
+maximumContentLength +
+

+To prevent Denial of Service (DoS) attacks, Yesod will +limit the size of request bodies. Some of the time, you’ll want to bump that +limit for some routes (e.g., a file upload page). This is where you’d do that. +

+
+
+fileUpload +
+

+Determines how uploaded files and treated, based on the size of +the request. The two most common approaches are saving the files in memory, or +streaming to temporary files. By default, small requests are kept in memory and +large ones are stored to disk. +

+
+
+shouldLog +
+

+Determines if a given log message (with associated source and +level) should be sent to the log. This allows you to put lots of debugging +information into your app, but only turn it on as necessary. +

+
+
+

For the most up-to-date information, please see the Haddock API documentation +for the Yesod typeclass.

+
+
+

Summary

+

The Yesod typeclass has a number of overrideable methods that allow you to +configure your application. They are all optional, and provide sensible +defaults. By using built-in Yesod constructs like defaultLayout and +getMessage, you’ll get a consistent look-and-feel throughout your site, +including pages automatically generated by Yesod such as error pages and +authentication.

+

We haven’t covered all the methods in the Yesod typeclass in this chapter. For +a full listing of methods available, you should consult the Haddock +documentation.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.4/yesods-monads.html b/public/book-1.4/yesods-monads.html new file mode 100644 index 00000000..f0792c4d --- /dev/null +++ b/public/book-1.4/yesods-monads.html @@ -0,0 +1,643 @@ + Yesod’s Monads :: Yesod Web Framework Book- Version 1.4 +
+

Yesod’s Monads

+ + +

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 +Model-View-Controller (MVC) terms, we could consider YesodDB to be the model, +Widget to be the view, and Handler to be the controller.

+

So far, we’ve presented some very straight-forward ways to use these monads: +your main handler will run in Handler, using runDB to execute a YesodDB +query, and defaultLayout to return a Widget, which in turn was created by +calls to toWidget.

+

However, if we have a deeper understanding of these types, we can achieve some +fancier results.

+
+

Monad Transformers

+
+ +Shrek- more or less + +

Monads are like onions. Monads are not like cakes.

+
+

Before we get into the heart of Yesod’s monads, we need to understand a bit +about monad transformers. (If you already know all about monad transformers, +you can likely skip this section.) Different monads provide different +functionality: Reader allows read-only access to some piece of data +throughout a computation, Error allows you to short-circuit computations, and +so on.

+

Often times, however, you would like to be able to combine a few of these +features together. After all, why not have a computation with read-only access +to some settings variable, that could error out at any time? One approach to +this would be to write a new monad like ReaderError, but this has the obvious +downside of exponential complexity: you’ll need to write a new monad for every +single possible combination.

+

Instead, we have monad transformers. In addition to Reader, we have +ReaderT, which adds reader functionality to any other monad. So we could +represent our ReaderError as (conceptually):

+
type ReaderError = ReaderT Error
+

In order to access our settings variable, we can use the ask function. But +what about short-circuiting a computation? We’d like to use throwError, but +that won’t exactly work. Instead, we need to lift our call into the next +monad up. In other words:

+
throwError :: errValue -> Error
+lift . throwError :: errValue -> ReaderT Error
+

There are a few things you should pick up here:

+
    +
  • +

    +A transformer can be used to add functionality to an existing monad. +

    +
  • +
  • +

    +A transformer must always wrap around an existing monad. +

    +
  • +
  • +

    +The functionality available in a wrapped monad will be dependent not only on + the monad transformer, but also on the inner monad that is being wrapped. +

    +
  • +
+

A great example of that last point is the IO monad. No matter how many layers +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

+ +

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.

+

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 +takeaway here is that we can layer our Persistent transformer on top of +Handler or Widget.

+

In order to make it simpler to refer to the relevant Persistent transformer, +the yesod-persistent package defines the YesodPersistBackend associated type. +For example, if I have a site called MyApp and it uses SQL, I would define +something like type instance YesodPersistBackend MyApp = SqlBackend. And for +more convenience, we have a type synonym called YesodDB which is defined as:

+
type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerT site IO)
+

Our database actions will then have types that look like YesodDB MyApp +SomeResult. In order to run these, we can use the standard Persistent unwrap +functions (like runSqlPool) to run the action and get back a normal +Handler. To automate this, we provide the runDB function. Putting it all +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 +can do, by using the handlerToWidget function. So for example, you can run +database queries inside a Widget by using something like handlerToWidget . +runDB.

+
+
+

Example: Database-driven navbar

+

Let’s put some of this new knowledge into action. We want to create a Widget +that generates its output based on the contents of the database. Previously, +our approach would have been to load up the data in a Handler, and then pass +that data into a Widget. Now, we’ll do the loading of data in the Widget +itself. This is a boon for modularity, as this Widget can be used in any +Handler we want, without any need to pass in the database contents.

+
{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.Logger    (runNoLoggingT)
+import           Data.Text               (Text)
+import           Data.Time
+import           Database.Persist.Sqlite
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Link
+    title Text
+    url Text
+    added UTCTime
+|]
+
+data App = App ConnectionPool
+
+mkYesod "App" [parseRoutes|
+/         HomeR    GET
+/add-link AddLinkR POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB db = do
+        App pool <- getYesod
+        runSqlPool db pool
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{AddLinkR}>
+            <p>
+                Add a new link to
+                <input type=url name=url value=http://>
+                titled
+                <input type=text name=title>
+                <input type=submit value="Add link">
+        <h2>Existing links
+        ^{existingLinks}
+    |]
+
+existingLinks :: Widget
+existingLinks = do
+    links <- handlerToWidget $ runDB $ selectList [] [LimitTo 5, Desc LinkAdded]
+    [whamlet|
+        <ul>
+            $forall Entity _ link <- links
+                <li>
+                    <a href=#{linkUrl link}>#{linkTitle link}
+    |]
+
+postAddLinkR :: Handler ()
+postAddLinkR = do
+    url <- runInputPost $ ireq urlField "url"
+    title <- runInputPost $ ireq textField "title"
+    now <- liftIO getCurrentTime
+    runDB $ insert $ Link title url now
+    setMessage "Link added"
+    redirect HomeR
+
+main :: IO ()
+main = runNoLoggingT $ withSqlitePool "links.db3" 10 $ \pool -> liftIO $ do
+    runSqlPersistMPool (runMigration migrateAll) pool
+    warp 3000 $ App pool
+

Pay attention in particular to the existingLinks function. Notice how all we +needed to do was apply handlerToWidget . runDB to a normal database action. +And from within getHomeR, we treated existingLinks like any ordinary +Widget, no special parameters at all. See the figure for the output of this +app.

+ +
+
+

Example: Request information

+

Likewise, you can get request information inside a Widget. Here we can determine the sort order of a list based on a GET parameter.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.List (sortBy)
+import           Data.Ord  (comparing)
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Michael" 26
+    , Person "Gavriella" 1
+    ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href="?sort=name">Sort by name
+            |
+            <a href="?sort=age">Sort by age
+            |
+            <a href="?">No sort
+        ^{showPeople}
+    |]
+
+showPeople :: Widget
+showPeople = do
+    msort <- runInputGet $ iopt textField "sort"
+    let people' =
+            case msort of
+                Just "name" -> sortBy (comparing personName) people
+                Just "age"  -> sortBy (comparing personAge)  people
+                _           -> people
+    [whamlet|
+        <dl>
+            $forall person <- people'
+                <dt>#{personName person}
+                <dd>#{show $ personAge person}
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Notice that in this case, we didn’t even have to call handlerToWidget. The +reason is that a number of the functions included in Yesod automatically work +for both Handler and Widget, by means of the MonadHandler typeclass. In +fact, MonadHandler will allow these functions to be "autolifted" through +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

+ +

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:

+
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 +application. As a motivating example, let’s consider the +monadcryptorandom +package from Hackage, which defines both a MonadCRandom typeclass for monads +which allow generating cryptographically-secure random values, and CRandT as +a concrete instance of that typeclass. You would like to write some code that +generates a random bytestring, e.g.:

+
import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+getHomeR = do
+    randomBS <- getBytes 128
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+

However, this results in an error message along the lines of:

+
    No instance for (MonadCRandom e0 (HandlerT App IO))
+      arising from a use of ‘getBytes’
+    In a stmt of a 'do' block: randomBS <- getBytes 128
+

How do we get such an instance? One approach is to simply use the CRandT monad transformer when we call getBytes. A complete example of doing so would be:

+
{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell, TypeFamilies #-}
+import Yesod
+import Crypto.Random (SystemRandom, newGenIO)
+import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    gen <- liftIO newGenIO
+    eres <- evalCRandT (getBytes 16) (gen :: SystemRandom)
+    randomBS <-
+        case eres of
+            Left e -> error $ show (e :: GenError)
+            Right gen -> return gen
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+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.

+

But there are two downsides to this approach:

+
    +
  1. +

    +It requires you to jump into this alternate monad each time you want to work with random values. +

    +
  2. +
  3. +

    +It’s inefficient: you need to create a new random seed each time you enter this other monad. +

    +
  4. +
+

The second point could be worked around by storing the random seed in the +foundation datatype, in a mutable reference like an IORef, and then +atomically sampling it each time we enter the CRandT transformer. But we can +even go a step further, and use this trick to make our Handler monad itself +an instance of MonadCRandom! Let’s look at the code, which is in fact a bit +involved:

+
{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE TypeSynonymInstances  #-}
+import           Control.Monad              (join)
+import           Control.Monad.Catch        (catch, throwM)
+import           Control.Monad.CryptoRandom
+import           Control.Monad.Error.Class  (MonadError (..))
+import           Crypto.Random              (SystemRandom, newGenIO)
+import           Data.ByteString.Base16     (encode)
+import           Data.IORef
+import           Data.Text.Encoding         (decodeUtf8)
+import           Yesod
+
+data App = App
+    { randGen :: IORef SystemRandom
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    randomBS <- getBytes 16
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+instance MonadError GenError Handler where
+    throwError = throwM
+    catchError = catch
+instance MonadCRandom GenError Handler where
+    getCRandom  = wrap crandom
+    {-# INLINE getCRandom #-}
+    getBytes i = wrap (genBytes i)
+    {-# INLINE getBytes #-}
+    getBytesWithEntropy i e = wrap (genBytesWithEntropy i e)
+    {-# INLINE getBytesWithEntropy #-}
+    doReseed bs = do
+        genRef <- fmap randGen getYesod
+        join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+            case reseed bs gen of
+                Left e -> (gen, throwM e)
+                Right gen' -> (gen', return ())
+    {-# INLINE doReseed #-}
+
+wrap :: (SystemRandom -> Either GenError (a, SystemRandom)) -> Handler a
+wrap f = do
+    genRef <- fmap randGen getYesod
+    join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+        case f gen of
+            Left e -> (gen, throwM e)
+            Right (x, gen') -> (gen', return x)
+
+main :: IO ()
+main = do
+    gen <- newGenIO
+    genRef <- newIORef gen
+    warp 3000 App
+        { randGen = genRef
+        }
+

This really comes down to a few different concepts:

+
    +
  1. +

    +We modify the App datatype to have a field for an IORef SystemRandom. +

    +
  2. +
  3. +

    +Similarly, we modify the main function to generate an IORef SystemRandom. +

    +
  4. +
  5. +

    +Our getHomeR function became a lot simpler: we can now simply call getBytes without playing with transformers. +

    +
  6. +
  7. +

    +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. +

    +
  8. +
+

Hopefully, this helps get across an important point: the power of the +HandlerT transformer. 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 +ErrorT with this abstraction.

+
+
+

Summary

+

If you completely ignore this chapter, you’ll still be able to use Yesod to +great benefit. The advantage of understanding how Yesod’s monads interact is to +be able to produce cleaner, more modular code. Being able to perform arbitrary +actions in a Widget can be a powerful tool, and understanding how Persistent +and your Handler code interact can help you make more informed design +decisions in your app.

+
+
+
+

Note: You are looking at version 1.4 of the book, which is one version behind

+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6.html b/public/book-1.6.html new file mode 100644 index 00000000..11ef87c8 --- /dev/null +++ b/public/book-1.6.html @@ -0,0 +1,187 @@ + Yesod Web Framework Book- Version 1.6 +

Available from O'Reilly:

+
Developing Web Applications with Haskell and Yesod + +
+
+

The current version of the book +covers Yesod 1.6. We also maintain older copies for +version 1.4, version 1.2 +, and +version 1.1.

+ +
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/authentication-and-authorization.html b/public/book-1.6/authentication-and-authorization.html new file mode 100644 index 00000000..09a87417 --- /dev/null +++ b/public/book-1.6/authentication-and-authorization.html @@ -0,0 +1,661 @@ + Authentication and Authorization :: Yesod Web Framework Book- Version 1.6 +
+

Authentication and Authorization

+ + +

Authentication and authorization are two very related, and yet separate, +concepts. While the former deals with identifying a user, the latter determines +what a user is allowed to do. Unfortunately, since both terms are often +abbreviated as "auth," the concepts are often conflated.

+

Yesod provides built-in support for a number of third-party authentication +systems, such as OpenID, BrowserID and OAuth. These are systems where your +application trusts some external system for validating a user’s credentials. +Additionally, there is support for more commonly used username/password and +email/password systems. The former route ensures simplicity for users (no new +passwords to remember) and implementors (no need to deal with an entire +security architecture), while the latter gives the developer more control.

+

On the authorization side, we are able to take advantage of REST and type-safe +URLs to create simple, declarative systems. Additionally, since all +authorization code is written in Haskell, you have the full flexibility of the +language at your disposal.

+

This chapter will cover how to set up an "auth" solution in Yesod and discuss +some trade-offs in the different authentication options.

+
+

Overview

+

The yesod-auth package provides a unified interface for a number of different +authentication plugins. The only real requirement for these backends is that +they identify a user based on some unique string. In OpenID, for instance, this +would be the actual OpenID value. In BrowserID, it’s the email address. For +HashDB (which uses a database of hashed passwords), it’s the username.

+

Each authentication plugin provides its own system for logging in, whether it +be via passing tokens with an external site or an email/password form. After a +successful login, the plugin sets a value in the user’s session to indicate +his/her AuthId. This AuthId is usually a Persistent ID from a table used +for keeping track of users.

+

There are a few functions available for querying a user’s AuthId, most +commonly maybeAuthId, requireAuthId, maybeAuth and requireAuth. The +“require” versions will redirect to a login page if the user is not logged in, +while the second set of functions (the ones not ending in Id) give both the +table ID and entity value.

+

Since all of the storage of AuthId is built on top of sessions, all of the +rules from there apply. In particular, the data is stored in an encrypted, +HMACed client cookie, which automatically times out after a certain configurable +period of inactivity. Additionally, since there is no server-side component to sessions, +logging out simply deletes the data from the session cookie; if a user reuses an +older cookie value, the session will still be valid.

+ +

On the flip side, authorization is handled by a few methods inside the Yesod +typeclass. For every request, these methods are run to determine if access +should be allowed, denied, or if the user needs to be authenticated. By +default, these methods allow access for every request. Alternatively, you can +implement authorization in a more ad-hoc way by adding calls to requireAuth +and the like within individual handler functions, though this undermines many +of the benefits of a declarative authorization system.

+
+
+

Authenticate Me

+

Let’s jump right in with an example of authentication. For the Google OAuth authentication to work you should follow these steps:

+
    +
  1. +

    +Read on Google Developers Help how to obtain OAuth 2.0 credentials such as a client ID and client secret that are known to both Google and your application. +

    +
  2. +
  3. +

    +Set Authorized redirect URIs to http://localhost:3000/auth/page/googleemail2/complete. +

    +
  4. +
  5. +

    +Enable Google+ API and Contacts API. +

    +
  6. +
  7. +

    +Once you have the clientId and secretId, replace them in the code below. +

    +
  8. +
+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default                (def)
+import           Data.Text                   (Text)
+import           Network.HTTP.Client.Conduit (Manager, newManager)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.GoogleEmail2
+
+
+-- Replace with Google client ID.
+clientId :: Text
+clientId = ""
+
+-- Replace with Google secret ID.
+clientSecret :: Text
+clientSecret = ""
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Note: In order to log in with BrowserID, you must correctly
+    -- set your hostname here.
+    approot = ApprootStatic "http://localhost:3000"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    authenticate = return . Authenticated . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ = [ authGoogleEmail clientId clientSecret ]
+
+    -- The default maybeAuthId assumes a Persistent database. We're going for a
+    -- simpler AuthId, so we'll just do a direct lookup in the session.
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = do
+    man <- newManager
+    warp 3000 $ App man
+

We’ll start with the route declarations. First, we declare our standard HomeR +route, and then we set up the authentication subsite. Remember that a subsite +needs four parameters: the path to the subsite, the route name, the subsite +name, and a function to get the subsite value. In other words, based on the +line:

+
/auth AuthR Auth getAuth
+

We need to have getAuth :: MyAuthSite → Auth. While we haven’t written +that function ourselves, yesod-auth provides it automatically. With other +subsites (like static files), we provide configuration settings in the subsite +value, and therefore need to specify the get function. In the auth subsite, we +specify these settings in a separate typeclass, YesodAuth.

+ +

So what exactly goes in this YesodAuth instance? There are five required declarations:

+
    +
  • +

    +AuthId is an associated type. This is the value yesod-auth will give you + when you ask if a user is logged in (via maybeAuthId or requireAuthId). + In our case, we’re simply using Text, to store the raw identifier- email + address in our case, as we’ll soon see. +

    +
  • +
  • +

    +authenticate gets the actual AuthId from the Creds (credentials) data + type. This type has three pieces of information: the authentication backend + used (googleemail in our case), the actual identifier, and an + associated list of arbitrary extra information. Each backend provides + different extra information; see their docs for more information. +

    +
  • +
  • +

    +loginDest gives the route to redirect to after a successful login. +

    +
  • +
  • +

    +Likewise, logoutDest gives the route to redirect to after a logout. +

    +
  • +
  • +

    +authPlugins is a list of individual authentication backends to use. In our example, we’re using Google OAuth, which authenticates a user using their Google account. +

    +
  • +
+

In addition to these five methods, there are other methods available to control +other behavior of the authentication system, such as what the login page looks +like. For more information, please +see the API documentation.

+

In our HomeR handler, we have some simple links to the login and logout +pages, depending on whether or not the user is logged in. Notice how we +construct these subsite links: first we give the subsite route name (AuthR), +followed by the route within the subsite (LoginR and LogoutR).

+
+
+

Email

+

For many use cases, third-party authentication of email will be sufficient. +Occasionally, you’ll want users to create passwords on your site. The +scaffolded site does not include this setup, because:

+
    +
  • +

    +In order to securely accept passwords, you need to be running over SSL. Many + users are not serving their sites over SSL. +

    +
  • +
  • +

    +While the email backend properly salts and hashes passwords, a compromised + database could still be problematic. Again, we make no assumptions that Yesod + users are following secure deployment practices. +

    +
  • +
  • +

    +You need to have a working system for sending email. Many web servers these + days are not equipped to deal with all of the spam protection measures used + by mail servers. +

    +
  • +
+ +

But assuming you are able to meet these demands, and you want to have a +separate password login specifically for your site, Yesod offers a built-in +backend. It requires quite a bit of code to set up, since it needs to store +passwords securely in the database and send a number of different emails to +users (verify account, password retrieval, etc.).

+

Let’s have a look at a site that provides email authentication, storing +passwords in a Persistent SQLite database.

+ +
{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad            (join)
+import           Control.Monad.Logger     (runNoLoggingT)
+import           Data.Maybe               (isJust)
+import           Data.Text                (Text, unpack)
+import qualified Data.Text.Lazy.Encoding
+import           Data.Typeable            (Typeable)
+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
+import           Yesod.Auth.Email
+
+share [mkPersist sqlSettings { mpsGeneric = False }, mkMigrate "migrateAll"] [persistLowerCase|
+User
+    email Text
+    password Text Maybe -- Password may not be set yet
+    verkey Text Maybe -- Used for resetting passwords
+    verified Bool
+    UniqueUser email
+    deriving Typeable
+|]
+
+data App = App SqlBackend
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Emails will include links, so be sure to include an approot so that
+    -- the links are valid!
+    approot = ApprootStatic "http://localhost:3000"
+    yesodMiddleware = defaultCsrfMiddleware . defaultYesodMiddleware
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Set up Persistent
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB f = do
+        App conn <- getYesod
+        runSqlConn f conn
+
+instance YesodAuth App where
+    type AuthId App = UserId
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    authPlugins _ = [authEmail]
+
+    -- Need to find the UserId for the given email address.
+    authenticate creds = liftHandler $ runDB $ do
+        x <- insertBy $ User (credsIdent creds) Nothing Nothing False
+        return $ Authenticated $
+            case x of
+                Left (Entity userid _) -> userid -- existing user
+                Right userid -> userid -- newly added user
+
+instance YesodAuthPersist App
+
+-- Here's all of the email-specific code
+instance YesodAuthEmail App where
+    type AuthEmailId App = UserId
+
+    afterPasswordRoute _ = HomeR
+
+    addUnverified email verkey =
+        liftHandler $ runDB $ insert $ User email Nothing (Just verkey) False
+
+    sendVerifyEmail email _ verurl = do
+        -- Print out to the console the verification email, for easier
+        -- debugging.
+        liftIO $ putStrLn $ "Copy/ Paste this URL in your browser:" ++ unpack verurl
+
+        -- Send email.
+        liftIO $ renderSendMail (emptyMail $ Address Nothing "noreply")
+            { mailTo = [Address Nothing email]
+            , mailHeaders =
+                [ ("Subject", "Verify your email address")
+                ]
+            , mailParts = [[textPart, htmlPart]]
+            }
+      where
+        textPart = Part
+            { partType = "text/plain; charset=utf-8"
+            , partEncoding = None
+            , partDisposition = DefaultDisposition
+            , partContent = PartContent $ Data.Text.Lazy.Encoding.encodeUtf8
+                [stext|
+                    Please confirm your email address by clicking on the link below.
+
+                    #{verurl}
+
+                    Thank you
+                |]
+            , partHeaders = []
+            }
+        htmlPart = Part
+            { partType = "text/html; charset=utf-8"
+            , partEncoding = None
+            , partDisposition = DefaultDisposition
+            , partContent = PartContent $ renderHtml
+                [shamlet|
+                    <p>Please confirm your email address by clicking on the link below.
+                    <p>
+                        <a href=#{verurl}>#{verurl}
+                    <p>Thank you
+                |]
+            , partHeaders = []
+            }
+    getVerifyKey = liftHandler . runDB . fmap (join . fmap userVerkey) . get
+    setVerifyKey uid key = liftHandler $ runDB $ update uid [UserVerkey =. Just key]
+    verifyAccount uid = liftHandler $ runDB $ do
+        mu <- get uid
+        case mu of
+            Nothing -> return Nothing
+            Just u -> do
+                update uid [UserVerified =. True, UserVerkey =. Nothing]
+                return $ Just uid
+    getPassword = liftHandler . runDB . fmap (join . fmap userPassword) . get
+    setPassword uid pass = liftHandler . runDB $ update uid [UserPassword =. Just pass]
+    getEmailCreds email = liftHandler $ runDB $ do
+        mu <- getBy $ UniqueUser email
+        case mu of
+            Nothing -> return Nothing
+            Just (Entity uid u) -> return $ Just EmailCreds
+                { emailCredsId = uid
+                , emailCredsAuthId = Just uid
+                , emailCredsStatus = isJust $ userPassword u
+                , emailCredsVerkey = userVerkey u
+                , emailCredsEmail = email
+                }
+    getEmail = liftHandler . runDB . fmap (fmap userEmail) . get
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = runNoLoggingT $ withSqliteConn "email.db3" $ \conn -> liftIO $ do
+    runSqlConn (runMigration migrateAll) conn
+    warp 3000 $ App conn
+
+
+

Authorization

+

Once you can authenticate your users, you can use their credentials to +authorize requests. Authorization in Yesod is simple and declarative: most of +the time, you just need to add the authRoute and isAuthorized methods to +your Yesod typeclass instance. Let’s see an example.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default         (def)
+import           Data.Text            (Text)
+import           Network.HTTP.Conduit (Manager, newManager, tlsManagerSettings)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy -- just for testing, don't use in real life!!!
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET POST
+/admin AdminR GET
+/auth  AuthR  Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+
+    -- route name, then a boolean indicating if it's a write request
+    isAuthorized HomeR True = isAdmin
+    isAuthorized AdminR _ = isAdmin
+
+    -- anyone can access other pages
+    isAuthorized _ _ = return Authorized
+
+isAdmin = do
+    mu <- maybeAuthId
+    return $ case mu of
+        Nothing -> AuthenticationRequired
+        Just "admin" -> Authorized
+        Just _ -> Unauthorized "You must be an admin"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    authenticate = return . Authenticated . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ = [authDummy]
+
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Note: Log in as "admin" to be an administrator.
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            <p>
+                <a href=@{AdminR}>Go to admin page
+            <form method=post>
+                Make a change (admins only)
+                \ #
+                <input type=submit>
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    setMessage "You made some change to the page"
+    redirect HomeR
+
+getAdminR :: Handler Html
+getAdminR = defaultLayout
+    [whamlet|
+        <p>I guess you're an admin!
+        <p>
+            <a href=@{HomeR}>Return to homepage
+    |]
+
+main :: IO ()
+main = do
+    manager <- newManager tlsManagerSettings
+    warp 3000 $ App manager
+

authRoute should be your login page, almost always AuthR LoginR. +isAuthorized is a function that takes two parameters: the requested route, +and whether or not the request was a "write" request. You can actually change +the meaning of what a write request is using the isWriteRequest method, but +the out-of-the-box version follows RESTful principles: anything but a GET, +HEAD, OPTIONS or TRACE request is a write request.

+

What’s convenient about the body of isAuthorized is that you can run any +Handler code you want. This means you can:

+
    +
  • +

    +Access the filesystem (normal IO) +

    +
  • +
  • +

    +Lookup values in the database +

    +
  • +
  • +

    +Pull any session or request values you want +

    +
  • +
+

Using these techniques, you can develop as sophisticated an authorization +system as you like, or even tie into existing systems used by your +organization.

+
+
+

Conclusion

+

This chapter covered the basics of setting up user authentication, as well as +how the built-in authorization functions provide a simple, declarative approach +for users. While these are complicated concepts, with many approaches, Yesod +should provide you with the building blocks you need to create your own +customized auth solution.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/basics.html b/public/book-1.6/basics.html new file mode 100644 index 00000000..9c3dcb79 --- /dev/null +++ b/public/book-1.6/basics.html @@ -0,0 +1,466 @@ + Basics :: Yesod Web Framework Book- Version 1.6 +
+

Basics

+ + +

The first step with any new technology is getting it running. The goal of +this chapter is to get you started with a simple Yesod application, and cover +some of the basic concepts and terminology.

+
+

Hello World

+

Let’s get this book started properly: a simple web page that says Hello +World:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data HelloWorld = HelloWorld
+
+mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod HelloWorld
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|Hello World!|]
+
+main :: IO ()
+main = warp 3000 HelloWorld
+

If you save that code in helloworld.hs and run it with runhaskell +helloworld.hs, you’ll get a web server running on port 3000. Note, +if you followed the quick start guide and installed yesod with stack +then you will not have runhaskell and will need to run stack runghc +helloworld.hs instead. If you point your browser to http://localhost:3000, +you’ll get the following HTML:

+
<!DOCTYPE html>
+<html><head><title></title></head><body>Hello World!</body></html>
+

We’ll refer back to this example through the rest of the chapter.

+
+
+

Routing

+

Like most modern web frameworks, Yesod follows a +front controller +pattern. This means that every request to a Yesod application enters at the +same point and is routed from there. As a contrast, in systems like PHP and ASP +you usually create a number of different files, and the web server +automatically directs requests to the relevant file.

+

In addition, Yesod uses a declarative style for specifying routes. In our +example above, this looked like:

+
mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+ +

In English, all this means is: In the HelloWorld application, create one route. +I’d like to call it HomeR, it should listen for requests to / (the root of +the application), and should answer GET requests. We call HomeR a resource, +which is where the "R" suffix comes from.

+ +

The mkYesod TH function generates quite a bit of code here: a route data +type, parser/render functions, a dispatch function, and some helper types. +We’ll look at this in more detail in the routing chapter. But by using the +-ddump-splices GHC option, we can get an immediate look at the generated +code. A much cleaned up version of it is:

+
instance RenderRoute HelloWorld where
+    data Route HelloWorld = HomeR
+        deriving (Show, Eq, Read)
+    renderRoute HomeR = ([], [])
+
+instance ParseRoute HelloWorld where
+    parseRoute ([], _) = Just HomeR
+    parseRoute _       = Nothing
+
+instance YesodDispatch HelloWorld where
+    yesodDispatch env req =
+        yesodRunner handler env mroute req
+      where
+        mroute = parseRoute (pathInfo req, textQueryString req)
+        handler =
+            case mroute of
+                Nothing -> notFound
+                Just HomeR ->
+                    case requestMethod req of
+                        "GET" -> getHomeR
+                        _     -> badMethod
+
+type Handler = HandlerT HelloWorld IO
+ +

We can see that the RenderRoute class defines an associated data type +providing the routes for our application. In this simple example, we have just +one route: HomeR. In real life applications, we’ll have many more, and they +will be more complicated than our HomeR.

+

renderRoute takes a route and turns it into path segments and query string +parameters. Again, our example is simple, so the code is likewise simple: both +values are empty lists.

+

ParseRoute provides the inverse function, parseRoute. Here we see the first +strong motivation for our reliance on Template Haskell: it ensures that the +parsing and rendering of routes correspond correctly with each other. This kind +of code can easily become difficult to keep in sync when written by hand. By +relying on code generation, we’re letting the compiler (and Yesod) handle those +details for us.

+

YesodDispatch provides a means of taking an input request and passing it to +the appropriate handler function. The process is essentially:

+
    +
  1. +

    +Parse the request. +

    +
  2. +
  3. +

    +Choose a handler function. +

    +
  4. +
  5. +

    +Run the handler function. +

    +
  6. +
+

The code generation follows a simple format for matching routes to handler +function names, which we’ll describe in the next section.

+

Finally, we have a simple type synonym defining Handler to make our code a +little easier to write.

+

There’s a lot more going on here than we’ve described. The generated dispatch +code actually uses the view patterns language extension for efficiency, more +type class instances are created, and there are other cases to handle such as +subsites. We’ll get into the details as we go through the book, especially in +the “Understanding a Request” chapter.

+
+
+

Handler function

+

So we have a route named HomeR, and it responds to GET requests. How do you +define your response? You write a handler function. Yesod follows a standard +naming scheme for these functions: it’s the lower case method name (e.g., GET +becomes get) followed by the route name. In this case, the function name +would be getHomeR.

+

Most of the code you write in Yesod lives in handler functions. This is where +you process user input, perform database queries and create responses. In our +simple example, we create a response using the defaultLayout function. This +function wraps up the content it’s given in your site’s template. By default, +it produces an HTML file with a doctype and html, head and body tags. As +we’ll see in the Yesod typeclass chapter, this function can be overridden to do +much more.

+

In our example, we pass [whamlet|Hello World!|] to defaultLayout. +whamlet is another quasi-quoter. In this case, it converts Hamlet syntax into +a Widget. Hamlet is the default HTML templating engine in Yesod. Together with +its siblings Cassius, Lucius and Julius, you can create HTML, CSS and +Javascript in a fully type-safe and compile-time-checked manner. We’ll see much +more about this in the Shakespeare chapter.

+

Widgets are another cornerstone of Yesod. They allow you to create modular +components of a site consisting of HTML, CSS and Javascript and reuse them +throughout your site. We’ll get into more detail on them in the widgets +chapter.

+
+
+

The Foundation

+

The word ‘HelloWorld’ shows up a number of times in our example. Every Yesod +application has a foundation datatype. This datatype must be an instance of the +Yesod typeclass, which provides a central place for declaring a number of +different settings controlling the execution of our application.

+

In our case, this datatype is pretty boring: it doesn’t contain any +information. Nonetheless, the foundation is central to how our example runs: it +ties together the routes with the instance declaration and lets it all be run. +We’ll see throughout this book that the foundation pops up in a whole bunch of +places.

+

But foundations don’t have to be boring: they can be used to store lots of +useful information, usually stuff that needs to be initialized at program +launch and used throughout. Some very common examples are:

+
    +
  • +

    +A database connection pool. +

    +
  • +
  • +

    +Settings loaded from a config file. +

    +
  • +
  • +

    +An HTTP connection manager. +

    +
  • +
  • +

    +A random number generator. +

    +
  • +
+ +
+
+

Running

+

Once again we mention HelloWorld in our main function. Our foundation +contains all the information we need to route and respond to requests in our +application; now we just need to convert it into something that can run. A +useful function for this in Yesod is warp, which runs the Warp webserver with +a number of default settings enabled on the specified port (here, it’s 3000).

+

One of the features of Yesod is that you aren’t tied down to a single +deployment strategy. Yesod is built on top of the Web Application Interface +(WAI), allowing it to run on FastCGI, SCGI, Warp, or even as a desktop +application using the Webkit library. We’ll discuss some of these options in +the deployment chapter. And at the end of this chapter, we will explain the +development server.

+

Warp is the premiere deployment option for Yesod. It is a lightweight, highly +efficient web server developed specifically for hosting Yesod. It is also used +outside of Yesod for other Haskell development (both framework and +non-framework applications), as well as a standard file server in a number of +production environments.

+
+
+

Resources and type-safe URLs

+

In our hello world, we defined just a single resource (HomeR). A web +application is usually much more exciting with more than one page on it. Let’s +take a look:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data Links = Links
+
+mkYesod "Links" [parseRoutes|
+/ HomeR GET
+/page1 Page1R GET
+/page2 Page2R GET
+|]
+
+instance Yesod Links
+
+getHomeR  = defaultLayout [whamlet|<a href=@{Page1R}>Go to page 1!|]
+getPage1R = defaultLayout [whamlet|<a href=@{Page2R}>Go to page 2!|]
+getPage2R = defaultLayout [whamlet|<a href=@{HomeR}>Go home!|]
+
+main = warp 3000 Links
+

Overall, this is very similar to Hello World. Our foundation is now Links +instead of HelloWorld, and in addition to the HomeR resource, we’ve added +Page1R and Page2R. As such, we’ve also added two more handler functions: +getPage1R and getPage2R.

+

The only truly new feature is inside the whamlet quasi-quotation. We’ll delve +into syntax in the “Shakespeare” chapter, but we can see that:

+
<a href=@{Page1R}>Go to page 1!
+

creates a link to the Page1R resource. The important thing to note here is +that Page1R is a data constructor. By making each resource a data +constructor, we have a feature called type-safe URLs. Instead of splicing +together strings to create URLs, we simply create a plain old Haskell value. By +using at-sign interpolation (@{…}), Yesod automatically renders those +values to textual URLs before sending things off to the user. We can see how +this is implemented by looking again at the -ddump-splices output:

+
instance RenderRoute Links where
+    data Route Links = HomeR | Page1R | Page2R
+      deriving (Show, Eq, Read)
+
+    renderRoute HomeR  = ([], [])
+    renderRoute Page1R = (["page1"], [])
+    renderRoute Page2R = (["page2"], [])
+

In the Route associated type for Links, we have additional constructors for +Page1R and Page2R. We also now have a better glimpse of the return values +for renderRoute. The first part of the tuple gives the path pieces for the +given route. The second part gives the query string parameters; for almost all +use cases, this will be an empty list.

+

It’s hard to over-estimate the value of type-safe URLs. They give you a huge +amount of flexibility and robustness when developing your application. You can +move URLs around at will without ever breaking links. In the routing chapter, +we’ll see that routes can take parameters, such as a blog entry URL taking the +blog post ID.

+

Let’s say you want to switch from routing on the numerical post ID to a +year/month/slug setup. In a traditional web framework, you would need to go +through every single reference to your blog post route and update +appropriately. If you miss one, you’ll have 404s at runtime. In Yesod, all you +do is update your route and compile: GHC will pinpoint every single line of +code that needs to be corrected.

+
+
+

Non-HTML responses

+

Yesod can serve up any kind of content you want, and has first-class support +for many commonly used response formats. You’ve seen HTML so far, but JSON data +is just as easy, via the aeson package:

+
{-# LANGUAGE ExtendedDefaultRules #-}
+{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE QuasiQuotes          #-}
+{-# LANGUAGE TemplateHaskell      #-}
+{-# LANGUAGE TypeFamilies         #-}
+import Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR  = return $ object ["msg" .= "Hello World"]
+
+main = warp 3000 App
+

We’ll cover JSON responses in more detail in later chapters, including how to +automatically switch between HTML and JSON representations depending on the +Accept request header.

+
+
+

The scaffolded site

+

We’ll use stack to install the Yesod libraries and the yesod helper +executable. Then we’ll use stack new to create a scaffolded site. It +will generate a folder containing the default scaffolded site. Inside that +folder, you can run stack install --only-dependencies to build any extra +dependencies (such as your database backends), and then yesod devel to run +your site.

+ +

The scaffolded site gives you a lot of best practices out of the box, setting +up files and dependencies in a time-tested approach used by most production +Yesod sites. However, all this convenience can get in the way of actually +learning Yesod. Therefore, most of this book will avoid the scaffolding tool, +and instead deal directly with Yesod as a library. But if you’re going to build +a real site, I strongly recommend using the scaffolding.

+

We will cover the structure of the scaffolded site in the scaffolding chapter.

+
+
+

Development server

+

One of the advantages interpreted languages have over compiled languages is +fast prototyping: you save changes to a file and hit refresh. If we want to +make any changes to our Yesod apps above, we’ll need to call runhaskell from +scratch, which can be a bit tedious.

+

Fortunately, there’s a solution to this: yesod devel automatically rebuilds +and reloads your code for you. This can be a great way to develop your Yesod +projects, and when you’re ready to move to production, you still get to compile +down to incredibly efficient code. The Yesod scaffolding automatically sets +things up for you. This gives you the best of both worlds: rapid prototyping +and fast production code.

+

It’s a little bit more involved to set up your code to be used by yesod +devel, so our examples will just use warp. Fortunately, the scaffolded site +is fully configured to use the development server, so when you’re ready to move +over to the real world, it will be waiting for you.

+
+
+

Summary

+

Every Yesod application is built around a foundation datatype. We associate +some resources with that datatype and define some handler functions, and Yesod +handles all of the routing. These resources are also data constructors, which +lets us have type-safe URLs.

+

By being built on top of WAI, Yesod applications can run with a number of +different backends. For simple apps, the warp function provides a convenient +way to use the Warp web server. For rapid development, using yesod devel is a +good choice. And when you’re ready to move to production, you have the full +power and flexibility to configure Warp (or any other WAI handler) to suit your +needs.

+

When developing in Yesod, we get a number of choices for coding style: +quasi-quotation or external files, warp or yesod devel, and so on. The +examples in this book will tend towards using the choices that are easiest to +copy-and-paste, but the more powerful options will be available when you start +building real Yesod applications.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/blog-example-advanced.html b/public/book-1.6/blog-example-advanced.html new file mode 100644 index 00000000..2dbedd1b --- /dev/null +++ b/public/book-1.6/blog-example-advanced.html @@ -0,0 +1,511 @@ + Blog: i18n, authentication, authorization, and database :: Yesod Web Framework Book- Version 1.6 +
+

Blog: i18n, authentication, authorization, and database

+ + +

This is a simple blog app. It allows an admin to add blog posts via a rich text +editor (nicedit), allows logged-in users to comment, and has full i18n support. +It is also a good example of using a Persistent database, leveraging Yesod’s +authorization system, and templates.

+

While in general we recommend placing templates, Persist entity definitions, +and routing in separate files, we’ll keep it all in one file here for +convenience. The one exception you’ll see below will be i18n messages.

+

We’ll start off with our language extensions. In scaffolded code, the language +extensions are specified in the cabal file, so you won’t need to put this in +your individual Haskell files.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, GADTs, FlexibleContexts,
+             MultiParamTypeClasses, DeriveDataTypeable,
+             GeneralizedNewtypeDeriving, ViewPatterns #-}
+

Now our imports.

+
import Yesod
+import Yesod.Auth
+import Yesod.Form.Nic (YesodNic, nicHtmlField)
+import Yesod.Auth.OpenId (IdentifierType(..), authOpenId)
+import Data.Text (Text)
+import Network.HTTP.Client.TLS (tlsManagerSettings)
+import Network.HTTP.Conduit (Manager, newManager)
+import Database.Persist.Sqlite
+    ( ConnectionPool, SqlBackend, runSqlPool, runMigration
+    , createSqlitePool, runSqlPersistMPool
+    )
+import Data.Time (UTCTime, getCurrentTime)
+import Control.Applicative ((<$>), (<*>), pure)
+import Data.Typeable (Typeable)
+import Control.Monad.Logger (runStdoutLoggingT)
+

First we’ll set up our Persistent entities. We’re going to both create our data +types (via mkPersist) and create a migration function, which will automatically +create and update our SQL schema. If you were using the MongoDB backend, +migration would not be needed.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+

Keeps track of users. In a more robust application, we would also keep account +creation date, display name, etc.

+
User
+   email Text
+   UniqueUser email
+

In order to work with yesod-auth’s caching, our User type must be an instance +of Typeable.

+
   deriving Typeable
+

An individual blog entry (I’ve avoided using the word "post" due to the +confusion with the request method POST).

+
Entry
+   title Text
+   posted UTCTime
+   content Html
+

And a comment on the blog post.

+
Comment
+   entry EntryId
+   posted UTCTime
+   user UserId
+   name Text
+   text Textarea
+|]
+

Every site has a foundation datatype. This value is initialized before +launching your application, and is available throughout. We’ll store a database +connection pool and HTTP connection manager in ours. See the very end of this +file for how those are initialized.

+
data Blog = Blog
+   { connPool    :: ConnectionPool
+   , httpManager :: Manager
+   }
+

To make i18n easy and translator friendly, we have a special file format for +translated messages. There is a single file for each language, and each file is +named based on the language code (e.g., en, es, de-DE) and placed in that +folder. We also specify the main language file (here, "en") as a default +language.

+
mkMessage "Blog" "blog-messages" "en"
+

Our blog-messages/en.msg file contains the following content:

+
-- @blog-messages/en.msg
+NotAnAdmin: You must be an administrator to access this page.
+
+WelcomeHomepage: Welcome to the homepage
+SeeArchive: See the archive
+
+NoEntries: There are no entries in the blog
+LoginToPost: Admins can login to post
+NewEntry: Post to blog
+NewEntryTitle: Title
+NewEntryContent: Content
+
+PleaseCorrectEntry: Your submitted entry had some errors, please correct and try again.
+EntryCreated title@Text: Your new blog post, #{title}, has been created
+
+EntryTitle title@Text: Blog post: #{title}
+CommentsHeading: Comments
+NoComments: There are no comments
+AddCommentHeading: Add a Comment
+LoginToComment: You must be logged in to comment
+AddCommentButton: Add comment
+
+CommentName: Your display name
+CommentText: Comment
+CommentAdded: Your comment has been added
+PleaseCorrectComment: Your submitted comment had some errors, please correct and try again.
+
+HomepageTitle: Yesod Blog Demo
+BlogArchiveTitle: Blog Archive
+

Now we’re going to set up our routing table. We have four entries: a homepage, +an entry list page (BlogR), an individual entry page (EntryR) and our +authentication subsite. Note that BlogR and EntryR both accept GET and POST +methods. The POST methods are for adding a new blog post and adding a new +comment, respectively.

+
mkYesod "Blog" [parseRoutes|
+/              HomeR  GET
+/blog          BlogR  GET POST
+/blog/#EntryId EntryR GET POST
+/auth          AuthR  Auth getAuth
+|]
+

Every foundation needs to be an instance of the Yesod typeclass. This is where +we configure various settings.

+
instance Yesod Blog where
+

The base of our application. Note that in order to make BrowserID work +properly, this must be a valid URL.

+
    approot = ApprootStatic "http://localhost:3000"
+

Our authorization scheme. We want to have the following rules:

+
    +
  • +

    +Only admins can add a new entry. +

    +
  • +
  • +

    +Only logged in users can add a new comment. +

    +
  • +
  • +

    +All other pages can be accessed by anyone. +

    +
  • +
+

We set up our routes in a RESTful way, where the actions that could make +changes are always using a POST method. As a result, we can simply check for +whether or not a request is a write request, given by the True in the second +field.

+

First, we’ll authorize requests to add a new entry.

+
    isAuthorized BlogR True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just (Entity _ user)
+                | isAdmin user -> return Authorized
+                | otherwise    -> unauthorizedI MsgNotAnAdmin
+

Now we’ll authorize requests to add a new comment.

+
    isAuthorized (EntryR _) True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just _  -> return Authorized
+

And for all other requests, the result is always authorized.

+
    isAuthorized _ _ = return Authorized
+

Where a user should be redirected to if they get an AuthenticationRequired.

+
    authRoute _ = Just (AuthR LoginR)
+

This is where we define our site look-and-feel. The function is given the +content for the individual page, and wraps it up with a standard template.

+
    defaultLayout inside = do
+

Yesod encourages the get-following-post pattern, where after a POST, the user +is redirected to another page. In order to allow the POST page to give the user +some kind of feedback, we have the getMessage and setMessage functions. It’s a +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 +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 +CSS for all pages. We’ll use the Lucius template language to create the latter.

+
        pc <- widgetToPageContent $ do
+            toWidget [lucius|
+body {
+    width: 760px;
+    margin: 1em auto;
+    font-family: sans-serif;
+}
+textarea {
+    width: 400px;
+    height: 200px;
+}
+#message {
+  color: #900;
+}
+|]
+            inside
+

And finally we’ll use a new Hamlet template to wrap up the individual +components (title, head data and body data) into the final output.

+
        withUrlRenderer [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <div #message>#{msg}
+        ^{pageBody pc}
+|]
+

This is a simple function to check if a user is the admin. In a real +application, we would likely store the admin bit in the database itself, or +check with some external system. For now, I’ve just hard-coded my own email +address.

+
isAdmin :: User -> Bool
+isAdmin user = userEmail user == "michael@snoyman.com"
+

In order to access the database, we need to create a YesodPersist instance, +which says which backend we’re using and how to run an action.

+
instance YesodPersist Blog where
+   type YesodPersistBackend Blog = SqlBackend
+   runDB f = do
+       master <- getYesod
+       let pool = connPool master
+       runSqlPool f pool
+

This is a convenience synonym. It is defined automatically for you in the +scaffolding.

+
type Form x = Html -> MForm Handler (FormResult x, Widget)
+

In order to use yesod-form and yesod-auth, we need an instance of RenderMessage +for FormMessage. This allows us to control the i18n of individual form +messages.

+
instance RenderMessage Blog FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

In order to use the built-in nic HTML editor, we need this instance. We just +take the default values, which use a CDN-hosted version of Nic.

+
instance YesodNic Blog
+

In order to use yesod-auth, we need a YesodAuth instance.

+
instance YesodAuth Blog where
+    type AuthId Blog = UserId
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+

We’ll use external OpenId providers to authenticate our users and request +email addresses to use as user id. This makes it easy to switch to other +systems in the future, locally authenticated email addresses (also included +with yesod-auth).

+
    authPlugins _ = [authOpenId Claimed
+                        [ ("openid.ns.ax", "http://openid.net/srv/ax/1.0")
+                        , ("openid.ax.mode", "fetch_request")
+                        , ("openid.ax.type.email",
+                           "http://axschema.org/contact/email")
+                        , ("openid.ax.required", "email")
+                        ]
+                    ]
+

This function takes someone’s login credentials (including his/her email address) +and gives back a UserId.

+
    getAuthId creds =
+      -- Key name for email value may vary between providers
+      let emailKey = "openid.ax.value.email" in
+      case lookup emailKey (credsExtra creds) of
+          Just email -> do
+              res <- liftHandler $ runDB $ insertBy (User email)
+              return $ Just $ either entityKey id res
+          Nothing -> return Nothing
+

We also need to provide a YesodAuthPersist instance to work with Persistent.

+
instance YesodAuthPersist Blog
+

Homepage handler. The one important detail here is our usage of setTitleI, +which allows us to use i18n messages for the title. We also use this message +with a _{Msg…} interpolation in Hamlet.

+
getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitleI MsgHomepageTitle
+    [whamlet|
+<p>_{MsgWelcomeHomepage}
+<p>
+   <a href=@{BlogR}>_{MsgSeeArchive}
+|]
+

Define a form for adding new entries. We want the user to provide the title and +content, and then fill in the post date automatically via getCurrentTime.

+

Note that slightly strange lift (liftIO getCurrentTime) manner of running an +IO action. The reason is that applicative forms are not monads, and therefore +cannot be instances of MonadIO. Instead, we use lift to run the action in +the underlying Handler monad, and liftIO to convert the IO action into a +Handler action.

+
entryForm :: Form Entry
+entryForm = renderDivs $ Entry
+    <$> areq textField (fieldSettingsLabel MsgNewEntryTitle) Nothing
+    <*> lift (liftIO getCurrentTime)
+    <*> areq nicHtmlField (fieldSettingsLabel MsgNewEntryContent) Nothing
+

Get the list of all blog entries, and present an admin with a form to create a +new entry.

+
getBlogR :: Handler Html
+getBlogR = do
+    muser <- maybeAuth
+    entries <- runDB $ selectList [] [Desc EntryPosted]
+    (entryWidget, enctype) <- generateFormPost entryForm
+    defaultLayout $ do
+        setTitleI MsgBlogArchiveTitle
+        [whamlet|
+$if null entries
+    <p>_{MsgNoEntries}
+$else
+    <ul>
+        $forall Entity entryId entry <- entries
+            <li>
+                <a href=@{EntryR entryId}>#{entryTitle entry}
+

We have three possibilities: the user is logged in as an admin, the user is +logged in and is not an admin, and the user is not logged in. In the first +case, we should display the entry form. In the second, we’ll do nothing. In the +third, we’ll provide a login link.

+
$maybe Entity _ user <- muser
+    $if isAdmin user
+        <form method=post enctype=#{enctype}>
+            ^{entryWidget}
+            <div>
+                <input type=submit value=_{MsgNewEntry}>
+$nothing
+    <p>
+        <a href=@{AuthR LoginR}>_{MsgLoginToPost}
+|]
+

Process an incoming entry addition. We don’t do any permissions checking, since +isAuthorized handles it for us. If the form submission was valid, we add the +entry to the database and redirect to the new entry. Otherwise, we ask the user +to try again.

+
postBlogR :: Handler Html
+postBlogR = do
+    ((res, entryWidget), enctype) <- runFormPost entryForm
+    case res of
+        FormSuccess entry -> do
+            entryId <- runDB $ insert entry
+            setMessageI $ MsgEntryCreated $ entryTitle entry
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectEntry
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{entryWidget}
+    <div>
+        <input type=submit value=_{MsgNewEntry}>
+|]
+

A form for comments, very similar to our entryForm above. It takes the +EntryId of the entry the comment is attached to. By using pure, we embed +this value in the resulting Comment output, without having it appear in the +generated HTML.

+
commentForm :: EntryId -> Form Comment
+commentForm entryId = renderDivs $ Comment
+    <$> pure entryId
+    <*> lift (liftIO getCurrentTime)
+    <*> lift requireAuthId
+    <*> areq textField (fieldSettingsLabel MsgCommentName) Nothing
+    <*> areq textareaField (fieldSettingsLabel MsgCommentText) Nothing
+

Show an individual entry, comments, and an add comment form if the user is +logged in.

+
getEntryR :: EntryId -> Handler Html
+getEntryR entryId = do
+    (entry, comments) <- runDB $ do
+        entry <- get404 entryId
+        comments <- selectList [CommentEntry ==. entryId] [Asc CommentPosted]
+        return (entry, map entityVal comments)
+    muser <- maybeAuth
+    (commentWidget, enctype) <-
+        generateFormPost (commentForm entryId)
+    defaultLayout $ do
+        setTitleI $ MsgEntryTitle $ entryTitle entry
+        [whamlet|
+<h1>#{entryTitle entry}
+<article>#{entryContent entry}
+    <section .comments>
+        <h1>_{MsgCommentsHeading}
+        $if null comments
+            <p>_{MsgNoComments}
+        $else
+            $forall Comment _entry posted _user name text <- comments
+                <div .comment>
+                    <span .by>#{name}
+                    <span .at>#{show posted}
+                    <div .content>#{text}
+        <section>
+            <h1>_{MsgAddCommentHeading}
+            $maybe _ <- muser
+                <form method=post enctype=#{enctype}>
+                    ^{commentWidget}
+                    <div>
+                        <input type=submit value=_{MsgAddCommentButton}>
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>_{MsgLoginToComment}
+|]
+

Receive an incoming comment submission.

+
postEntryR :: EntryId -> Handler Html
+postEntryR entryId = do
+    ((res, commentWidget), enctype) <-
+        runFormPost (commentForm entryId)
+    case res of
+        FormSuccess comment -> do
+            _ <- runDB $ insert comment
+            setMessageI MsgCommentAdded
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectComment
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{commentWidget}
+    <div>
+        <input type=submit value=_{MsgAddCommentButton}>
+|]
+

Finally our main function.

+
main :: IO ()
+main = do
+    pool <- runStdoutLoggingT $ createSqlitePool "blog.db3" 10 -- create a new pool
+    -- perform any necessary migration
+    runSqlPersistMPool (runMigration migrateAll) pool
+    manager <- newManager tlsManagerSettings -- create a new HTTP manager
+    warp 3000 $ Blog pool manager -- start our server
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/case-study-sphinx.html b/public/book-1.6/case-study-sphinx.html new file mode 100644 index 00000000..b45899c6 --- /dev/null +++ b/public/book-1.6/case-study-sphinx.html @@ -0,0 +1,790 @@ + Case Study: Sphinx-based Search :: Yesod Web Framework Book- Version 1.6 +
+

Case Study: Sphinx-based Search

+ + +

Sphinx is a search server, and powers the +search feature on many sites. While the actual code necessary to integrate +Yesod with Sphinx is relatively short, it touches on a number of complicated +topics, and is therefore a great case study in how to play with some of the +under-the-surface details of Yesod.

+

There are essentially three different pieces at play here:

+
    +
  • +

    +Storing the content we wish to search. This is fairly straight-forward + Persistent code, and we won’t dwell on it much in this chapter. +

    +
  • +
  • +

    +Accessing Sphinx search results from inside Yesod. Thanks to the sphinx + package, this is actually very easy. +

    +
  • +
  • +

    +Providing the document content to Sphinx. This is where the interesting stuff + happens, and will show how to deal with streaming content from a database + directly to XML, which gets sent directly over the wire to the client. +

    +
  • +
+

The full code for this example can be +found +on FP Haskell Center.

+
+

Sphinx Setup

+

Unlike many of our other examples, to start with here we’ll need to actually +configure and run our external Sphinx server. I’m not going to go into all the +details of Sphinx, partly because it’s not relevant to our point here, and +mostly because I’m not an expert on Sphinx.

+

Sphinx provides three main command line utilities: searchd is the actual +search daemon that receives requests from the client (in this case, our web +app) and returns the search results. indexer parses the set of documents and +creates the search index. search is a debugging utility that will run simple +queries against Sphinx.

+

There are two important settings: the source and the index. The source tells +Sphinx where to read document information from. It has direct support for MySQL +and PostgreSQL, as well as a more general XML format known as xmlpipe2. We’re +going to use the last one. This not only will give us more flexibility with +choosing Persistent backends, but will also demonstrate some more powerful +Yesod concepts.

+

The second setting is the index. Sphinx can handle multiple indices +simultaneously, which allows it to provide search for multiple services at +once. Each index will have a source it pulls from.

+

In our case, we’re going to provide a URL from our application +(/search/xmlpipe) that provides the XML file required by Sphinx, and then pipe +that through to the indexer. So we’ll add the following to our Sphinx config +file:

+
source searcher_src
+{
+        type = xmlpipe2
+        xmlpipe_command = curl http://localhost:3000/search/xmlpipe
+}
+
+index searcher
+{
+        source = searcher_src
+        path = /var/data/searcher
+        docinfo = extern
+        charset_type = utf-8
+}
+
+searchd
+{
+        listen                  = 9312
+        pid_file                = /var/run/sphinxsearch/searchd.pid
+}
+

In order to build your search index, you would run indexer searcher. +Obviously this won’t work until you have your web app running. For a production +site, it would make sense to run this command via a cron job so the index +is regularly updated.

+
+
+

Basic Yesod Setup

+

Let’s get our basic Yesod setup going. We’re going to have a single table in +the database for holding documents, which consist of a title and content. We’ll +store this in a SQLite database, and provide routes for searching, adding +documents, viewing documents and providing the xmlpipe file to Sphinx.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlBackend
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where -- see below
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

Hopefully all of this looks pretty familiar by now. The one new thing we’ve +defined here is an instance of YesodPersistRunner. This is a typeclass +necessary for creating streaming database responses. The default implementation +(defaultGetDBRunner) is almost always appropriate.

+

Next we’ll define some forms: one for creating documents, and one for searching:

+
addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+

The True parameter to searchField makes the field auto-focus on page load. +Finally, we have some standard handlers for the homepage (shows the add +document form and the search form), the document display, and adding a +document.

+
getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+
+

Searching

+

Now that we’ve got the boring stuff out of the way, let’s jump into the actual +searching. We’re going to need three pieces of information for displaying a +result: the document ID it comes from, the title of that document, and the +excerpts. Excerpts are the highlighted portions of the document which contain +the search term.

+

Search Result

+ + + + + + +
+

So let’s start off by defining a Result datatype:

+
data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+

Next we’ll look at the search handler:

+
getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+

Nothing magical here, we’re just relying on the searchForm defined above, and +the getResults function which hasn’t been defined yet. This function just +takes a search string, and returns a list of results. This is where we first +interact with the Sphinx API. We’ll be using two functions: query will return +a list of matches, and buildExcerpts will return the highlighted excerpts. +Let’s first look at getResults:

+
getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (toSqlKey . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+

query takes three parameters: the configuration options, the index to search +against (searcher in this case) and the search string. It returns a list of +document IDs that contain the search string. The tricky bit here is that those +documents are returned as Int64 values, whereas we need DocIds. +Fortunately, for the SQL Persist backends, we can just use the toSqlKey +function to perform the conversion.

+ +

We then loop over the resulting IDs to get a [Maybe Result] value, and use +catMaybes to turn it into a [Result]. In the where clause, we define our +local settings, which override the default port and set up the search to work +when any term matches the document.

+

Let’s finally look at the getResult function:

+
getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [escape $ docContent doc]
+        "searcher"
+        qstring
+    let excerpt =
+            case excerpt' of
+                ST.Ok texts -> preEscapedToHtml $ mconcat texts
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+

buildExcerpts takes four parameters: the configuration options, the textual +contents of the document, the search index and the search term. The interesting +bit is that we entity escape the text content. Sphinx won’t automatically +escape these for us, so we must do it explicitly.

+

Similarly, the result from Sphinx is a list of Texts. But of course, we’d +rather have Html. So we concat that list into a single Text and use +preEscapedToHtml to make sure that the tags inserted for matches are not +escaped. A sample of this HTML is:

+
&#8230; Departments.  The President shall have <span class='match'>Power</span> to fill up all Vacancies
+&#8230;  people. Amendment 11 The Judicial <span class='match'>power</span> of the United States shall
+&#8230; jurisdiction. 2. Congress shall have <span class='match'>power</span> to enforce this article by
+&#8230; 5. The Congress shall have <span class='match'>power</span> to enforce, by appropriate legislation
+&#8230;
+
+
+

Streaming xmlpipe output

+

We’ve saved the best for last. For the majority of Yesod handlers, the +recommended approach is to load up the database results into memory and then +produce the output document based on that. It’s simpler to work with, but more +importantly it’s more resilient to exceptions. If there’s a problem loading the +data from the database, the user will get a proper 500 response code.

+ +

However, generating the xmlpipe output is a perfect example of the alternative. +There are potentially a huge number of documents, and documents could easily be +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: +a content type, and a conduit Source providing a stream of blaze-builder +Builders. 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.

+

Now we know we want to create a stream of Builders from some XML content. +Fortunately, the xml-conduit package provides this interface directly. +xml-conduit provides some high-level interfaces for dealing with documents as +a whole, but in our case, we’re going to need to use the low-level Event +interface to ensure minimal memory impact. So the function we’re interested in +is:

+
renderBuilder :: Monad m => RenderSettings -> Conduit Event m Builder
+

In plain English, that means renderBuilder takes some settings (we’ll just use +the defaults), and will then convert a stream of Events to a stream of +Builders. This is looking pretty good, all we need now is a stream of +Events.

+

Speaking of which, what should our XML document actually look like? It’s pretty +simple, we have a sphinx:docset root element, a sphinx:schema element +containing a single sphinx:field (which defines the content field), and then +a sphinx:document for each document in our database. That last element will +have an id attribute and a child content element. Below is an example of +such a document:

+
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">
+    <sphinx:schema>
+        <sphinx:field name="content"/>
+    </sphinx:schema>
+    <sphinx:document id="1">
+        <content>bar</content>
+    </sphinx:document>
+    <sphinx:document id="2">
+        <content>foo bar baz</content>
+    </sphinx:document>
+</sphinx:docset>
+ +

Every document is going to start off with the same events (start the docset, +start the schema, etc) and end with the same event (end the docset). We’ll +start off by defining those:

+
toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+

Now that we have the shell of our document, we need to get the Events for +each individual document. This is actually a fairly simple function:

+
entityToEvents :: Entity Doc -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+

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.

+

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 +also provides the (more powerful) selectSource function. So we end up with +the function:

+
docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+

The $= operator joins together a source and a conduit into a new source. 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 +piece of cake:

+
fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+

Now we need to tie it together in getXmlpipeR. To do so, we’ll use the respondSourceDB function mentioned earlier. The last trick we need to do is convert our stream of Events into a stream of Chunk Builders. Converting to a stream of Builders is achieved with renderBuilder, and finally we’ll just wrap each Builder in its own Chunk:

+
getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+
+

Full code

+
{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# 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)
+import qualified Text.Search.Sphinx                      as S
+import qualified Text.Search.Sphinx.ExcerptConfiguration as E
+import qualified Text.Search.Sphinx.Types                as ST
+import           Text.XML.Stream.Render                  (def, renderBuilder)
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlBackend
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+
+getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [escape $ docContent doc]
+        "searcher"
+        qstring
+    let excerpt =
+            case excerpt' of
+                ST.Ok texts -> preEscapedToHtml $ mconcat texts
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+
+getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (toSqlKey . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+
+getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+
+fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+
+docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+
+toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+
+main :: IO ()
+main = runStdoutLoggingT $ withSqlitePool "searcher.db3" 10 $ \pool -> liftIO $ do
+    runSqlPool (runMigration migrateAll) pool
+    warp 3000 $ Searcher pool
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/creating-a-subsite.html b/public/book-1.6/creating-a-subsite.html new file mode 100644 index 00000000..c5c15e17 --- /dev/null +++ b/public/book-1.6/creating-a-subsite.html @@ -0,0 +1,225 @@ + Creating a Subsite :: Yesod Web Framework Book- Version 1.6 +
+

Creating a Subsite

+ + +

How many sites provide authentication systems? Or need to provide +create-read-update-delete (CRUD) management of some objects? Or a blog? Or a +wiki?

+

The theme here is that many websites include common components that can be +reused throughout multiple sites. However, it is often quite difficult to get +code to be modular enough to be truly plug-and-play: a component will require +hooks into the routing system, usually for multiple routes, and will need some +way of sharing styling information with the master site.

+

In Yesod, the solution is subsites. A subsite is a collection of routes and +their handlers that can be easily inserted into a master site. By using type +classes, it is easy to ensure that the master site provides certain +capabilities, and to access the default site layout. And with type-safe URLs, +it’s easy to link from the master site to subsites.

+
+

Hello World

+

Perhaps the trickiest part of writing subsites is getting started. Let’s dive +in with a simple Hello World subsite. We need to create one module to contain +our subsite’s data types, another for the subsite’s dispatch code, and then a +final module for an application that uses the subsite.

+ +
-- @HelloSub/Data.hs
+{-# LANGUAGE QuasiQuotes     #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies    #-}
+module HelloSub.Data where
+
+import           Yesod
+
+-- Subsites have foundations just like master sites.
+data HelloSub = HelloSub
+
+-- We have a familiar analogue from mkYesod, with just one extra parameter.
+-- We'll discuss that later.
+mkYesodSubData "HelloSub" [parseRoutes|
+/ SubHomeR GET
+|]
+
-- @HelloSub.hs
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+module HelloSub
+    ( module HelloSub.Data
+    , module HelloSub
+    ) where
+
+import           HelloSub.Data
+import           Yesod
+
+-- And we'll spell out the handler type signature.
+getSubHomeR :: Yesod master => SubHandlerFor HelloSub master Html
+getSubHomeR = liftHandler $ defaultLayout [whamlet|Welcome to the subsite!|]
+
+instance Yesod master => YesodSubDispatch HelloSub master where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesHelloSub)
+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           HelloSub
+import           Yesod
+
+-- And let's create a master site that calls it.
+data Master = Master
+    { getHelloSub :: HelloSub
+    }
+
+mkYesod "Master" [parseRoutes|
+/ HomeR GET
+/subsite SubsiteR HelloSub getHelloSub
+|]
+
+instance Yesod Master
+
+-- Spelling out type signature again.
+getHomeR :: HandlerFor Master Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>Welcome to the homepage
+        <p>
+            Feel free to visit the #
+            <a href=@{SubsiteR SubHomeR}>subsite
+            \ as well.
+    |]
+
+main = warp 3000 $ Master HelloSub
+

This simple example actually shows most of the complications involved in +creating a subsite. Like a normal Yesod application, everything in a subsite is +centered around a foundation datatype, HelloSub in our case. We then use +mkYesodSubData to generate our subsite route data type and associated parse +and render functions.

+

On the dispatch side, we start off by defining our handler function for the SubHomeR route. You should pay special attention to the type signature on this function:

+
getSubHomeR :: Yesod master
+            => SubHandlerFor HelloSub master Html
+

This is the heart and soul of what a subsite is all about. All of our actions +live in this layered monad, where we have our subsite wrapping around our main +site. Given this monadic layering, it should come as no surprise that we end up +calling liftHandler. In this case, our subsite is using the master site’s +defaultLayout function to render a widget.

+

The defaultLayout function is part of the Yesod typeclass. Therefore, in +order to call it, the master type argument must be an instance of Yesod. +The advantage of this approach is that any modifications to the master site’s +defaultLayout method will automatically be reflected in subsites.

+

When we embed a subsite in our master site route definition, we need to specify +four pieces of information: the route to use as the base of the subsite (in +this case, /subsite), the constructor for the subsite routes (SubsiteR), +the subsite foundation data type (HelloSub) and a function that takes a +master foundation value and returns a subsite foundation value (getHelloSub).

+

In the definition of getHomeR, we can see how the route constructor gets used. +In a sense, SubsiteR promotes any subsite route to a master site route, +making it possible to safely link to it from any master site template.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/deploying-your-webapp.html b/public/book-1.6/deploying-your-webapp.html new file mode 100644 index 00000000..e1464a7a --- /dev/null +++ b/public/book-1.6/deploying-your-webapp.html @@ -0,0 +1,581 @@ + Deploying your Webapp :: Yesod Web Framework Book- Version 1.6 +
+

Deploying your Webapp

+ + +

I can’t speak for others, but I personally prefer programming to system +administration. But the fact is that, eventually, you need to serve your app +somehow, and odds are that you’ll need to be the one to set it up.

+

There are some promising initiatives in the Haskell web community towards +making deployment easier. In the future, we may even have a service that allows +you to deploy your app with a single command.

+

But we’re not there yet. And even if we were, such a solution will never work +for everyone. This chapter covers the different options you have for +deployment, and gives some general recommendations on what you should choose in +different situations.

+

The Yesod team strongly recommends using the stack build tool as discussed in +the quick start guide for Yesod +development, check out the quick start if you haven’t already.

+
+

Keter

+

The Yesod scaffolding comes with some built-in support for the Keter deployment +engine, which is also written in Haskell and uses many of the same underlying +technologies like WAI and http-client. Keter works as a reverse proxy to your +applications, as well as a system for starting, monitoring, and redeploying +running apps. If you’d like to deploy with Keter, follow these steps:

+
    +
  1. +

    +Edit the config/keter.yaml file in your scaffolded application as necessary. +

    +
  2. +
  3. +

    +Set up some kind of server for hosting your apps. I recommend trying Ubuntu on Amazon EC2. +

    +
  4. +
  5. +

    +Install Keter on that machine. Please follow the instructions on the Keter website, which will be the most up to date. +

    +
  6. +
  7. +

    +Run yesod keter to generate a Keter bundle, e.g., myapp.keter. +

    +
  8. +
  9. +

    +Copy myapp.keter to the /opt/keter/incoming directory on your server. +

    +
  10. +
+

If you’ve got things configured correctly, you should now be able to view +your website, running in a production environment! In the future, upgrades can +be handled by simply rerunning yesod keter and recopying the myapp.keter +bundle to the server. Note that Keter will automatically detect the presence of +the new file and reload your application.

+

The rest of this chapter will go into some more details about various steps, and +provide some alternatives for people looking to either not use the scaffolding +or not use Keter.

+
+
+

Compiling

+

The biggest advice I can give is: don’t compile on your server. It’s tempting to do so, as you have to just transfer source code around, and you avoid confusing dependency issues. However, compiling a Yesod application takes significant memory and CPU, which means:

+
    +
  • +

    +While you’re recompiling your app, your existing application will suffer performance-wise. +

    +
  • +
  • +

    +You will need to get a much larger machine to handle compilation, and that capacity will likely sit idle most of the time, since Yesod applications tend to require far less CPU and memory than GHC itself. +

    +
  • +
+

Once you’re ready to compile, you should always make sure to stack clean +before a new production build, to make sure no old files are lying around. +Then, you can run stack build to get an executable, which +will be located at dist/build/myapp/myapp.

+
+
+

Files to deploy

+

With a Yesod scaffolded application, there are essentially three sets of files that need +to be deployed:

+
    +
  1. +

    +Your executable. +

    +
  2. +
  3. +

    +The config folder. +

    +
  4. +
  5. +

    +The static folder. +

    +
  6. +
+

Everything else, such as Shakespearean templates, gets compiled into the +executable itself.

+

There is one caveat, however: the config/client_session_key.aes file. This +file controls the server side encryption used for securing client side session +cookies. Yesod will automatically generate a new one of these keys if none is +present. In practice, this means that, if you do not include this file in +deployment, all of your users will have to log in again when you redeploy. If +you follow the advice above and include the config folder, this issue will be +partially resolved. Another approach is to +put +your session key in an environment variable.

+

The other half of the resolution is to ensure that once you generate a +config/client_session_key.aes file, you keep the same one for all future +deployments. The simplest way to ensure this is to keep that file in your +version control. However, if your version control is open source, this will be +dangerous: anyone with access to your repository will be able to spoof login +credentials!

+

The problem described here is essentially one of system administration, not +programming. Yesod does not provide any built-in approach for securely storing +client session keys. If you have an open source repository, or do not trust +everyone who has access to your source code repository, it’s vital to figure +out a safe storage solution for the client session key.

+
+
+

SSL and static files

+

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 +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:

+
    +
  • +

    +Serve your static files over HTTPS as well. +

    +
  • +
  • +

    +Serve your static files from the same domain name as your main site. +

    +
  • +
+

Note that if you go for the first option (which is the better one), you’ll +either need two separate SSL certificates, or a wildcard certificate.

+
+
+

Warp

+

As we have mentioned before, Yesod is built on the Web Application Interface +(WAI), allowing it to run on any WAI backend. At the time of writing, the +following backends are available:

+
    +
  • +

    +Warp +

    +
  • +
  • +

    +FastCGI +

    +
  • +
  • +

    +SCGI +

    +
  • +
  • +

    +CGI +

    +
  • +
  • +

    +Webkit +

    +
  • +
  • +

    +Development server +

    +
  • +
+

The last two are not intended for production deployments. Of the remaining +four, all can be used for production deployment in theory. In practice, a CGI +backend will likely be horribly inefficient, since a new process must be +spawned for each connection. And SCGI is not nearly as well supported by +frontend web servers as Warp (via reverse proxying) or FastCGI.

+

So between the two remaining choices, Warp gets a very strong recommendation +because:

+
    +
  • +

    +It is significantly faster. +

    +
  • +
  • +

    +Like FastCGI, it can run behind a frontend server like Nginx, using reverse + HTTP proxy. +

    +
  • +
  • +

    +In addition, it is a fully capable server of its own accord, and can + therefore be used without any frontend server. +

    +
  • +
+

So that leaves one last question: should Warp run on its own, or via reverse +proxy behind a frontend server? For most use cases, I recommend the latter, +because:

+
    +
  • +

    +Having a reverse proxy in front of your app makes it easier to deploy new versions. +

    +
  • +
  • +

    +Also, if you have a bug in your application, a reverse proxy can give slightly nicer error messages to users. +

    +
  • +
  • +

    +You can host multiple applications from a single host via virtual hosting. +

    +
  • +
  • +

    +Your reverse proxy can function as both a load balancer or SSL proxy as well, simplifying your application. +

    +
  • +
+

As discussed above, Keter is a great way to get started. If you have an +existing web server running like Nginx, Yesod will work just fine sitting +behind it instead.

+
+

Nginx Configuration

+

Keter configuration is trivial, since it is designed to work with Yesod +applications. But if you want to instead use Nginx, how do you set it up?

+

In general, Nginx will listen on port 80 and your Yesod/Warp app will listen on +some unprivileged port (let’s say 4321). You will then need to provide a +nginx.conf file, such as:

+
daemon off; # Don't run nginx in the background, good for monitoring apps
+events {
+    worker_connections 4096;
+}
+
+http {
+    server {
+        listen 80; # Incoming port for Nginx
+        server_name www.myserver.com;
+        location / {
+            proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+        }
+    }
+}
+

You can add as many server blocks as you like. A common addition is to ensure +users always access your pages with the www prefix on the domain name, ensuring +the RESTful principle of canonical URLs. (You could just as easily do the +opposite and always strip the www, just make sure that your choice is reflected +in both the nginx config and the approot of your site.) In this case, we would +add the block:

+
server {
+    listen 80;
+    server_name myserver.com;
+    rewrite ^/(.*) http://www.myserver.com/$1 permanent;
+}
+

A highly recommended optimization is to serve static files from a separate +domain name, therefore bypassing the cookie transfer overhead. Assuming that +our static files are stored in the static folder within our site folder, and +the site folder is located at /home/michael/sites/mysite, this would look +like:

+
server {
+    listen 80;
+    server_name static.myserver.com;
+    root /home/michael/sites/mysite/static;
+    # Since yesod-static appends a content hash in the query string,
+    # we are free to set expiration dates far in the future without
+    # concerns of stale content.
+    expires max;
+}
+

In order for this to work, your site must properly rewrite static URLs to this +alternate domain name. The scaffolded site is set up to make this fairly simple +via the Settings.staticRoot function and the definition of +urlRenderOverride. However, if you just want to get the benefit of nginx’s +faster static file serving without dealing with separate domain names, you can +instead modify your original server block like so:

+
server {
+    listen 80; # Incoming port for Nginx
+    server_name www.myserver.com;
+    location / {
+        proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+    }
+    location /static {
+        root /home/michael/sites/mysite; # Notice that we do *not* include /static
+        expires max;
+    }
+}
+
+
+

Server Process

+

Many people are familiar with an Apache/mod_php or Lighttpd/FastCGI kind of +setup, where the web server automatically spawns the web application. With +nginx, either for reverse proxying or FastCGI, this is not the case: you are +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 +/etc/systemd/system/yesod-sample.service:

+
[Service]
+ExecStart=/home/sibi/.local/bin/my-yesod-executable
+Restart=always
+StandardOutput=syslog
+StandardError=syslog
+SyslogIdentifier=yesod-sample
+
+[Install]
+WantedBy=multi-user.target
+

Now you can start your service with:

+
systemctl enable yesod-sample
+systemctl start yesod-sample
+

You can also see the status of your process using systemctl status +yesod-sample.

+
+
+
+

Nginx + FastCGI

+

Some people may prefer using FastCGI for deployment. In this case, you’ll need +to add an extra tool to the mix. FastCGI works by receiving new connection from +a file descriptor. The C library assumes that this file descriptor will be 0 +(standard input), so you need to use the spawn-fcgi program to bind your +application’s standard input to the correct socket.

+

It can be very convenient to use Unix named sockets for this instead of binding +to a port, especially when hosting multiple applications on a single host. A +possible script to load up your app could be:

+
spawn-fcgi \
+    -d /home/michael/sites/mysite \
+    -s /tmp/mysite.socket \
+    -n \
+    -M 511 \
+    -u michael \
+    -- /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi
+

You will also need to configure your frontend server to speak to your app over +FastCGI. This is relatively painless in Nginx:

+
server {
+    listen 80;
+    server_name www.myserver.com;
+    location / {
+        fastcgi_pass unix:/tmp/mysite.socket;
+    }
+}
+

That should look pretty familiar from above. The only last trick is that, with +Nginx, you need to manually specify all of the FastCGI variables. It is +recommended to store these in a separate file (say, fastcgi.conf) and then add +include fastcgi.conf; to the end of your http block. The contents of the +file, to work with WAI, should be:

+
fastcgi_param  QUERY_STRING       $query_string;
+fastcgi_param  REQUEST_METHOD     $request_method;
+fastcgi_param  CONTENT_TYPE       $content_type;
+fastcgi_param  CONTENT_LENGTH     $content_length;
+fastcgi_param  PATH_INFO          $fastcgi_script_name;
+fastcgi_param  SERVER_PROTOCOL    $server_protocol;
+fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
+fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
+fastcgi_param  REMOTE_ADDR        $remote_addr;
+fastcgi_param  SERVER_ADDR        $server_addr;
+fastcgi_param  SERVER_PORT        $server_port;
+fastcgi_param  SERVER_NAME        $server_name;
+
+
+

Desktop

+

Another nifty backend is wai-handler-webkit. This backend combines Warp and +QtWebkit to create an executable that a user simply double-clicks. This can be +a convenient way to provide an offline version of your application.

+

One of the very nice conveniences of Yesod for this is that your templates are +all compiled into the executable, and thus do not need to be distributed with +your application. Static files do, however.

+ +

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 +doesn’t receive a ping for two minutes, it shuts down.

+
+
+

CGI on Apache

+

CGI and FastCGI work almost identically on Apache, so it should be fairly +straight-forward to port this configuration. You essentially need to accomplish +two goals:

+
    +
  1. +

    +Get the server to serve your file as (Fast)CGI. +

    +
  2. +
  3. +

    +Rewrite all requests to your site to go through the (Fast)CGI executable. +

    +
  4. +
+

Here is a configuration file for serving a blog application, with an executable +named "bloggy.cgi", living in a subfolder named "blog" of the document root. +This example was taken from an application living in the path +/f5/snoyman/public/blog.

+
Options +ExecCGI
+AddHandler cgi-script .cgi
+Options +FollowSymlinks
+
+RewriteEngine On
+RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1]
+RewriteCond $1 !^bloggy.cgi
+RewriteCond $1 !^static/
+RewriteRule ^(.*) bloggy.cgi/$1 [L]
+

The first RewriteRule is to deal with subfolders. In particular, it redirects a +request for /blog to /blog/. The first RewriteCond prevents directly +requesting the executable, the second allows Apache to serve the static files, +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." +when absent and serves static files from a separate domain. However, this +should serve to show the basics.

+

Here, "/home/michael/fastcgi" is the fastcgi application. The idea is to +rewrite all requests to start with "/app", and then serve everything beginning +with "/app" via the FastCGI executable.

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_fastcgi", "mod_rewrite")
+
+url.rewrite-once = (
+  "(.*)" => "/app/$1"
+)
+
+fastcgi.server = (
+    "/app" => ((
+        "socket" => "/tmp/test.fastcgi.socket",
+        "check-local" => "disable",
+        "bin-path" => "/home/michael/fastcgi", # full path to executable
+        "min-procs" => 1,
+        "max-procs" => 30,
+        "idle-timeout" => 30
+    ))
+)
+
+
+

CGI on lighttpd

+

This is basically the same as the FastCGI version, but tells lighttpd to run a +file ending in ".cgi" as a CGI executable. In this case, the file lives at +"/home/michael/myapp.cgi".

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_cgi", "mod_rewrite")
+
+url.rewrite-once = (
+    "(.*)" => "/myapp.cgi/$1"
+)
+
+cgi.assign = (".cgi" => "")
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/environment-variables.html b/public/book-1.6/environment-variables.html new file mode 100644 index 00000000..2ce0d421 --- /dev/null +++ b/public/book-1.6/environment-variables.html @@ -0,0 +1,171 @@ + Environment variables for configuration :: Yesod Web Framework Book- Version 1.6 +
+

Environment variables for configuration

+ + +

There’s a recent move, perhaps most prominently advocated by +the twelve-factor app, to store all app +configuration in environment variables, instead of using config files or +hard-coding them into the application source code (you don’t do that, right?).

+

Yesod’s scaffolding comes built in with some support for this, most +specifically for respecting the YESOD_APPROOT environment variable to indicate how +URLs should be generated, the YESOD_PORT environment variable for which port to +listen for requests on, and database connection settings. (Incidentally, this +ties in nicely with the Keter deployment +manager.)

+

The technique for doing this is quite easy: just do the environment variable +lookup in your main function. This example demonstrates this technique, along +with the slightly special handling necessary for setting the application root.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text          (Text, pack)
+import           System.Environment
+import           Yesod
+
+data App = App
+    { myApproot      :: Text
+    , welcomeMessage :: Text
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    approot = ApprootMaster myApproot
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    App {..} <- getYesod
+    setTitle "Environment variables"
+    [whamlet|
+        <p>Here's the welcome message: #{welcomeMessage}
+        <p>
+            <a href=@{HomeR}>And a link to: @{HomeR}
+    |]
+
+main :: IO ()
+main = do
+    myApproot <- fmap pack $ getEnv "YESOD_APPROOT"
+    welcomeMessage <- fmap pack $ getEnv "WELCOME_MESSAGE"
+    warp 3000 App {..}
+

The only tricky things here are:

+
    +
  1. +

    +You need to convert the String value returned by getEnv into a Text by using pack. +

    +
  2. +
  3. +

    +We use the ApprootMaster constructor for approot, which says "apply this function to the foundation datatype to get the actual application root." +

    +
  4. +
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/forms.html b/public/book-1.6/forms.html new file mode 100644 index 00000000..ca3dac34 --- /dev/null +++ b/public/book-1.6/forms.html @@ -0,0 +1,1127 @@ + Forms :: Yesod Web Framework Book- Version 1.6 +
+

Forms

+ + +

I’ve mentioned the boundary issue already: whenever data enters or leaves an +application, we need to validate it. Probably the most difficult place this +occurs is forms. Coding forms is complex; in an ideal world, we’d like a +solution that addresses the following problems:

+
    +
  • +

    +Ensure data is valid. +

    +
  • +
  • +

    +Marshal string data in the form submission to Haskell datatypes. +

    +
  • +
  • +

    +Generate HTML code for displaying the form. +

    +
  • +
  • +

    +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. +

    +
  • +
  • +

    +Automatically assign names to our fields that are guaranteed to be unique. +

    +
  • +
+

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 +Haskell’s type system to make sure everything is working correctly.

+
+

Synopsis

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative ((<$>), (<*>))
+import           Data.Text           (Text)
+import           Data.Time           (Day)
+import           Yesod
+import           Yesod.Form.Jquery
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/person PersonR POST
+|]
+
+instance Yesod App
+
+-- Tells our application to use the standard English messages.
+-- If you want i18n, then you can supply a translating function instead.
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- And tell us where to find the jQuery libraries. We'll just use the defaults,
+-- which point to the Google CDN.
+instance YesodJquery App
+
+-- The datatype we wish to receive from the form
+data Person = Person
+    { personName          :: Text
+    , personBirthday      :: Day
+    , personFavoriteColor :: Maybe Text
+    , personEmail         :: Text
+    , personWebsite       :: Maybe Text
+    }
+  deriving Show
+
+-- Declare the form. The type signature is a bit intimidating, but here's the
+-- overview:
+--
+-- * The Html parameter is used for encoding some extra information. See the
+-- discussion regarding runFormGet and runFormPost below for further
+-- explanation.
+--
+-- * We have our Handler as the inner monad, which indicates which site this is
+-- running in.
+--
+-- * FormResult can be in three states: FormMissing (no data available),
+-- FormFailure (invalid data) and FormSuccess
+--
+-- * The Widget is the viewable form to place into the web page.
+--
+-- Note that the scaffolded site provides a convenient Form type synonym,
+-- so that our signature could be written as:
+--
+-- > personForm :: Form Person
+--
+-- For our purposes, it's good to see the long version.
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm = renderDivs $ Person
+    <$> areq textField "Name" Nothing
+    <*> areq (jqueryDayField def
+        { jdsChangeYear = True -- give a year dropdown
+        , jdsYearRange = "1900:-5" -- 1900 till five years ago
+        }) "Birthday" Nothing
+    <*> aopt textField "Favorite color" Nothing
+    <*> areq emailField "Email address" Nothing
+    <*> aopt urlField "Website" Nothing
+
+-- The GET handler displays the form
+getHomeR :: Handler Html
+getHomeR = do
+    -- Generate the form to be displayed
+    (widget, enctype) <- generateFormPost personForm
+    defaultLayout
+        [whamlet|
+            <p>
+                The widget generated contains only the contents
+                of the form, not the form tag itself. So...
+            <form method=post action=@{PersonR} enctype=#{enctype}>
+                ^{widget}
+                <p>It also doesn't include the submit button.
+                <button>Submit
+        |]
+
+-- The POST handler processes the form. If it is successful, it displays the
+-- parsed person. Otherwise, it displays the form again with error messages.
+postPersonR :: Handler Html
+postPersonR = do
+    ((result, widget), enctype) <- runFormPost personForm
+    case result of
+        FormSuccess person -> defaultLayout [whamlet|<p>#{show person}|]
+        _ -> defaultLayout
+            [whamlet|
+                <p>Invalid input, let's try again.
+                <form method=post action=@{PersonR} enctype=#{enctype}>
+                    ^{widget}
+                    <button>Submit
+            |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Kinds of Forms

+

Before jumping into the types themselves, we should begin with an overview of +the different kinds of forms. There are three categories:

+
+
+Applicative +
+

+These are the most commonly used (it’s what appeared in the +synopsis). Applicative gives us some nice properties of letting error messages +coalesce together and keep a very high-level, declarative approach. (For more +information on applicative code, see +the Haskell +wiki.) +

+
+
+Monadic +
+

+A more powerful alternative to applicative. While this allows you +more flexibility, it does so at the cost of being more verbose. Useful if you +want to create forms that don’t fit into the standard two-column look. +

+
+
+Input +
+

+Used only for receiving input. Does not generate any HTML for receiving +the user input. Useful for interacting with existing forms. +

+
+
+

In addition, there are a number of different variables that come into play for +each form and field you will want to set up:

+
    +
  • +

    +Is the field required or optional? +

    +
  • +
  • +

    +Should it be submitted with GET or POST? +

    +
  • +
  • +

    +Does it have a default value, or not? +

    +
  • +
+

An overriding goal is to minimize the number of field definitions and let them +work in as many contexts as possible. One result of this is that we end up with +a few extra words for each field. In the synopsis, you may have noticed things +like areq and that extra Nothing parameter. We’ll cover why all of those +exist in the course of this chapter, but for now realize that by making these +parameters explicit, we are able to reuse the individuals fields (like +intField) in many different ways.

+

A quick note on naming conventions. Each form type has a one-letter prefix (A, +M and I) which is used in a few places, such as saying MForm. We also use req +and opt to mean required and optional. Combining these, we create a required +applicative field with areq, or an optional input field with iopt.

+
+
+

Types

+

The Yesod.Form.Types module declares a few types. We won’t cover all the types +available, but will instead focus on the most crucial. Let’s start with some of +the simple ones:

+
+
+Enctype +
+

+The encoding type, either UrlEncoded or Multipart. This datatype +declares an instance of ToHtml, so you can use the enctype directly in +Hamlet. +

+
+
+FormResult +
+

+Has one of three possible states: FormMissing if no data was +submitted, FormFailure if there was an error parsing the form (e.g., missing +a required field, invalid content), or FormSuccess if everything went +smoothly. +

+
+
+FormMessage +
+

+Represents all of the different messages that can be generated as +a data type. For example, MsgInvalidInteger is used by the library to +indicate that the textual value provided is not an integer. By keeping this +data highly structured, you are able to provide any kind of rendering function +you want, which allows for internationalization (i18n) of your application. +

+
+
+

Next we have some datatypes used for defining individual fields. We define a +field as a single piece of information, such as a number, a string, or an email +address. Fields are combined together to build forms.

+
+
+Field +
+

+Defines two pieces of functionality: how to parse the text input from a +user into a Haskell value, and how to create the widget to be displayed to the +user. yesod-form defines a number of individual Fields in Yesod.Form.Fields. +

+
+
+FieldSettings +
+

+Basic information on how a field should be displayed, such as +the display name, an optional tooltip, and possibly hardcoded id and name +attributes. (If none are provided, they are automatically generated.) Note that +FieldSettings provides an IsString instance, so when you need to provide a +FieldSettings value, you can actually type in a literal string. That’s how we +interacted with it in the synopsis. +

+
+
+

And finally, we get to the important stuff: the forms themselves. There are +three types for this: MForm is for monadic forms, AForm for applicative and +FormInput for input. MForm is actually a type synonym for a +monad stack that provides the following features:

+
    +
  • +

    +A Reader monad giving us the parameters submitted by the user, the + foundation datatype and the list of languages the user supports. The last two + are used for rendering of the FormMessages to support i18n (more on this + later). +

    +
  • +
  • +

    +A Writer monad keeping track of the Enctype. A form will always be + UrlEncoded, unless there is a file input field, which will force us to use + multipart instead. +

    +
  • +
  • +

    +A State monad keeping track of generated names and identifiers for fields. +

    +
  • +
+

An AForm is pretty similar. However, there are a few major differences:

+
    +
  • +

    +It produces a list of FieldViews, which are used for tracking what we + will display to the user. This allows us to keep an abstract idea of the form + display, and then at the end of the day choose an appropriate function for + laying it out on the page. In the synopsis, we used renderDivs, which + creates a bunch of div tags. Two other options are renderBootstrap and + renderTable. +

    +
  • +
  • +

    +It does not provide a Monad instance. The goal of Applicative is to allow + the entire form to run, grab as much information on each field as possible, + and then create the final result. This cannot work in the context of Monad. +

    +
  • +
+

A FormInput is even simpler: it returns either a list of error messages or a +result.

+
+
+

Converting

+

“But wait a minute,” you say. “You said the synopsis uses applicative forms, +but I’m sure the type signature said MForm. Shouldn’t it be Monadic?” That’s +true, the final form we produced was monadic. But what really happened is that +we converted an applicative form to a monadic one.

+

Again, our goal is to reuse code as much as possible, and minimize the number +of functions in the API. And Monadic forms are more powerful than Applicative, +if a bit clumsy, so anything that can be expressed in an Applicative form could +also be expressed in a Monadic form. There are two core functions that help out +with this: aformToForm converts any applicative form to a monadic one, and +formToAForm converts certain kinds of monadic forms to applicative forms.

+

“But wait another minute,” you insist. “I didn’t see any aformToForm!” +Also true. The renderDivs function takes care of that for us.

+
+
+

Create AForms

+

Now that I’ve (hopefully) convinced you that in our synopsis we were really +dealing with applicative forms, let’s have a look and try to understand how +these things get created. Let’s take a simple example:

+
data Car = Car
+    { carModel :: Text
+    , carYear  :: Int
+    }
+  deriving Show
+
+carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable carAForm
+

Here, we’ve explicitly split up applicative and monadic forms. In carAForm, +we use the <$> and <*> operators. This should not be surprising; these are +almost always used in applicative-style code. And we have one line for each +record in our Car datatype. Perhaps also unsurprisingly, we have a +textField for the Text record, and an intField for the Int record.

+

Let’s look a bit more closely at the areq function. Its (simplified) type +signature is Field a → FieldSettings → Maybe a → AForm a. That +first argument specifies the datatype of this field, how to parse +it, and how to render it. The next argument, FieldSettings, tells us the +label, tooltip, name and ID of the field. In this case, we’re using the +previously-mentioned IsString instance of FieldSettings.

+

And what’s up with that Maybe a? It provides the optional default value. For +example, if we want our form to fill in "2007" as the default car year, we +would use areq intField "Year" (Just 2007). We can even take this to the next +level, and have a form that takes an optional parameter giving the default +values.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+
+

Optional fields

+

Suppose we wanted to have an optional field (like the car color). All we do +instead is use the aopt function.

+
carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+    <*> aopt textField "Color" Nothing
+

And like required fields, the last argument is the optional default value. +However, this has two layers of Maybe wrapping. This is actually a bit +redundant, but it makes it much easier to write code that takes an optional +default form parameter, such as in the next example.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+    <*> aopt textField "Color" (carColor <$> mcar)
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable $ carAForm $ Just $ Car "Forte" 2010 $ Just "gray"
+
+
+
+

Validation

+

How would we make our form only accept cars created after 1990? If you +remember, we said above that the Field itself contained the information on +what is a valid entry. So all we need to do is write a new Field, right? +Well, that would be a bit tedious. Instead, let’s just modify an existing one:

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField    "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year"  (carYear  <$> mcar)
+    <*> aopt textField    "Color" (carColor <$> mcar)
+  where
+    errorMessage :: Text
+    errorMessage = "Your car is too old, get a new one!"
+
+    carYearField = check validateYear intField
+
+    validateYear y
+        | y < 1990 = Left errorMessage
+        | otherwise = Right y
+

The trick here is the check function. It takes a function (validateYear) +that returns either an error message or a modified field value. In this +example, we haven’t modified the value at all. That is usually going to be the +case. This kind of checking is very common, so we have a shortcut:

+
carYearField = checkBool (>= 1990) errorMessage intField
+

checkBool takes two parameters: a condition that must be fulfilled, and an +error message to be displayed if it was not.

+ +

It’s great to make sure the car isn’t too old. But what if we want to make sure +that the year specified is not from the future? In order to look up the current +year, we’ll need to run some IO. For such circumstances, we’ll need checkM, +which allows our validation code to perform arbitrary actions:

+
    carYearField = checkM inPast $ checkBool (>= 1990) errorMessage intField
+
+    inPast y = do
+        thisYear <- liftIO getCurrentYear
+        return $ if y <= thisYear
+            then Right y
+            else Left ("You have a time machine!" :: Text)
+
+getCurrentYear :: IO Int
+getCurrentYear = do
+    now <- getCurrentTime
+    let today = utctDay now
+    let (year, _, _) = toGregorian today
+    return $ fromInteger year
+

inPast is a function that will return an Either result in the Handler +monad. We use liftIO getCurrentYear to get the current year and then compare +it against the user-supplied year. Also, notice how we can chain together +multiple validators.

+ +
+
+

More sophisticated fields

+

Our color entry field is nice, but it’s not exactly user-friendly. What we +really want is a drop-down list.

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectFieldList colors) "Color" (carColor <$> mcar)
+  where
+    colors :: [(Text, Color)]
+    colors = [("Red", Red), ("Blue", Blue), ("Gray", Gray), ("Black", Black)]
+

selectFieldList takes a list of pairs. The first item in the pair is the text displayed to the user in the drop-down list, and the second item is the actual Haskell value. Of course, the code above looks really repetitive; we can get the same result using the Enum and Bounded instance GHC automatically derives for us.

+
colors = map (pack . show &&& id) [minBound..maxBound]
+

[minBound..maxBound] gives us a list of all the different Color values. We +then apply a map and &&& (a.k.a, the fan-out operator) to turn that into a +list of pairs. And even this can be simplified by using the optionsEnum +function provided by yesod-form, which would turn our original code into:

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectField optionsEnum) "Color" (carColor <$> mcar)
+

Some people prefer radio buttons to drop-down lists. Fortunately, this is just a one-word change.

+
carAForm = Car
+    <$> areq textField                    "Model" Nothing
+    <*> areq intField                     "Year"  Nothing
+    <*> aopt (radioField optionsEnum) "Color" Nothing
+
+
+

Running forms

+

At some point, we’re going to need to take our beautiful forms and produce some +results. There are a number of different functions available for this, each +with its own purpose. I’ll go through them, starting with the most common.

+
+
+runFormPost +
+

+This will run your form against any submitted POST parameters. +If this is not a POST submission, it will return a FormMissing. This +automatically inserts a security token as a hidden form field to avoid +cross-site request +forgery (CSRF) attacks. +

+
+
+runFormGet +
+

+The equivalent of runFormPost for GET parameters. In order to +distinguish a normal GET page load from a GET submission, it includes an +extra _hasdata hidden field in the form. Unlike runFormPost, it does +not include CSRF protection. +

+
+
+runFormPostNoToken +
+

+Same as runFormPost, but does not include (or require) +the CSRF security token. +

+
+
+generateFormPost +
+

+Instead of binding to existing POST parameters, acts as if +there are none. This can be useful when you want to generate a new form after a +previous form was submitted, such as in a wizard. +

+
+
+generateFormGet +
+

+Same as generateFormPost, but for GET. +

+
+
+

The return type from the first three is ((FormResult a, Widget), Enctype). +The Widget will already have any validation errors and previously submitted +values.

+ +
+
+

i18n

+

There have been a few references to i18n in this chapter. The topic will get +more thorough coverage in its own chapter, but since it has such a profound +effect on yesod-form, I wanted to give a brief overview. The idea behind i18n +in Yesod is to have data types represent messages. Each site can have an +instance of RenderMessage for a given datatype which will translate that +message based on a list of languages the user accepts. As a result of all this, +there are a few things you should be aware of:

+
    +
  • +

    +There is an automatic instance of RenderMessage for Text in every site, + so you can just use plain strings if you don’t care about i18n support. + However, you may need to use explicit type signatures occasionally. +

    +
  • +
  • +

    +yesod-form expresses all of its messages in terms of the FormMessage datatype. Therefore, to use yesod-form, you’ll need to have an appropriate RenderMessage instance. A simple one that uses the default English translations would be: +

    +
  • +
+
instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

This is provided automatically by the scaffolded site.

+
+
+

Monadic Forms

+

Often times, a simple form layout is adequate, and applicative forms excel at +this approach. Sometimes, however, you’ll want to have a more customized look +to your form.

+

A non-standard form layout

+ + + + + + +
+

For these use cases, monadic forms fit the bill. They are a bit more verbose +than their applicative cousins, but this verbosity allows you to have complete +control over what the form will look like. In order to generate the form above, +we could code something like this.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm extra = do
+    (nameRes, nameView) <- mreq textField "this is not used" Nothing
+    (ageRes, ageView) <- mreq intField "neither is this" Nothing
+    let personRes = Person <$> nameRes <*> ageRes
+    let widget = do
+            toWidget
+                [lucius|
+                    ##{fvId ageView} {
+                        width: 3em;
+                    }
+                |]
+            [whamlet|
+                #{extra}
+                <p>
+                    Hello, my name is #
+                    ^{fvInput nameView}
+                    \ and I am #
+                    ^{fvInput ageView}
+                    \ years old. #
+                    <input type=submit value="Introduce myself">
+            |]
+    return (personRes, widget)
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet personForm
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+        |]
+
+main :: IO ()
+main = warp 3000 App
+

Similar to the applicative areq, we use mreq for monadic forms. (And yes, +there’s also mopt for optional fields.) But there’s a big difference: mreq +gives us back a pair of values. Instead of hiding away the FieldView value and +automatically inserting it into a widget, we have the ability to insert it as +we see fit.

+

FieldView has a number of pieces of information. The most important is +fvInput, which is the actual form field. In this example, we also use fvId, +which gives us back the HTML id attribute of the input tag. In our example, +we use that to specify the width of the field.

+

You might be wondering what the story is with the “this is not used” and +“neither is this” values. mreq takes a FieldSettings as its second +argument. Since FieldSettings provides an IsString instance, the strings +are essentially expanded by the compiler to:

+
fromString "this is not used" == FieldSettings
+    { fsLabel = "this is not used"
+    , fsTooltip = Nothing
+    , fsId = Nothing
+    , fsName = Nothing
+    , fsAttrs = []
+    }
+

In the case of applicative forms, the fsLabel and fsTooltip values are used +when constructing your HTML. In the case of monadic forms, Yesod does not +generate any of the “wrapper” HTML for you, and therefore these values are +ignored. However, we still keep the FieldSettings parameter to allow you to +override the id and name attributes of your fields if desired.

+

The other interesting bit is the extra value. GET forms include an extra +field to indicate that they have been submitted, and POST forms include a +security token to prevent CSRF attacks. If you don’t include this extra hidden +field in your form, the form submission will fail.

+

Other than that, things are pretty straight-forward. We create our personRes +value by combining together the nameRes and ageRes values, and then return +a tuple of the person and the widget. And in the getHomeR function, +everything looks just like an applicative form. In fact, you could swap out our +monadic form with an applicative one and the code would still work.

+
+
+

Input forms

+

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 +forms.

+

These work mostly the same as applicative and monadic forms, with some differences:

+
    +
  • +

    +You use runInputPost and runInputGet. +

    +
  • +
  • +

    +You use ireq and iopt. These functions now only take two arguments: the + field type and the name (i.e., HTML name attribute) of the field in + question. +

    +
  • +
  • +

    +After running a form, it returns the value. It doesn’t return a widget or an + encoding type. +

    +
  • +
  • +

    +If there are any validation errors, the page returns an "invalid arguments" + error page. +

    +
  • +
+

You can use input forms to recreate the previous example. Note, however, that +the input version is less user friendly. If you make a mistake in an +applicative or monadic form, you will be brought back to the same page, with +your previously entered values in the form, and an error message explaining what +you need to correct. With input forms, the user simply gets an error message.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/input InputR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form action=@{InputR}>
+            <p>
+                My name is
+                <input type=text name=name>
+                and I am
+                <input type=text name=age>
+                years old.
+                <input type=submit value="Introduce myself">
+    |]
+
+getInputR :: Handler Html
+getInputR = do
+    person <- runInputGet $ Person
+                <$> ireq textField "name"
+                <*> ireq intField "age"
+    defaultLayout [whamlet|<p>#{show person}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Custom fields

+

The fields that come built-in with Yesod will likely cover the vast majority of +your form needs. But occasionally, you’ll need something more specialized. +Fortunately, you can create new fields in Yesod yourself. The Field constructor +has three values: fieldParse takes a list of values submitted by the user and +returns one of three results:

+
    +
  • +

    +An error message saying validation failed. +

    +
  • +
  • +

    +The parsed value. +

    +
  • +
  • +

    +Nothing, indicating that no data was supplied. +

    +
  • +
+

That last case might sound surprising. It would seem that Yesod can +automatically know that no information is supplied when the input list is +empty. But in reality, for some field types, the lack of any input is actually +valid input. Checkboxes, for instance, indicate an unchecked state by sending +in an empty list.

+

Also, what’s up with the list? Shouldn’t it be a Maybe? That’s also not the +case. With grouped checkboxes and multi-select lists, you’ll have multiple +widgets with the same name. We also use this trick in our example below.

+

The second value in the constructor is fieldView, and it renders a widget to display to the +user. This function has the following arguments:

+
    +
  1. +

    +The id attribute. +

    +
  2. +
  3. +

    +The name attribute. +

    +
  4. +
  5. +

    +Any other arbitrary attributes. +

    +
  6. +
  7. +

    +The result, given as an Either value. This will provide either the unparsed +input (when parsing failed) or the successfully parsed value. intField is a +great example of how this works. If you type in 42, the value of result +will be Right 42. But if you type in turtle, the result will be Left +"turtle". This lets you put in a value attribute on your input tag that will +give the user a consistent experience. +

    +
  8. +
  9. +

    +A Bool indicating if the field is required. +

    +
  10. +
+

The final value in the constructor is fieldEnctype. If you’re dealing with +file uploads, this should be Multipart; otherwise, it should be UrlEncoded.

+

As a small example, let’s create a new field type that is a password confirm +field. This field has two text inputs- both with the same name attribute- and +returns an error message if the values don’t match. Note that, unlike most +fields, it does not provide a value attribute on the input tags, as you don’t +want to send back a user-entered password in your HTML ever.

+
passwordConfirmField :: Field Handler Text
+passwordConfirmField = Field
+    { fieldParse = \rawVals _fileVals ->
+        case rawVals of
+            [a, b]
+                | a == b -> return $ Right $ Just a
+                | otherwise -> return $ Left "Passwords don't match"
+            [] -> return $ Right Nothing
+            _ -> return $ Left "You must enter two values"
+    , fieldView = \idAttr nameAttr otherAttrs eResult isReq ->
+        [whamlet|
+            <input id=#{idAttr} name=#{nameAttr} *{otherAttrs} type=password>
+            <div>Confirm:
+            <input id=#{idAttr}-confirm name=#{nameAttr} *{otherAttrs} type=password>
+        |]
+    , fieldEnctype = UrlEncoded
+    }
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet $ renderDivs
+        $ areq passwordConfirmField "Password" Nothing
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+                <input type=submit value="Change password">
+        |]
+
+
+

Values that don’t come from the user

+

Imagine you’re writing a blog hosting web app, and you want to have a form for +users to enter a blog post. A blog post will consist of four pieces of +information:

+
    +
  • +

    +Title +

    +
  • +
  • +

    +HTML contents +

    +
  • +
  • +

    +User ID of the author +

    +
  • +
  • +

    +Publication date +

    +
  • +
+

We want the user to enter the first two values, but not the second two. User ID +should be determined automatically by authenticating the user (a topic we +haven’t covered yet), and the publication date should just be the current time. +The question is, how do we keep our simple applicative form syntax, and yet +pull in values that don’t come from the user?

+

The answer is two separate helper functions:

+
    +
  • +

    +pure allows us to wrap up a plain value as an applicative form value. +

    +
  • +
  • +

    +lift allows us to run arbitrary Handler actions inside an applicative form. +

    +
  • +
+

Let’s see an example of using these two functions:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Data.Time
+import           Yesod
+
+-- In the authentication chapter, we'll address this properly
+newtype UserId = UserId Int
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+type Form a = Html -> MForm Handler (FormResult a, Widget)
+
+data Blog = Blog
+    { blogTitle    :: Text
+    , blogContents :: Textarea
+    , blogUser     :: UserId
+    , blogPosted   :: UTCTime
+    }
+    deriving Show
+
+form :: UserId -> Form Blog
+form userId = renderDivs $ Blog
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+    <*> pure userId
+    <*> lift (liftIO getCurrentTime)
+
+getHomeR :: Handler Html
+getHomeR = do
+    let userId = UserId 5 -- again, see the authentication chapter
+    ((res, widget), enctype) <- runFormPost $ form userId
+    defaultLayout
+        [whamlet|
+            <p>Previous result: #{show res}
+            <form method=post action=@{HomeR} enctype=#{enctype}>
+                ^{widget}
+                <input type=submit>
+        |]
+
+postHomeR :: Handler Html
+postHomeR = getHomeR
+
+main :: IO ()
+main = warp 3000 App
+

One trick we’ve introduced here is using the same handler code for both the +GET and POST request methods. This is enabled by the implementation of +runFormPost, which will behave exactly like generateFormPost in the case of +a GET request. Using the same handler for both request methods cuts down on +some boilerplate.

+
+
+

Summary

+

Forms in Yesod are broken up into three groups. Applicative is the most common, +as it provides a nice user interface with an easy-to-use API. Monadic forms +give you more power, but are harder to use. Input forms are intended when you +just want to read data from the user, not generate the input widgets.

+

There are a number of different Fields provided by Yesod out-of-the-box. In +order to use these in your forms, you need to indicate the kind of form and +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 +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.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/haskell.html b/public/book-1.6/haskell.html new file mode 100644 index 00000000..d5ebe0a4 --- /dev/null +++ b/public/book-1.6/haskell.html @@ -0,0 +1,450 @@ + Haskell :: Yesod Web Framework Book- Version 1.6 +
+

Haskell

+ + +

Haskell is a powerful, fast, type-safe, functional programming language. This +book takes as an assumption that you are already familiar with most of the +basics of Haskell. There are two wonderful books for learning Haskell, both of +which are available for reading online:

+ +

Additionally, there are a number of great articles on +School of Haskell.

+

In order to use Yesod, you’re going to have to know at least the basics of +Haskell. Additionally, Yesod uses some features of Haskell that aren’t covered +in most introductory texts. While this book assumes the reader has a basic +familiarity with Haskell, this chapter is intended to fill in the gaps.

+

If you are already fluent in Haskell, feel free to completely skip this +chapter. Also, if you would prefer to start off by getting your feet wet with +Yesod, you can always come back to this chapter later as a reference.

+
+

Terminology

+

Even for those familiar with Haskell as a language, there can sometimes be some +confusion about terminology. Let’s establish some base terms that we can use +throughout this book.

+
+
+Data type +
+

+This is one of the core building blocks for a strongly typed +language like Haskell. Some data types, like Int, can be treated as primitive +values, while other data types will build on top of these to create more +complicated values. For example, you might represent a person with: +

+
data Person = Person Text Int
+

Here, the Text would give the person’s name, and the Int would give the +person’s age. Due to its simplicity, this specific example type will recur +throughout the book. There are essentially three ways you can create a new data +type:

+
    +
  • +

    +A type declaration such as type GearCount = Int merely creates a + synonym for an existing type. The type system will do nothing to prevent + you from using an Int where you asked for a GearCount. Using this can + make your code more self-documenting. +

    +
  • +
  • +

    +A newtype declaration such as newtype Make = Make Text. In this case, + you cannot accidentally use a Text in place of a Make; the compiler + will stop you. The newtype wrapper always disappears during compilation, + and will introduce no overhead. +

    +
  • +
  • +

    +A data declaration, such as Person above. You can also create + Algebraic Data Types (ADTs), such as data Vehicle = Bicycle GearCount | + Car Make Model. +

    +
  • +
+
+
+Data constructor +
+

+In our examples above, Person, Make, Bicycle, and +Car are all data constructors. +

+
+
+Type constructor +
+

+In our examples above, Person, Make, and Vehicle are +all type constructors. +

+
+
+Type variables +
+

+Consider the data type data Maybe a = Just a | Nothing. In +this case, a is a type variable. +

+
+
+ +
+
+

Tools

+

Since July 2015, the tooling recommendation for Yesod has become very simple: +use stack. stack is a +complete build tool for Haskell which deals with your compiler (Glasgow Haskell +Compiler, aka GHC), libraries (including Yesod), additional build tools (like +alex and happy), and much more. There are other build tools available in +Haskell, and most of them support Yesod quite well. But for the easiest +experience, it’s strongly recommended to stick with stack. The Yesod website +keeps an up-to-date quick start +guide, which provides instructions on installing stack and getting started +with a new scaffolded site.

+

Once you have your toolchain set up correctly, you’ll need to install a number +of Haskell libraries. For the vast majority of the book, the following command +will install all the libraries you need:

+
stack build classy-prelude-yesod persistent-sqlite
+

In order to run an example from the book, save it in a file, e.g., +yesod-example.hs, and then run it with:

+
stack runghc yesod-example.hs
+
+
+

Language Pragmas

+

GHC will run by default in something very close to Haskell98 mode. It also +ships with a large number of language extensions, allowing more powerful type +classes, syntax changes, and more. There are multiple ways to tell GHC to turn +on these extensions. For most of the code snippets in this book, you’ll see +language pragmas, which look like this:

+
{-# LANGUAGE MyLanguageExtension #-}
+

These should always appear at the top of your source file. Additionally, there +are two other common approaches:

+
    +
  • +

    +On the GHC command line, pass an extra argument -XMyLanguageExtension. +

    +
  • +
  • +

    +In your cabal file, add an default-extensions block. +

    +
  • +
+

I personally never use the GHC command line argument approach. It’s a personal +preference, but I like to have my settings clearly stated in a file. In general +it’s recommended to avoid putting extensions in your cabal file; however, +this rule mostly applies when writing publicly available libraries. When you’re +writing an application that you and your team will be working on, having all of +your language extensions defined in a single location makes a lot of sense. +The Yesod scaffolded site specifically uses this approach to avoid the +boilerplate of specifying the same language pragmas in every source file.

+

We’ll end up using quite a few language extensions in this book (at the time of +writing, the scaffolding uses 13). We will not cover the meaning of all of +them. Instead, please see the +GHC +documentation.

+
+
+

Overloaded Strings

+

What’s the type of "hello"? Traditionally, it’s String, which is defined as +type String = [Char]. Unfortunately, there are a number of limitations with +this:

+
    +
  • +

    +It’s a very inefficient implementation of textual data. We need to allocate + extra memory for each cons cell, plus the characters themselves each take up + a full machine word. +

    +
  • +
  • +

    +Sometimes we have string-like data that’s not actually text, such as + ByteStrings and HTML. +

    +
  • +
+

To work around these limitations, GHC has a language extension called +OverloadedStrings. When enabled, literal strings no longer have the +monomorphic type String; instead, they have the type IsString a ⇒ a, +where IsString is defined as:

+
class IsString a where
+    fromString :: String -> a
+

There are IsString instances available for a number of types in Haskell, such +as Text (a much more efficient packed String type), ByteString, and +Html. Virtually every example in this book will assume that this language +extension is turned on.

+

Unfortunately, there is one drawback to this extension: it can sometimes +confuse GHC’s type checker. Imagine we have:

+
{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}
+import Data.Text (Text)
+
+class DoSomething a where
+    something :: a -> IO ()
+
+instance DoSomething String where
+    something _ = putStrLn "String"
+
+instance DoSomething Text where
+    something _ = putStrLn "Text"
+
+myFunc :: IO ()
+myFunc = something "hello"
+

Will the program print out String or Text? It’s not clear. So instead, +you’ll need to give an explicit type annotation to specify whether "hello" +should be treated as a String or Text.

+ +
+
+

Type Families

+

The basic idea of a type family is to state some association between two +different types. Suppose we want to write a function that will safely take the +first element of a list. But we don’t want it to work just on lists; we’d like +it to treat a ByteString like a list of Word8s. To do so, we need to +introduce some associated type to specify what the contents of a certain type +are.

+
{-# LANGUAGE TypeFamilies, OverloadedStrings #-}
+import Data.Word (Word8)
+import qualified Data.ByteString as S
+import Data.ByteString.Char8 () -- get an orphan IsString instance
+
+class SafeHead a where
+    type Content a
+    safeHead :: a -> Maybe (Content a)
+
+instance SafeHead [a] where
+    type Content [a] = a
+    safeHead [] = Nothing
+    safeHead (x:_) = Just x
+
+instance SafeHead S.ByteString where
+    type Content S.ByteString = Word8
+    safeHead bs
+        | S.null bs = Nothing
+        | otherwise = Just $ S.head bs
+
+main :: IO ()
+main = do
+    print $ safeHead ("" :: String)
+    print $ safeHead ("hello" :: String)
+
+    print $ safeHead ("" :: S.ByteString)
+    print $ safeHead ("hello" :: S.ByteString)
+

The new syntax is the ability to place a type inside of a class and +instance. We can also use data instead, which will create a new datatype +instead of reference an existing one.

+ +
+
+

Template Haskell

+

Template Haskell (TH) is an approach to code generation. We use it in Yesod +in a number of places to reduce boilerplate, and to ensure that the generated +code is correct. Template Haskell is essentially Haskell which generates a +Haskell Abstract Syntax Tree (AST).

+ +

Writing TH code can be tricky, and unfortunately there isn’t very much type +safety involved. You can easily write TH that will generate code that won’t +compile. This is only an issue for the developers of Yesod, not for its users. +During development, we use a large collection of unit tests to ensure that the +generated code is correct. As a user, all you need to do is call these already +existing functions. For example, to include an externally defined Hamlet +template, you can write:

+
$(hamletFile "myfile.hamlet")
+

(Hamlet is discussed in the Shakespeare chapter.) The dollar sign immediately +followed by parentheses tell GHC that what follows is a Template Haskell +function. The code inside is then run by the compiler and generates a Haskell +AST, which is then compiled. And yes, it’s even possible to +go meta +with this.

+

A nice trick is that TH code is allowed to perform arbitrary IO actions, and +therefore we can place some input in external files and have it parsed at +compile time. One example usage is to have compile-time checked HTML, CSS, and +Javascript templates.

+

If your Template Haskell code is being used to generate declarations, and is +being placed at the top level of our file, we can leave off the dollar sign and +parentheses. In other words:

+
{-# LANGUAGE TemplateHaskell #-}
+
+-- Normal function declaration, nothing special
+myFunction = ...
+
+-- Include some TH code
+$(myThCode)
+
+-- Or equivalently
+myThCode
+

It can be useful to see what code is being generated by Template Haskell for +you. To do so, you should use the -ddump-splices GHC option.

+ +

Template Haskell introduces something called the stage +restriction, which essentially means that code before a Template Haskell splice +cannot refer to code in the Template Haskell, or what follows. This will +sometimes require you to rearrange your code a bit. The same restriction +applies to QuasiQuotes.

+

While out of the box, Yesod is really geared for using code generation to avoid +boilerplate, it’s perfectly acceptable to use Yesod in a Template Haskell-free +way. There’s more information on that in the "Yesod for Haskellers" chapter.

+
+
+

QuasiQuotes

+

QuasiQuotes (QQ) are a minor extension of Template Haskell that let us embed +arbitrary content within our Haskell source files. For example, we mentioned +previously the hamletFile TH function, which reads the template contents from +an external file. We also have a quasi-quoter named hamlet that takes the +content inline:

+
{-# LANGUAGE QuasiQuotes #-}
+
+[hamlet|<p>This is quasi-quoted Hamlet.|]
+

The syntax is set off using square brackets and pipes. The name of the +quasi-quoter is given between the opening bracket and the first pipe, and the +content is given between the pipes.

+

Throughout the book, we will often times use the QQ-approach over a TH-powered +external file since the former is simpler to copy-and-paste. However, in +production, external files are recommended for all but the shortest of inputs +as it gives a nice separation of the non-Haskell syntax from your Haskell code.

+
+
+

API Documentation

+

The standard API documentation program in Haskell is called Haddock. The +standard Haddock search tool is called Hoogle. My recommendation is to use +Stackage’s Hoogle search and its +accompanying Haddocks for searching and browsing documentation. The reason for +this is that the Stackage Hoogle database covers a very large number of open +source Haskell packages, and the documentation provided is always fully +generated and known to link to other working Haddocks.

+

If when reading this book you run into types or functions that you do not +understand, try doing a Hoogle search with Hoogle to get more +information.

+
+
+

Summary

+

You don’t need to be an expert in Haskell to use Yesod, a basic familiarity +will suffice. This chapter hopefully gave you just enough extra information to +feel more comfortable following the rest of the book.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/http-conduit.html b/public/book-1.6/http-conduit.html new file mode 100644 index 00000000..4f694fcd --- /dev/null +++ b/public/book-1.6/http-conduit.html @@ -0,0 +1,127 @@ + http-conduit :: Yesod Web Framework Book- Version 1.6 +
+ +
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/initializing-foundation-data.html b/public/book-1.6/initializing-foundation-data.html new file mode 100644 index 00000000..0d024c64 --- /dev/null +++ b/public/book-1.6/initializing-foundation-data.html @@ -0,0 +1,270 @@ + Initializing data in the foundation datatype :: Yesod Web Framework Book- Version 1.6 +
+

Initializing data in the foundation datatype

+ + +

This example is meant to demonstrate a relatively simple concept: performing +some initialization of data to be kept in the foundation datatype. There are +various reasons to do this, though the two most important are:

+
    +
  • +

    +Efficiency: by initializing data once, at process startup, you can avoid + having to recompute the same value in each request. +

    +
  • +
  • +

    +Persistence: we want to store some information in a mutable location which + will be persisted between individual requests. Often times, this is done via + an external database, but it can also be done via an in-memory mutable + variable. +

    +
  • +
+ +

To demonstrate, we’ll implement a very simple website. It will contain a single +route, and will serve content stored in a Markdown file. In addition to serving +that content, we’ll also display an old-school visitor counter indicating how +many visitors have been to the site.

+
+

Step 1: define your foundation

+

We’ve identified two pieces of information to be initialized: the Markdown +content to be display on the homepage, and a mutable variable holding the +visitor count. Remember that our goal is to perform as much of the work in the +initialization phase as possible and thereby avoid performing the same work in +the handlers themselves. Therefore, we want to preprocess the Markdown content +into HTML. As for the visitor count, a simple IORef should be sufficient. So +our foundation data type is:

+
data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+
+

Step 2: use the foundation

+

For this trivial example, we only have one route: the homepage. All we need to do is:

+
    +
  1. +

    +Increment the visitor count. +

    +
  2. +
  3. +

    +Get the new visitor count. +

    +
  4. +
  5. +

    +Display the Markdown content together with the visitor count. +

    +
  6. +
+

One trick we’ll use to make the code a bit shorter is to utilize record +wildcard syntax: App {..}. This is convenient when we want to deal with a +number of different fields in a datatype.

+
getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+
+

Step 3: create the foundation value

+

When we initialize our application, we’ll now need to provide values for the +two fields we described above. This is normal IO code, and can perform any +arbitrary actions needed. In our case, we need to:

+
    +
  1. +

    +Read the Markdown from the file. +

    +
  2. +
  3. +

    +Convert that Markdown to HTML. +

    +
  4. +
  5. +

    +Create the visitor counter variable. +

    +
  6. +
+

The code ends up being just as simple as those steps imply:

+
go :: IO ()
+go = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+

Conclusion

+

There’s no rocket science involved in this example, just very straightforward +programming. The purpose of this chapter is to demonstrate the commonly used +best practice for achieving these often needed objectives. In your own +applications, the initialization steps will likely be much more complicated: +setting up database connection pools, starting background jobs to batch process +large data, or anything else. After reading this chapter, you should now have a +good idea of where to place your application-specific initialization code.

+

Below is the full source code for the example described above:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import qualified Data.Text.Lazy.IO as TLIO
+import           Text.Markdown
+import           Yesod
+
+data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+main :: IO ()
+main = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/internationalization.html b/public/book-1.6/internationalization.html new file mode 100644 index 00000000..4df16105 --- /dev/null +++ b/public/book-1.6/internationalization.html @@ -0,0 +1,439 @@ + Internationalization :: Yesod Web Framework Book- Version 1.6 +
+

Internationalization

+ + +

Users expect our software to speak their language. Unfortunately for us, there +will likely be more than one language involved. While doing simple string +replacement isn’t too involved, correctly dealing with all the grammar issues +can be tricky. After all, who wants to see "List 1 file(s)" from a program +output?

+

But a real i18n solution needs to do more than just provide a means of +achieving the correct output. It needs to make this process easy for both the +programmer and the translator and relatively error-proof. Yesod’s answer to the +problem gives you:

+
    +
  • +

    +Intelligent guessing of the user’s desired language based on request headers, + with the ability to override. +

    +
  • +
  • +

    +A simple syntax for giving translations which requires no Haskell knowledge. + (After all, most translators aren’t programmers.) +

    +
  • +
  • +

    +The ability to bring in the full power of Haskell for tricky grammar issues + as necessary, along with a default selection of helper functions to cover + most needs. +

    +
  • +
  • +

    +Absolutely no issues at all with word order. +

    +
  • +
+
+

Synopsis

+
-- @messages/en.msg
+Hello: Hello
+EnterItemCount: I would like to buy:
+Purchase: Purchase
+ItemCount count@Int: You have purchased #{showInt count} #{plural count "item" "items"}.
+SwitchLanguage: Switch language to:
+Switch: Switch
+
-- @messages/he.msg
+Hello: שלום
+EnterItemCount: אני רוצה לקנות:
+Purchase: קנה
+ItemCount count: קנית #{showInt count} #{plural count "דבר" "דברים"}.
+SwitchLanguage: החלף שפה ל:
+Switch: החלף
+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkMessage "App" "messages" "en"
+
+plural :: Int -> String -> String -> String
+plural 1 x _ = x
+plural _ _ y = y
+
+showInt :: Int -> String
+showInt = show
+
+mkYesod "App" [parseRoutes|
+/     HomeR GET
+/buy  BuyR  GET
+/lang LangR POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>_{MsgHello}
+        <form action=@{BuyR}>
+            _{MsgEnterItemCount}
+            <input type=text name=count>
+            <input type=submit value=_{MsgPurchase}>
+        <form action=@{LangR} method=post>
+            _{MsgSwitchLanguage}
+            <select name=lang>
+                <option value=en>English
+                <option value=he>Hebrew
+            <input type=submit value=_{MsgSwitch}>
+    |]
+
+getBuyR :: Handler Html
+getBuyR = do
+    count <- runInputGet $ ireq intField "count"
+    defaultLayout [whamlet|<p>_{MsgItemCount count}|]
+
+postLangR :: Handler ()
+postLangR = do
+    lang <- runInputPost $ ireq textField "lang"
+    setLanguage lang
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Overview

+

Most existing i18n solutions out there, like gettext or Java message bundles, +work on the principle of string lookups. Usually some form of +printf-interpolation is used to interpolate variables into the strings. In +Yesod, as you might guess, we instead rely on types. This gives us all of our +normal advantages, such as the compiler automatically catching mistakes.

+

Let’s take a concrete example. Suppose our application has two things it wants +to say to a user: say hello, and state how many users are logged into the +system. This can be modeled with a sum type:

+
data MyMessage = MsgHello | MsgUsersLoggedIn Int
+

I can also write a function to turn this datatype into an English representation:

+
toEnglish :: MyMessage -> String
+toEnglish MsgHello = "Hello there!"
+toEnglish (MsgUsersLoggedIn 1) = "There is 1 user logged in."
+toEnglish (MsgUsersLoggedIn i) = "There are " ++ show i ++ " users logged in."
+

We can also write similar functions for other languages. The advantage to this +inside-Haskell approach is that we have the full power of Haskell for +addressing tricky grammar issues, especially pluralization.

+ +

The downside, however, is that you have to write all of this inside of Haskell, +which won’t be very translator-friendly. To solve this, Yesod introduces the +concept of message files. We’ll cover that in a little bit.

+

Assuming we have this full set of translation functions, how do we go about +using them? What we need is a new function to wrap them all up together, and +then choose the appropriate translation function based on the user’s selected +language. Once we have that, Yesod can automatically choose the most relevant +render function and call it on the values you provide.

+

In order to simplify things a bit, Hamlet has a special interpolation syntax, +_{…}, which handles all the calls to the render functions. And in order to +associate a render function with your application, you use the YesodMessage +typeclass.

+
+
+

Message files

+

The simplest approach to creating translations is via message files. The setup +is simple: there is a single folder containing all of your translation files, +with a single file for each language. Each file is named based on its language +code, e.g. en.msg. And each line in a file handles one phrase, which +correlates to a single constructor in your message data type.

+

So firstly, a word about language codes. There are really two choices +available: using a two-letter language code, or a language-LOCALE code. For +example, when I load up a page in my web browser, it sends two language codes: +en-US and en. What my browser is saying is "if you have American English, I +like that the most. If you have English, I’ll take that instead."

+

So which format should you use in your application? Most likely two-letter +codes, unless you are actually creating separate translations by locale. This +ensures that someone asking for Canadian English will still see your English. +Behind the scenes, Yesod will add the two-letter codes where relevant. For +example, suppose a user has the following language list:

+
pt-BR, es, he
+

What this means is "I like Brazilian Portuguese, then Spanish, and then +Hebrew." Suppose your application provides the languages pt (general +Portuguese) and English, with English as the default. Strictly following the +user’s language list would result in the user being served English. Instead, +Yesod translates that list into:

+
pt-BR, es, he, pt
+

In other words: unless you’re giving different translations based on locale, +just stick to the two-letter language codes.

+

Now what about these message files? The syntax should be very familiar after +your work with Hamlet and Persistent. The line starts off with the name of the +message. Since this is a data constructor, it must start with a capital letter. +Next, you can have individual parameters, which must be given as lower case. +These will be arguments to the data constructor.

+

The argument list is terminated by a colon, and then followed by the translated +string, which allows usage of our typical variable interpolation syntax +translation helper functions to deal with issues like pluralization, you can +create all the translated messages you need.

+
+

Scaffolding

+

The scaffolding used to include a messages folder for i18n messages. Since it is +used rarely it was removed to save some performance. +To add back i18n to your application you need to:

+
    +
  • +

    +Add the line mkMessage "App" "messages" "en" to Foundation.hs. +

    +
  • +
  • +

    +Create a directory "messages" in the main folder of your scaffolding project. +

    +
  • +
  • +

    +Create a file "messages/en.msg" with the following dummy content: Hello: Hello +

    +
  • +
+

After that you can use {..} anywhere in all your Hamlet files. Just make sure +to insert mkMessage "App" "messages" "en" before instance Yesod App where. +Otherwise you can’t use i18n in your defaultLayout. If your default language is +not "en", you can decide it here. Just make sure to also name your message file accordingly.

+
+
+

Specifying types

+

Since we will be creating a datatype out of our message specifications, each +parameter to a data constructor must be given a data type. We use a @-syntax +for this. For example, to create the datatype data MyMessage = MsgHello | +MsgSayAge Int, we would write:

+
Hello: Hi there!
+SayAge age@Int: Your age is: #{show age}
+

But there are two problems with this:

+
    +
  1. +

    +It’s not very DRY (don’t repeat yourself) to have to specify this datatype in every file. +

    +
  2. +
  3. +

    +Translators will be confused having to specify these datatypes. +

    +
  4. +
+

So instead, the type specification is only required in the main language file. +This is specified as the third argument in the mkMessage function. This also +specifies what the backup language will be, to be used when none of the +languages provided by your application match the user’s language list.

+
+
+
+

RenderMessage typeclass

+

Your call to mkMessage creates an instance of the RenderMessage typeclass, +which is the core of Yesod’s i18n. It is defined as:

+
class RenderMessage master message where
+    renderMessage :: master  -- ^ type that specifies which set of translations to use
+                  -> [Lang]  -- ^ acceptable languages in descending order of preference
+                  -> message -- ^ message to translate
+                  -> Text
+
+-- | an RFC1766 / ISO 639-1 language code (eg, @fr@, @en-GB@, etc).
+type Lang = Text
+

Notice that there are two parameters to the RenderMessage class: the master +site and the message type. In theory, we could skip the master type here, but +that would mean that every site would need to have the same set of translations +for each message type. When it comes to shared libraries like forms, that would +not be a workable solution.

+

The renderMessage function takes a parameter for each of the class’s type +parameters: master and message. The extra parameter is a list of languages the +user will accept, in descending order of priority. The method then returns a +user-ready Text that can be displayed.

+

A simple instance of RenderMessage may involve no actual translation of +strings; instead, it will just display the same value for every language. For +example:

+
data MyMessage = Hello | Greet Text
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ _ Hello = "Hello"
+    renderMessage _ _ (Greet name) = "Welcome, " <> name <> "!"
+

Notice how we ignore the first two parameters to renderMessage. We can now +extend this to support multiple languages:

+
renderEn Hello = "Hello"
+renderEn (Greet name) = "Welcome, " <> name <> "!"
+renderHe Hello = "שלום"
+renderHe (Greet name) = "ברוכים הבאים, " <> name <> "!"
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ ("en":_) = renderEn
+    renderMessage _ ("he":_) = renderHe
+    renderMessage master (_:langs) = renderMessage master langs
+    renderMessage _ [] = renderEn
+

The idea here is fairly straight-forward: we define helper functions to support +each language. We then add a clause to catch each of those languages in the +renderMessage definition. We then have two final cases: if no languages +matched, continue checking with the next language in the user’s priority list. +If we’ve exhausted all languages the user specified, then use the default +language (in our case, English).

+

But odds are that you will never need to worry about writing this stuff +manually, as the message file interface does all this for you. But it’s always +a good idea to have an understanding of what’s going on under the surface.

+
+
+

Interpolation

+

One way to use your new RenderMessage instance would be to directly call the +renderMessage function. This would work, but it’s a bit tedious: you need to +pass in the foundation value and the language list manually. Instead, Hamlet +provides a specialized i18n interpolation, which looks like _{…}.

+ +

Hamlet will then automatically translate that to a call to renderMessage. +Once Hamlet gets the output Text value, it uses the toHtml function to +produce an Html value, meaning that any special characters (<, &, +>) will be automatically escaped.

+
+
+

Phrases, not words

+

As a final note, I’d just like to give some general i18n advice. Let’s say you +have an application for selling turtles. You’re going to use the word "turtle" +in multiple places, like "You have added 4 turtles to your cart." and "You have +purchased 4 turtles, congratulations!" As a programmer, you’ll immediately +notice the code reuse potential: we have the phrase "4 turtles" twice. So you +might structure your message file as:

+
AddStart: You have added
+AddEnd: to your cart.
+PurchaseStart: You have purchased
+PurchaseEnd: , congratulations!
+Turtles count@Int: #{show count} #{plural count "turtle" "turtles"}
+

STOP RIGHT THERE! This is all well and good from a programming perspective, but translations are not programming. There are a many things that could go wrong with this, such as:

+
    +
  • +

    +Some languages might put "to your cart" before "You have added." +

    +
  • +
  • +

    +Maybe "added" will be constructed differently depending on whether you added 1 or more turtles. +

    +
  • +
  • +

    +There are a bunch of whitespace issues as well. +

    +
  • +
+

So the general rule is: translate entire phrases, not just words.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/introduction.html b/public/book-1.6/introduction.html new file mode 100644 index 00000000..d3f6bff3 --- /dev/null +++ b/public/book-1.6/introduction.html @@ -0,0 +1,220 @@ + Introduction :: Yesod Web Framework Book- Version 1.6 +
+

Introduction

+ + +

Since web programming began, people have been trying to make the development +process a more pleasant one. As a community, we have continually pushed new +techniques to try and solve some of the lingering difficulties of security +threats, the stateless nature of HTTP, the multiple languages (HTML, CSS, +Javascript) necessary to create a powerful web application, and more.

+

Yesod attempts to ease the web development process by playing to the strengths +of the Haskell programming language. Haskell’s strong compile-time guarantees +of correctness not only encompass types; referential transparency ensures that +we don’t have any unintended side effects. Pattern matching on algebraic data +types can help guarantee we’ve accounted for every possible case. By building +upon Haskell, entire classes of bugs disappear.

+

Unfortunately, using Haskell isn’t enough. The web, by its very nature, is +not type safe. Even the simplest case of distinguishing between an integer +and string is impossible: all data on the web is transferred as raw bytes, +evading our best efforts at type safety. Every app writer is left with the task +of validating all input. I call this problem the boundary issue: as much as +your application is type safe on the inside, every boundary with the outside +world still needs to be sanitized.

+
+

Type Safety

+

This is where Yesod comes in. By using high-level declarative techniques, you +can specify the exact input types you are expecting. And the process works the +other way as well: using a process of type-safe URLs, you can make sure that +the data you send out is also guaranteed to be well formed.

+

The boundary issue is not just a problem when dealing with the client: the same +problem exists when persisting and loading data. Once again, Yesod saves you on +the boundary by performing the marshaling of data for you. You can specify your +entities in a high-level definition and remain blissfully ignorant of the +details.

+
+
+

Concise

+

We all know that there is a lot of boilerplate coding involved in web +applications. Wherever possible, Yesod tries to use Haskell’s features to save +your fingers the work:

+
    +
  • +

    +The forms library reduces the amount of code used for common cases by + leveraging the Applicative type class. +

    +
  • +
  • +

    +Routes are declared in a very terse format, without sacrificing type safety. +

    +
  • +
  • +

    +Serializing your data to and from a database is handled automatically via + code generation. +

    +
  • +
+

In Yesod, we have two kinds of code generation. To get your project started, we +provide a scaffolding tool to set up your file and folder structure. However, +most code generation is done at compile time via meta-programming. This means +your generated code will never get stale, as a simple library upgrade will +bring all your generated code up-to-date.

+

But for those who like to stay in control, and know exactly what their code is +doing, you can always run closer to the compiler and write all your code +yourself.

+
+
+

Performance

+

Haskell’s main compiler, the GHC, has amazing performance characteristics, and +is improving all the time. This choice of language by itself gives Yesod a +large performance advantage over other offerings. But that’s not enough: we +need an architecture designed for performance.

+

Our approach to templates is one example: by allowing HTML, CSS and JavaScript +to be analyzed at compile time, Yesod both avoids costly disk I/O at runtime +and can optimize the rendering of this code. But the architectural decisions go +deeper: we use advanced techniques such as conduits and builders in the +underlying libraries to make sure our code runs in constant memory, without +exhausting precious file handles and other resources. By offering high-level +abstractions, you can get highly compressed and properly cached CSS and +JavaScript.

+

Yesod’s flagship web server, Warp, is the fastest Haskell web server around. +When these two pieces of technology are combined, it produces one of the +fastest web application deployment solutions available.

+
+
+

Modular

+

Yesod has spawned the creation of dozens of packages, most of which are usable +in a context outside of Yesod itself. One of the goals of the project is to +contribute back to the community as much as possible; as such, even if you are +not planning on using Yesod in your next project, a large portion of this book +may still be relevant for your needs.

+

Of course, these libraries have all been designed to integrate well together. +Using the Yesod Framework should give you a strong feeling of consistency +throughout the various APIs.

+
+
+

A solid foundation

+

I remember once seeing a PHP framework advertising support for UTF-8. This +struck me as surprising: you mean having UTF-8 support isn’t automatic? In the +Haskell world, issues like character encoding are already well addressed and +fully supported. In fact, we usually have the opposite problem: there are a +number of packages providing powerful and well-designed support for the +problem. The Haskell community is constantly pushing the boundaries finding the +cleanest, most efficient solutions for each challenge.

+

The downside of such a powerful ecosystem is the complexity of choice. By using +Yesod, you will already have most of the tools chosen for you, and you can be +guaranteed they work together. Of course, you always have the option of pulling +in your own solution.

+

As a real-life example, Yesod and Hamlet (the default templating language) use +blaze-builder for textual content generation. This choice was made because +blaze provides the fastest interface for generating UTF-8 data. Anyone who +wants to use one of the other great libraries out there, such as text, should +have no problem dropping it in.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/json-web-service.html b/public/book-1.6/json-web-service.html new file mode 100644 index 00000000..54d46667 --- /dev/null +++ b/public/book-1.6/json-web-service.html @@ -0,0 +1,221 @@ + JSON Web Service :: Yesod Web Framework Book- Version 1.6 +
+

JSON Web Service

+ + +

Let’s create a very simple web service: it takes a JSON request and returns a +JSON response. We’re going to write the server in WAI/Warp, and the client in +http-conduit. We’ll be using aeson for JSON parsing and rendering. We could +also write the server in Yesod itself, but for such a simple example, the extra +features of Yesod don’t add much.

+
+

Server

+

WAI uses the conduit package to handle streaming request bodies, and +efficiently generates responses using blaze-builder. aeson uses attoparsec for +parsing; by using attoparsec-conduit we get easy interoperability with WAI. +This plays out as:

+
{-# LANGUAGE OverloadedStrings #-}
+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.ByteString          (ByteString)
+import           Data.Conduit             (($$))
+import           Data.Conduit.Attoparsec  (sinkParser)
+import           Network.HTTP.Types       (status200, status400)
+import           Network.Wai              (Application, Response, responseLBS)
+import           Network.Wai.Conduit      (sourceRequestBody)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req sendResponse = handle (sendResponse . invalidJson) $ do
+    value <- sourceRequestBody req $$ sinkParser json
+    newValue <- liftIO $ modValue value
+    sendResponse $ responseLBS
+        status200
+        [("Content-Type", "application/json")]
+        $ encode newValue
+
+invalidJson :: SomeException -> Response
+invalidJson ex = responseLBS
+    status400
+    [("Content-Type", "application/json")]
+    $ encode $ object
+        [ ("message" .= show ex)
+        ]
+
+-- Application-specific logic would go here.
+modValue :: Value -> IO Value
+modValue = return
+
+
+

Client

+

http-conduit was written as a companion to WAI. It too uses conduit and +blaze-builder pervasively, meaning we once again get easy interop with +aeson. A few extra comments for those not familiar with http-conduit:

+
    +
  • +

    +A Manager is present to keep track of open connections, so that multiple + requests to the same server use the same connection. You usually want to use + the getGlobalManager function to get the global connection manager. +

    +
  • +
  • +

    +We need to know the size of our request body, which can’t be determined + directly from a Builder. Instead, we convert the Builder into a lazy + ByteString and take the size from there. +

    +
  • +
  • +

    +There are a number of different functions for initiating a request. We use + http, which allows us to directly access the data stream. There are other + higher level functions (such as httpLbs) that let you ignore the issues of + sources and get the entire body directly. +

    +
  • +
+
{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Data.Aeson              (Value (Object, String))
+import           Data.Aeson              (encode, object, (.=))
+import           Data.Aeson.Parser       (json)
+import           Data.Conduit            (($$+-))
+import           Data.Conduit.Attoparsec (sinkParser)
+import           Network.HTTP.Conduit    (RequestBody (RequestBodyLBS),
+                                          Response (..), http, method, parseUrl,
+                                          requestBody, getGlobalManager)
+
+main :: IO ()
+main = do
+    manager <- getGlobalManager
+    value <- liftIO makeValue
+    -- We need to know the size of the request body, so we convert to a
+    -- ByteString
+    let valueBS = encode value
+    req' <- liftIO $ parseUrl "http://localhost:3000/"
+    let req = req' { method = "POST", requestBody = RequestBodyLBS valueBS }
+    res <- http req manager
+    resValue <- responseBody res $$+- sinkParser json
+    liftIO $ handleResponse resValue
+
+-- Application-specific function to make the request value
+makeValue :: IO Value
+makeValue = return $ object
+    [ ("foo" .= ("bar" :: String))
+    ]
+
+-- Application-specific function to handle the response from the server
+handleResponse :: Value -> IO ()
+handleResponse = print
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/monad-control.html b/public/book-1.6/monad-control.html new file mode 100644 index 00000000..49d77b8e --- /dev/null +++ b/public/book-1.6/monad-control.html @@ -0,0 +1,450 @@ + monad-control :: Yesod Web Framework Book- Version 1.6 +
+

monad-control

+ + +

monad-control is used in a few places within Yesod, most notably to ensure +proper exception handling within Persistent. It is a general purpose package to +extend standard functionality in monad transformers.

+
+

Overview

+

One of the powerful, and sometimes confusing, features in Haskell is monad +transformers. They allow you to take different pieces of functionality- such as +mutable state, error handling, or logging- and compose them together easily. +Though I swore I’d never write a monad tutorial, I’m going to employ a painful +analogy here: monads are like onions. (Monads are not like cakes.) By that, I +mean layers.

+

We have the core monad- also known as the innermost or bottom monad. On top of +this core, we add layers, each adding a new feature and spreading +outward/upward. As a motivating example, let’s consider an ErrorT transformer +stacked on top of the IO monad:

+
newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
+type MyStack = ErrorT MyError IO
+

Now pay close attention here: ErrorT is just a simple newtype around an Either +wrapped in a monad. Getting rid of the newtype, we have:

+
type ErrorTUnwrapped e m a = m (Either e a)
+

At some point, we’ll need to actually perform some IO inside our MyStack. If we +went with the unwrapped approach, it would be trivial, since there would be no +ErrorT constructor in the way. However, we need that newtype wrapper for a +whole bunch of type reasons I won’t go into here (this isn’t a monad +transformer tutorial after all). So the solution is the MonadTrans typeclass:

+
class MonadTrans t where
+    lift :: Monad m => m a -> t m a
+

I’ll admit, the first time I saw that type signature, my response was stunned +confusion, and incredulity that it actually meant anything. But looking at an +instance helps a bit:

+
instance (Error e) => MonadTrans (ErrorT e) where
+    lift m = ErrorT $ do
+        a <- m
+        return (Right a)
+

All we’re doing is wrapping the inside of the IO with a Right value, and then +applying our newtype wrapper. This allows us to take an action that lives in +IO, and "lift" it to the outer/upper monad.

+

But now to the point at hand. This works very well for simple functions. For +example:

+
sayHi :: IO ()
+sayHi = putStrLn "Hello"
+
+sayHiError :: ErrorT MyError IO ()
+sayHiError = lift $ putStrLn "Hello"
+

But let’s take something slightly more complicated, like a callback:

+
withMyFile :: (Handle -> IO a) -> IO a
+withMyFile = withFile "test.txt" WriteMode
+
+sayHi :: Handle -> IO ()
+sayHi handle = hPutStrLn handle "Hi there"
+
+useMyFile :: IO ()
+useMyFile = withMyFile sayHi
+

So far so good, right? Now let’s say that we need a version of sayHi that has +access to the Error monad:

+
sayHiError :: Handle -> ErrorT MyError IO ()
+sayHiError handle = do
+    lift $ hPutStrLn handle "Hi there, error!"
+    throwError MyError
+

We would like to write a function that combines withMyFile and sayHiError. +Unfortunately, GHC doesn’t like this very much:

+
useMyFileErrorBad :: ErrorT MyError IO ()
+useMyFileErrorBad = withMyFile sayHiError
+
+    Couldn't match expected type `ErrorT MyError IO ()'
+                with actual type `IO ()'
+

Why does this happen, and how can we work around it?

+
+
+

Intuition

+

Let’s try and develop an external intuition of what’s happening here. The +ErrorT monad transformer adds extra functionality to the IO monad. We’ve +defined a way to "tack on" that extra functionality to normal IO actions: we +add that Right constructor and wrap it all in ErrorT. Wrapping in Right is our +way of saying "it went OK," there wasn’t anything wrong with this action.

+

Now this intuitively makes sense: since the IO monad doesn’t have the concept +of returning a MyError when something goes wrong, it will always succeed in the +lifting phase. (Note: This has nothing to do with runtime exceptions, don’t +even think about them.) What we have is a guaranteed one-directional +translation up the monad stack.

+

Let’s take another example: the Reader monad. A Reader has access to some extra +piece of data floating around. Whatever is running in the inner monad doesn’t +know about that extra piece of information. So how would you do a lift? You +just ignore that extra information. The Writer monad? Don’t write anything. +State? Don’t change anything. I’m seeing a pattern here.

+

But now let’s try and go in the opposite direction: I have something in a +Reader, and I’d like to run it in the base monad (e.g., IO). Well… that’s not +going to work, is it? I need that extra piece of information, I’m relying on +it, and it’s not there. There’s simply no way to go in the opposite direction +without providing that extra value.

+

Or is there? If you remember, we’d pointed out earlier that ErrorT is just a +simple wrapper around the inner monad. In other words, if I have errorValue +:: ErrorT MyError IO MyValue, I can apply runErrorT and get a value of +type IO (Either MyError MyValue). The looks quite a bit like bi-directional +translation, doesn’t it?

+

Well, not quite. We originally had an ErrorT MyError IO monad, with a value +of type MyValue. Now we have a monad of type IO with a value of type +Either MyError MyValue. So this process has in fact changed the value, while +the lifting process leaves it the same.

+

But still, with a little fancy footwork we can unwrap the ErrorT, do some +processing, and then wrap it back up again.

+
useMyFileError1 :: ErrorT MyError IO ()
+useMyFileError1 =
+    let unwrapped :: Handle -> IO (Either MyError ())
+        unwrapped handle = runErrorT $ sayHiError handle
+        applied :: IO (Either MyError ())
+        applied = withMyFile unwrapped
+        rewrapped :: ErrorT MyError IO ()
+        rewrapped = ErrorT applied
+     in rewrapped
+

This is the crucial point of this whole article, so look closely. We first +unwrap our monad. This means that, to the outside world, it’s now just a plain +old IO value. Internally, we’ve stored all the information from our ErrorT +transformer. Now that we have a plain old IO, we can easily pass it off to +withMyFile. withMyFile takes in the internal state and passes it back out +unchanged. Finally, we wrap everything back up into our original ErrorT.

+

This is the entire pattern of monad-control: we embed the extra features of our +monad transformer inside the value. Once in the value, the type system ignores +it and focuses on the inner monad. When we’re done playing around with that +inner monad, we can pull our state back out and reconstruct our original monad +stack.

+
+
+

Types

+

I purposely started with the ErrorT transformer, as it is one of the simplest +for this inversion mechanism. Unfortunately, others are a bit more complicated. +Take for instance ReaderT. It is defined as newtype ReaderT r m a = ReaderT { +runReaderT :: r -> m a }. If we apply runReaderT to it, we get a +function that returns a monadic value. So we’re going to need some extra +machinery to deal with all that stuff. And this is when we leave Kansas behind.

+

There are a few approaches to solving these problems. In the past, I +implemented a solution using type families in the neither package. Anders +Kaseorg implemented a much more straight-forward solution in monad-peel. And +for efficiency, in monad-control, Bas van Dijk uses CPS (continuation passing +style) and existential types.

+ +

The first type we’re going to look at is:

+
type Run t = forall n o b. (Monad n, Monad o, Monad (t o)) => t n b -> n (t o b)
+

That’s incredibly dense, let’s talk it out. The only "input" datatype to this +thing is t, a monad transformer. A Run is a function that will then work with +any combination of types n, o and b (that’s what the forall means). n and o +are both monads, while b is a simple value contained by them.

+

The left hand side of the Run function, t n b, is our monad transformer +wrapped around the n monad and holding a b value. So for example, that could be +a MyTrans FirstMonad MyValue. It then returns a value with the transformer +"popped" inside, with a brand new monad at its core. In other words, +FirstMonad (MyTrans NewMonad MyValue).

+

That might sound pretty scary at first, but it actually isn’t as foreign as +you’d think: this is essentially what we did with ErrorT. We started with +ErrorT on the outside, wrapping around IO, and ended up with an IO by itself +containing an Either. Well guess what: another way to represent an Either is +ErrorT MyError Identity. So essentially, we pulled the IO to the outside and +plunked an Identity in its place. We’re doing the same thing in a Run: pulling +the FirstMonad outside and replacing it with a NewMonad.

+ +

Alright, now we’re getting somewhere. If we had access to one of those Run +functions, we could use it to peel off the ErrorT on our sayHiError function +and pass it to withMyFile. With the magic of undefined, we can play such a +game:

+
errorRun :: Run (ErrorT MyError)
+errorRun = undefined
+
+useMyFileError2 :: IO (ErrorT MyError Identity ())
+useMyFileError2 =
+    let afterRun :: Handle -> IO (ErrorT MyError Identity ())
+        afterRun handle = errorRun $ sayHiError handle
+        applied :: IO (ErrorT MyError Identity ())
+        applied = withMyFile afterRun
+     in applied
+

This looks eerily similar to our previous example. In fact, errorRun is acting +almost identically to runErrorT. However, we’re still left with two problems: +we don’t know where to get that errorRun value from, and we still need to +restructure the original ErrorT after we’re done.

+
+

MonadTransControl

+

Obviously in the specific case we have before us, we could use our knowledge of +the ErrorT transformer to beat the types into submission and create our Run +function manually. But what we really want is a general solution for many +transformers. At this point, you know we need a typeclass.

+

So let’s review what we need: access to a Run function, and some way to +restructure our original transformer after the fact. And thus was born +MonadTransControl, with its single method liftControl:

+
class MonadTrans t => MonadTransControl t where
+    liftControl :: Monad m => (Run t -> m a) -> t m a
+

Let’s look at this closely. liftControl takes a function (the one we’ll be +writing). That function is provided with a Run function, and must return a +value in some monad (m). liftControl will then take the result of that function +and reinstate the original transformer on top of everything.

+
useMyFileError3 :: Monad m => ErrorT MyError IO (ErrorT MyError m ())
+useMyFileError3 =
+    liftControl inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

Close, but not exactly what I had in mind. What’s up with the double monads? +Well, let’s start at the end: sayHiError handle returns a value of type ErrorT +MyError IO (). This we knew already, no surprises. What might be a little +surprising (it got me, at least) is the next two steps.

+

First we apply run to that value. Like we’d discussed before, the result is +that the IO inner monad is popped to the outside, to be replaced by some +arbitrary monad (represented by m here). So we end up with an IO (ErrorT +MyError m ()). Ok… We then get the same result after applying withMyFile. Not +surprising.

+

The last step took me a long time to understand correctly. Remember how we said +that we reconstruct the original transformer? Well, so we do: by plopping it +right on top of everything else we have. So our end result is the previous +type- IO (ErrorT MyError m ())- with a ErrorT MyError stuck on the front.

+

Well, that seems just about utterly worthless, right? Well, almost. But don’t +forget, that "m" can be any monad, including IO. If we treat it that way, we +get ErrorT MyError IO (ErrorT MyError IO ()). That looks a lot like m (m +a), and we want just plain old m a. Fortunately, now we’re in luck:

+
useMyFileError4 :: ErrorT MyError IO ()
+useMyFileError4 = join useMyFileError3
+

And it turns out that this usage is so common, that Bas had mercy on us and +defined a helper function:

+
control :: (Monad m, Monad (t m), MonadTransControl t)
+        => (Run t -> m (t m a)) -> t m a
+control = join . liftControl
+

So all we need to write is:

+
useMyFileError5 :: ErrorT MyError IO ()
+useMyFileError5 =
+    control inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

And just to make it a little shorter:

+
useMyFileError6 :: ErrorT MyError IO ()
+useMyFileError6 = control $ \run -> withMyFile $ run . sayHiError
+
+
+

MonadControlIO

+

The MonadTrans class provides the lift method, which allows you to lift an +action one level in the stack. There is also the MonadIO class that provides +liftIO, which lifts an IO action as far in the stack as desired. We have the +same breakdown in monad-control. But first, we need a corrolary to Run:

+
type RunInBase m base = forall b. m b -> base (m b)
+

Instead of dealing with a transformer, we’re dealing with two monads. base is +the underlying monad, and m is a stack built on top of it. RunInBase is a +function that takes a value of the entire stack, pops out that base, and puts +in on the outside. Unlike in the Run type, we don’t replace it with an +arbitrary monad, but with the original one. To use some more concrete types:

+
RunInBase (ErrorT MyError IO) IO = forall b. ErrorT MyError IO b -> IO (ErrorT MyError IO b)
+

This should look fairly similar to what we’ve been looking at so far, the only +difference is that we want to deal with a specific inner monad. Our +MonadControlIO class is really just an extension of MonadControlTrans using +this RunInBase.

+
class MonadIO m => MonadControlIO m where
+    liftControlIO :: (RunInBase m IO -> IO a) -> m a
+

Simply put, liftControlIO takes a function which receives a RunInBase. That +RunInBase can be used to strip down our monad to just an IO, and then +liftControlIO builds everything back up again. And like MonadControlTrans, it +comes with a helper function

+
controlIO :: MonadControlIO m => (RunInBase m IO -> IO (m a)) -> m a
+controlIO = join . liftControlIO
+

We can easily rewrite our previous example with it:

+
useMyFileError7 :: ErrorT MyError IO ()
+useMyFileError7 = controlIO $ \run -> withMyFile $ run . sayHiError
+

And as an advantage, it easily scales to multiple transformers:

+
sayHiCrazy :: Handle -> ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+sayHiCrazy handle = liftIO $ hPutStrLn handle "Madness!"
+
+useMyFileCrazy :: ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+useMyFileCrazy = controlIO $ \run -> withMyFile $ run . sayHiCrazy
+
+
+
+

Real Life Examples

+

Let’s solve some real-life problems with this code. Probably the biggest +motivating use case is exception handling in a transformer stack. For example, +let’s say that we want to automatically run some cleanup code when an exception +is thrown. If this were normal IO code, we’d use:

+
onException :: IO a -> IO b -> IO a
+

But if we’re in the ErrorT monad, we can’t pass in either the action or the +cleanup. In comes controlIO to the rescue:

+
onExceptionError :: ErrorT MyError IO a
+                 -> ErrorT MyError IO b
+                 -> ErrorT MyError IO a
+onExceptionError action after = controlIO $ \run ->
+    run action `onException` run after
+

Let’s say we need to allocate some memory to store a Double in. In the IO +monad, we could just use the alloca function. Once again, our solution is +simple:

+
allocaError :: (Ptr Double -> ErrorT MyError IO b)
+            -> ErrorT MyError IO b
+allocaError f = controlIO $ \run -> alloca $ run . f
+
+
+

Lost State

+

Let’s rewind a bit to our onExceptionError. It uses onException under the +surface, which has a type signature: IO a -> IO b -> IO a. Let me ask +you something: what happened to the b in the output? Well, it was thoroughly +ignored. But that seems to cause us a bit of a problem. After all, we store our +transformer state information in the value of the inner monad. If we ignore it, +we’re essentially ignoring the monadic side effects as well!

+

And the answer is that, yes, this does happen with monad-control. Certain functions will drop some of the monadic side effects. This is put best by Bas, in the comments on the relevant functions:[quote]

+
+

Note, any monadic side effects in m of the "release" computation will be discarded; it is run only for its side effects in IO.

+
+

In practice, monad-control will usually be doing the right thing for you, but +you need to be aware that some side effects may disappear.

+
+
+

More Complicated Cases

+

In order to make our tricks work so far, we’ve needed to have functions that +give us full access to play around with their values. Sometimes, this isn’t the +case. Take, for instance:

+
addMVarFinalizer :: MVar a -> IO () -> IO ()
+

In this case, we are required to have no value inside our finalizer function. +Intuitively, the first thing we should notice is that there will be no way to +capture our monadic side effects. So how do we get something like this to +compile? Well, we need to explicitly tell it to drop all of its state-holding +information:

+
addMVarFinalizerError :: MVar a -> ErrorT MyError IO () -> ErrorT MyError IO ()
+addMVarFinalizerError mvar f = controlIO $ \run ->
+    return $ liftIO $ addMVarFinalizer mvar (run f >> return ())
+

Another case from the same module is:

+
modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b
+

Here, we have a restriction on the return type in the second argument: it must +be a tuple of the value passed to that function and the final return value. +Unfortunately, I can’t see a way of writing a little wrapper around modifyMVar +to make it work for ErrorT. Instead, in this case, I copied the definition of +modifyMVar and modified it:

+
modifyMVar :: MVar a
+           -> (a -> ErrorT MyError IO (a, b))
+           -> ErrorT MyError IO b
+modifyMVar m io =
+  Control.Exception.Control.mask $ \restore -> do
+    a      <- liftIO $ takeMVar m
+    (a',b) <- restore (io a) `onExceptionError` liftIO (putMVar m a)
+    liftIO $ putMVar m a'
+    return b
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/persistent.html b/public/book-1.6/persistent.html new file mode 100644 index 00000000..c9d64378 --- /dev/null +++ b/public/book-1.6/persistent.html @@ -0,0 +1,1653 @@ + Persistent :: Yesod Web Framework Book- Version 1.6 +
+

Persistent

+ + +

Forms deal with the boundary between the user and the application. Another +boundary we need to deal with is between the application and the storage layer. +Whether it be a SQL database, a YAML file, or a binary blob, odds are your +storage layer does not natively understand your application’s data types, and +you’ll need to perform some marshaling. Persistent is Yesod’s answer to data +storage- a type-safe, universal data store interface for Haskell.

+

Haskell has many different database bindings available. However, most of these +have little knowledge of a schema and therefore do not provide useful static +guarantees. They also force database-dependent APIs and data types on the +programmer.

+

Some Haskellers have attempted a more revolutionary route: creating Haskell +specific data stores that allow one to easily store any strongly typed Haskell +data. These options are great for certain use cases, but they constrain one to +the storage techniques provided by the library and do not interface well with +other languages.

+

In contrast, Persistent allows us to choose among existing databases that are +highly tuned for different data storage use cases, interoperate with other +programming languages, and to use a safe and productive query interface, while +still keeping the type safety of Haskell datatypes.

+

Persistent follows the guiding principles of type safety and concise, +declarative syntax. Some other nice features are:

+
    +
  • +

    +Database-agnostic. There is first class support for PostgreSQL, SQLite, MySQL + and MongoDB, with experimental Redis support. +

    +
  • +
  • +

    +Convenient data modeling. + Persistent lets you model relationships and use them in type-safe ways. + The default type-safe persistent API does not support joins, allowing support for a + wider number of storage layers. + Joins and other SQL specific functionality can be achieved through using + a raw SQL layer (with very little type safety). + An additional library, Esqueleto, + builds on top of the Persistent data model, adding type-safe joins and SQL functionality. +

    +
  • +
  • +

    +Automatic database migrations in non-production environments to speed up + development. +

    +
  • +
+

Persistent works well with Yesod, but it is quite +usable on its own as a standalone library. Most of this chapter will address +Persistent on its own.

+
+

Synopsis

+

The required dependencies for the below are: persistent, persistent-sqlite and persistent-template.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Sqlite
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+    janeId <- insert $ Person "Jane Doe" Nothing
+
+    insert $ BlogPost "My fr1st p0st" johnId
+    insert $ BlogPost "One more for good measure" johnId
+
+    oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+    liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+    john <- get johnId
+    liftIO $ print (john :: Maybe Person)
+
+    delete janeId
+    deleteWhere [BlogPostAuthorId ==. johnId]
+ +
+
+

Solving the boundary issue

+

Suppose you are storing information on people in a SQL database. Your table +might look something like:

+
CREATE TABLE person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)
+

And if you are using a database like PostgreSQL, you can be guaranteed that the +database will never store some arbitrary text in your age field. (The same +cannot be said of SQLite, but let’s forget about that for now.) To mirror this +database table, you would likely create a Haskell datatype that looks something +like:

+
data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+

It looks like everything is type safe: the database schema matches our Haskell +datatypes, the database ensures that invalid data can never make it into our +data store, and everything is generally awesome. Well, until:

+
    +
  • +

    +You want to pull data from the database, and the database layer gives you the + data in an untyped format. +

    +
  • +
  • +

    +You want to find everyone older than 32, and you accidentally write "thirtytwo" + in your SQL statement. Guess what: that will compile just fine, and you won’t + find out you have a problem until runtime. +

    +
  • +
  • +

    +You decide you want to find the first 10 people alphabetically. No problem… + until you make a typo in your SQL. Once again, you don’t find out until + runtime. +

    +
  • +
+

In dynamic languages, the answer to these issues is unit testing. For +everything that can go wrong, make sure you write a test case. But as I am +sure you are aware by now, that doesn’t jive well with the Yesod approach to +things. We like to take advantage of Haskell’s strong typing to save us +wherever possible, and data storage is no exception.

+

So the question remains: how can we use Haskell’s type system to save the day?

+
+

Types

+

Like routing, there is nothing intrinsically difficult about type-safe data +access. It just requires a lot of monotonous, error prone, boiler plate code. +As usual, this means we can use the type system to keep us honest. And to avoid +some of the drudgery, we’ll use a sprinkling of Template Haskell.

+

PersistValue is the basic building block of Persistent. It is a sum type that +can represent data that gets sent to and from a database. Its definition is:

+
data PersistValue
+    = PersistText Text
+    | PersistByteString ByteString
+    | PersistInt64 Int64
+    | PersistDouble Double
+    | PersistRational Rational
+    | PersistBool Bool
+    | PersistDay Day
+    | PersistTimeOfDay TimeOfDay
+    | PersistUTCTime UTCTime
+    | 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
+

A PersistValue correlates to a column in a SQL database. In our person example +above, name and age would be our PersistValuess.

+

Each Persistent backend needs to know how to translate the relevant values into +something the database can understand. However, it would be awkward to have to +express all of our data simply in terms of these basic types. The next layer is +the PersistField typeclass, which defines how an arbitrary Haskell datatype +can be marshaled to and from a PersistValue.

+

To tie up the user side of the code, our last typeclass is PersistEntity. An +instance of PersistEntity correlates with a table in a SQL database. This +typeclass defines a number of functions and some associated types. To review, +we have the following correspondence between Persistent and SQL:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SQLPersistent

Datatypes (VARCHAR, INTEGER, etc)

PersistValue

Column

PersistField

Table

PersistEntity

+
+
+

Code Generation

+

In order to ensure that the PersistEntity instances match up properly with your +Haskell datatypes, Persistent takes responsibility for both. This is also good +from a DRY (Don’t Repeat Yourself) perspective: you only need to define your +entities once. Let’s see a quick example:

+
{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+mkPersist sqlSettings [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+

We use a combination of Template Haskell and Quasi-Quotation (like when +defining routes): persistLowerCase is a quasi-quoter which converts a +whitespace-sensitive syntax into a list of entity definitions. "Lower case" +refers to the format of the generated table names. In this scheme, an +entity like SomeTable would become the SQL table some_table. You can also +declare your entities in a separate file using persistFileWith. mkPersist +takes that list of entities and declares:

+
    +
  • +

    +One Haskell datatype for each entity. +

    +
  • +
  • +

    +A PersistEntity instance for each datatype defined. +

    +
  • +
+

The example above generates code that looks like the following:

+
{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+import Control.Applicative
+
+data Person = Person
+    { personName :: !String
+    , personAge :: !Int
+    }
+  deriving Show
+
+type PersonId = Key Person
+
+instance PersistEntity Person where
+    newtype Key Person = PersonKey (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 Unique Person
+    type PersistEntityBackend Person = SqlBackend
+
+    toPersistFields (Person name age) =
+        [ SomePersistField name
+        , SomePersistField age
+        ]
+
+    fromPersistValues [nameValue, ageValue] = Person
+        <$> fromPersistValue nameValue
+        <*> fromPersistValue ageValue
+    fromPersistValues _ = Left "Invalid fromPersistValues input"
+
+    -- Information on each field, used internally to generate SQL statements
+    persistFieldDef PersonId = FieldDef
+        (HaskellName "Id")
+        (DBName "id")
+        (FTTypeCon Nothing "PersonId")
+        SqlInt64
+        []
+        True
+        NoReference
+    persistFieldDef PersonName = FieldDef
+        (HaskellName "name")
+        (DBName "name")
+        (FTTypeCon Nothing "String")
+        SqlString
+        []
+        True
+        NoReference
+    persistFieldDef PersonAge = FieldDef
+        (HaskellName "age")
+        (DBName "age")
+        (FTTypeCon Nothing "Int")
+        SqlInt64
+        []
+        True
+        NoReference
+

As you might expect, our Person datatype closely matches the definition we +gave in the original Template Haskell version. We also have a Generalized +Algebraic Datatype (GADT) which gives a separate constructor for each field. +This GADT encodes both the type of the entity and the type of the field. We use +its constructors throughout Persistent, such as to ensure that when we apply a +filter, the types of the filtering value match the field. There’s another +associated newtype for the database primary key of this entity.

+

We can use the generated Person type like any other Haskell type, and then +pass it off to other Persistent functions.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# 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, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+runSqlite' :: (MonadUnliftIO m) => Text -> ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a
+runSqlite' = runSqlite
+
+main :: IO ()
+main = runSqlite' ":memory:" $ do
+    michaelId <- insert $ Person "Michael" $ Just 26
+    michael <- get michaelId
+    liftIO $ print michael
+ +

We start off with some standard database connection code. In this case, we used +the single-connection functions. Persistent also comes built in with connection +pool functions, which we will generally want to use in production.

+

In this example, we have seen two functions: insert creates a new record in +the database and returns its ID. Like everything else in Persistent, IDs are +type safe. We’ll get into more details of how these IDs work later. So when you +call insert $ Person "Michael" 26, it gives you a value back of type +PersonId.

+

The next function we see is get, which attempts to load a value from the +database using an Id. In Persistent, you never need to worry that you are +using the key from the wrong table: trying to load up a different entity (like +House) using a PersonId will never compile.

+
+
+

PersistStore

+

One last detail is left unexplained from the previous example: what exactly +does runSqlite do, and what is that monad that our database actions are +running in?

+

All database actions require a parameter which is an instance of +PersistStore. As its name implies, every data store (PostgreSQL, SQLite, +MongoDB) has an instance of PersistStore. This is where all the translations +from PersistValue to database-specific values occur, where SQL query +generation happens, and so on.

+ +

runSqlite creates a single connection to a database using its supplied +connection string. For our test cases, we will use :memory:, which uses an +in-memory database. All of the SQL backends share the same instance of +PersistStore: SqlBackend. runSqlite then provides the SqlBackend value +as an environment parameter to the action via runReaderT.

+ +

One important thing to note is that everything which occurs inside a single +call to runSqlite runs in a single transaction. This has two important +implications:

+
    +
  • +

    +For many databases, committing a transaction can be a costly activity. By + putting multiple steps into a single transaction, you can speed up code + dramatically. +

    +
  • +
  • +

    +If an exception is thrown anywhere inside a single call to runSqlite, all + actions will be rolled back (assuming your backend has rollback support). +

    + +
  • +
+
+
+
+

Migrations

+

I’m sorry to tell you, but so far I have lied to you a bit: the example from +the previous section does not actually work. If you try to run it, you will get +an error message about a missing table.

+

For SQL databases, one of the major pains can be managing schema changes. +Instead of leaving this to the user, Persistent steps in to help, but you have +to ask it to help. Let’s see what this looks like:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# 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, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration $ migrate entityDefs $ entityDef (Nothing :: Maybe Person)
+    michaelId <- insert $ Person "Michael" $ Just 26
+    michael <- get michaelId
+    liftIO $ print michael
+

With this one little code change, Persistent will automatically create your +Person table for you. This split between runMigration and migrate allows +you to migrate multiple tables simultaneously.

+ +

This works when dealing with just a few entities, but can quickly get tiresome +once we are dealing with a dozen entities. Instead of repeating yourself, +Persistent provides a helper function, mkMigrate:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+Car
+    color String
+    make String
+    model String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do runMigration migrateAll
+

mkMigrate is a Template Haskell function which creates a new function that +will automatically call migrate on all entities defined in the persist +block. The share function is just a little helper that passes the information +from the persist block to each Template Haskell function and concatenates the +results.

+

Persistent has very conservative rules about what it will do during a +migration. It starts by loading up table information from the database, +complete with all defined SQL datatypes. It then compares that against the +entity definition given in the code. For the following cases, it will +automatically alter the schema:

+
    +
  • +

    +The datatype of a field changed. However, the database may object to this + modification if the data cannot be translated. +

    +
  • +
  • +

    +A field was added. However, if the field is not null, no default value is + supplied (we’ll discuss defaults later) and there is already data in the + database, the database will not allow this to happen. +

    +
  • +
  • +

    +A field is converted from not null to null. In the opposite case, Persistent + will attempt the conversion, contingent upon the database’s approval. +

    +
  • +
  • +

    +A brand new entity is added. +

    +
  • +
+

However, there are some cases that Persistent will not handle:

+
    +
  • +

    +Field or entity renames: Persistent has no way of knowing that "name" has now + been renamed to "fullName": all it sees is an old field called name and a new + field called fullName. +

    +
  • +
  • +

    +Field removals: since this can result in data loss, Persistent by default + will refuse to perform the action (you can force the issue by using + runMigrationUnsafe instead of runMigration, though it is not + recommended). +

    +
  • +
+

runMigration will print out the migrations it is running on stderr (you can +bypass this by using runMigrationSilent). Whenever possible, it uses ALTER +TABLE calls. However, in SQLite, ALTER TABLE has very limited abilities, and +therefore Persistent must resort to copying the data from one table to another.

+

Finally, if instead of performing a migration, you want Persistent to give +you hints about what migrations are necessary, use the printMigration +function. This function will print out the migrations which runMigration +would perform for you. This may be useful for performing migrations that +Persistent is not capable of, for adding arbitrary SQL to a migration, or just +to log what migrations occurred.

+
+
+

Uniqueness

+

In addition to declaring fields within an entity, you can also declare +uniqueness constraints. A typical example would be requiring that a username be +unique.

+
User
+    username Text
+    UniqueUsername username
+

While each field name must begin with a lowercase letter, the uniqueness +constraints must begin with an uppercase letter, since it will be represented +in Haskell as a data constructor.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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|
+Person
+    firstName String
+    lastName String
+    age Int
+    PersonName firstName lastName
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael" "Snoyman" 26
+    michael <- getBy $ PersonName "Michael" "Snoyman"
+    liftIO $ print michael
+

To declare a unique combination of fields, we add an extra line to our +declaration. Persistent knows that it is defining a unique constructor, since +the line begins with a capital letter. Each following word must be a field in +this entity.

+

The main restriction on uniqueness is that it can only be applied to non-null +fields. The reason for this is that the SQL standard is ambiguous on how +uniqueness should be applied to NULL (e.g., is NULL=NULL true or false?). +Besides that ambiguity, most SQL engines in fact implement rules which would be +contrary to what the Haskell datatypes anticipate (e.g., PostgreSQL says that +NULL=NULL is false, whereas Haskell says Nothing == Nothing is True).

+

In addition to providing nice guarantees at the database level about +consistency of your data, uniqueness constraints can also be used to perform +some specific queries within your Haskell code, like the getBy demonstrated +above. This happens via the Unique associated type. In the example above, we +end up with a new constructor:

+
PersonName :: String -> String -> Unique Person
+ +
+
+

Queries

+

Depending on what your goal is, there are different approaches to querying the +database. Some commands query based on a numeric ID, while others will filter. +Queries also differ in the number of results they return: some lookups should +return no more than one result (if the lookup key is unique) while others can +return many results.

+

Persistent therefore provides a few different query functions. As usual, we try +to encode as many invariants in the types as possible. For example, a query +that can return only 0 or 1 results will use a Maybe wrapper, whereas a query +returning many results will return a list.

+
+

Fetching by ID

+

The simplest query you can perform in Persistent is getting based on an ID. +Since this value may or may not exist, its return type is wrapped in a Maybe.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+maybePerson <- get personId
+case maybePerson of
+    Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+    Just person -> liftIO $ print person
+

This can be very useful for sites that provide URLs like /person/5. However, +in such a case, we don’t usually care about the Maybe wrapper, and just want +the value, returning a 404 message if it is not found. Fortunately, the +get404 (provided by the yesod-persistent package) function helps us out here. +We’ll go into more details when we see integration with Yesod.

+
+
+

Fetching by unique constraint

+

getBy is almost identical to get, except:

+
    +
  1. +

    +it takes a uniqueness constraint; that is, instead of an ID it takes a Unique value. +

    +
  2. +
  3. +

    +it returns an Entity instead of a value. An Entity is a combination of database ID and value. +

    +
  4. +
+
personId <- insert $ Person "Michael" "Snoyman" 26
+maybePerson <- getBy $ PersonName "Michael" "Snoyman"
+case maybePerson of
+    Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+    Just (Entity personId person) -> liftIO $ print person
+

Like get404, there is also a getBy404 function.

+
+
+

Select functions

+

Most likely, you’re going to want more powerful queries. You’ll want to find +everyone over a certain age; all cars available in blue; all users without a +registered email address. For this, you need one of the select functions.

+

All the select functions use a similar interface, with slightly different outputs:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionReturns

selectSource

A Source 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 +recommend reading the +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.

+

selectList is the most commonly used, so we will cover it specifically. Understanding the others should be trivial after that.

+

selectList takes two arguments: a list of Filters, and a list of +SelectOpts. The former is what limits your results based on +characteristics; it allows for equals, less than, is member of, and such. +SelectOpts provides for three different features: sorting, limiting output +to a certain number of rows, and offsetting results by a certain number of +rows.

+ +

Let’s jump straight into an example of filtering, and then analyze it.

+
people <- selectList [PersonAge >. 25, PersonAge <=. 30] []
+liftIO $ print people
+

As simple as that example is, we really need to cover three points:

+
    +
  1. +

    +PersonAge is a constructor for an associated phantom type. That might sound +scary, but what’s important is that it uniquely identifies the "age" column of +the "person" table, and that it knows that the age field is an Int. (That’s +the phantom part.) +

    +
  2. +
  3. +

    +We have a bunch of Persistent filtering operators. They’re all pretty +straight-forward: just tack a period to the end of what you’d expect. There are +three gotchas here, I’ll explain below. +

    +
  4. +
  5. +

    +The list of filters is ANDed together, so that our constraint means "age is +greater than 25 AND age is less than or equal to 30". We’ll describe ORing +later. +

    +
  6. +
+

The one operator that’s surprisingly named is "not equals." We use !=., since +/=. is used for updates (for "divide-and-set", described later). Don’t worry: +if you use the wrong one, the compiler will catch you. The other two surprising +operators are the "is member" and "is not member". They are, respectively, +<-. and /<-. (both end with a period).

+

And regarding ORs, we use the ||. operator. For example:

+
people <- selectList
+    (       [PersonAge >. 25, PersonAge <=. 30]
+        ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
+        ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
+    )
+    []
+liftIO $ print people
+

This (completely nonsensical) example means: find people who are 26-30, +inclusive, OR whose names are neither Adam or Bonny, OR whose age is either 50 +or 60.

+
+

SelectOpt

+

All of our selectList calls have included an empty list as the second +parameter. That specifies no options, meaning: sort however the database wants, +return all results, and don’t skip any results. A SelectOpt has four +constructors that can be used to change all that.

+
+
+Asc +
+

+Sort by the given column in ascending order. This uses the same phantom type as filtering, such as PersonAge. +

+
+
+Desc +
+

+Same as Asc, in descending order. +

+
+
+LimitTo +
+

+Takes an Int argument. Only return up to the specified number of results. +

+
+
+OffsetBy +
+

+Takes an Int argument. Skip the specified number of results. +

+
+
+

The following code defines a function that will break down results into pages. +It returns all people aged 18 and over, and then sorts them by age (oldest +person first). For people with the same age, they are sorted alphabetically by +last name, then first name.

+
resultsForPage pageNumber = do
+    let resultsPerPage = 10
+    selectList
+        [ PersonAge >=. 18
+        ]
+        [ Desc PersonAge
+        , Asc PersonLastName
+        , Asc PersonFirstName
+        , LimitTo resultsPerPage
+        , OffsetBy $ (pageNumber - 1) * resultsPerPage
+        ]
+
+
+
+
+

Manipulation

+

Querying is only half the battle. We also need to be able to add data to and +modify existing data in the database.

+
+

Insert

+

It’s all well and good to be able to play with data in the database, but how +does it get there in the first place? The answer is the insert function. You +just give it a value, and it gives back an ID.

+

At this point, it makes sense to explain a bit of the philosophy behind +Persistent. In many other ORM solutions, the datatypes used to hold data are +opaque: you need to go through their defined interfaces to get at and modify +the data. That’s not the case with Persistent: we’re using plain old Algebraic +Data Types for the whole thing. This means you still get all the great benefits +of pattern matching, currying and everything else you’re used to.

+

However, there are a few things we can’t do. For one, there’s no way to +automatically update values in the database every time the record is updated in +Haskell. Of course, with Haskell’s normal stance of purity and immutability, +this wouldn’t make much sense anyway, so I don’t shed any tears over it.

+

However, there is one issue that newcomers are often bothered by: why are IDs +and values completely separate? It seems like it would be very logical to embed +the ID inside the value. In other words, instead of having:

+
data Person = Person { name :: String }
+

have

+
data Person = Person { personId :: PersonId, name :: String }
+

Well, there’s one problem with this right off the bat: how do we do an insert? If a Person needs to have an ID, and we get the ID by inserting, and an insert needs a Person, we have an impossible loop. We could solve this with undefined, but that’s just asking for trouble.

+

OK, you say, let’s try something a bit safer:

+
data Person = Person { personId :: Maybe PersonId, name :: String }
+

I definitely prefer insert $ Person Nothing "Michael" to insert $ Person +undefined "Michael". And now our types will be much simpler, right? For +example, selectList could return a simple [Person] instead of that ugly +[Entity SqlPersist Person].

+

The problem is that the "ugliness" is incredibly useful. Having Entity Person +makes it obvious, at the type level, that we’re dealing with a value that +exists in the database. Let’s say we want to create a link to another page that +requires the PersonId (not an uncommon occurrence as we’ll discuss later). +The Entity Person form gives us unambiguous access to that information; +embedding PersonId within Person with a Maybe wrapper means an extra +runtime check for Just, instead of a more error-proof compile time check.

+

Finally, there’s a semantic mismatch with embedding the ID within the value. +The Person is the value. Two people are identical (in the context of +Haskell) if all their fields are the same. By embedding the ID in the value, +we’re no longer talking about a person, but about a row in the database. +Equality is no longer really equality, it’s identity: is this the same +person, as opposed to an equivalent person.

+

In other words, there are some annoyances with having the ID separated out, but +overall, it’s the right approach, which in the grand scheme of things leads +to better, less buggy code.

+
+
+

Update

+

Now, in the context of that discussion, let’s think about updating. The simplest way to update is:

+
let michael = Person "Michael" 26
+    michaelAfterBirthday = michael { personAge = 27 }
+

But that’s not actually updating anything, it’s just creating a new Person +value based on the old one. When we say update, we’re not talking about +modifications to the values in Haskell. (We better not be of course, since +data in Haskell is immutable.)

+

Instead, we’re looking at ways of modifying rows in a table. And the simplest +way to do that is with the update function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+update personId [PersonAge =. 27]
+

update takes two arguments: an ID, and a list of Updates. The simplest +update is assignment, but it’s not always the best. What if you want to +increase someone’s age by 1, but you don’t have their current age? Persistent +has you covered:

+
haveBirthday personId = update personId [PersonAge +=. 1]
+

And as you might expect, we have all the basic mathematical operators: ++=., -=., *=., and /=. (full stop). These can be convenient for +updating a single record, but they are also essential for proper ACID +guarantees. Imagine the alternative: pull out a Person, increment the age, +and update the new value. If you have two threads/processes working on this +database at the same time, you’re in for a world of hurt (hint: race +conditions).

+

Sometimes you’ll want to update many rows at once (give all your employees a +5% pay increase, for example). updateWhere takes two parameters: a list of +filters, and a list of updates to apply.

+
updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- it's been a long day
+

Occasionally, you’ll just want to completely replace the value in a database +with a different value. For that, you use (surprise) the replace function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+replace personId $ Person "John" "Doe" 20
+
+
+

Delete

+

As much as it pains us, sometimes we must part with our data. To do so, we have three functions:

+
+
+delete +
+

+Delete based on an ID +

+
+
+deleteBy +
+

+Delete based on a unique constraint +

+
+
+deleteWhere +
+

+Delete based on a set of filters +

+
+
+
personId <- insert $ Person "Michael" "Snoyman" 26
+delete personId
+deleteBy $ PersonName "Michael" "Snoyman"
+deleteWhere [PersonFirstName ==. "Michael"]
+

We can even use deleteWhere to wipe out all the records in a table, we just +need to give some hints to GHC as to what table we’re interested in:

+
    deleteWhere ([] :: [Filter Person])
+
+
+
+

Attributes

+

So far, we have seen a basic syntax for our persistLowerCase blocks: a line +for the name of our entities, and then an indented line for each field with two +words: the name of the field and the datatype of the field. Persistent handles +more than this: you can assign an arbitrary list of attributes after the first +two words on a line.

+

Suppose we want to have a Person entity with an (optional) age and the +timestamp of when he/she was added to the system. For entities already in the +database, we want to just use the current date-time for that timestamp.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+import Control.Monad.IO.Class
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    time <- liftIO getCurrentTime
+    runMigration migrateAll
+    insert $ Person "Michael" (Just 26) time
+    insert $ Person "Greg" Nothing time
+    return ()
+

Maybe is a built in, single word attribute. It makes the field optional. In +Haskell, this means it is wrapped in a Maybe. In SQL, it makes the column +nullable.

+

The default attribute is backend specific, and uses whatever syntax is +understood by the database. In this case, it uses the database’s built-in +CURRENT_TIME function. Suppose that we now want to add a field for a person’s +favorite programming language:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+ +

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:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    country String "default='El Salvador'"
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+

One last trick you can do with attributes is to specify the names to be used +for the SQL tables and columns. This can be convenient when interacting with +existing databases.

+
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"
+    PersonName firstName lastName
+    deriving Show
+|]
+

There are a number of other features to the entity definition syntax. An +up-to-date list is maintained +in the Persistent documentation.

+
+
+

Relations

+

Persistent allows references between your data types in a manner that is +consistent with supporting non-SQL databases. We do this by embedding an ID in +the related entity. So if a person has many cars:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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
+    name String
+    deriving Show
+Car
+    ownerId PersonId
+    name String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    bruce <- insert $ Person "Bruce Wayne"
+    insert $ Car bruce "Bat Mobile"
+    insert $ Car bruce "Porsche"
+    -- this could go on a while
+    cars <- selectList [CarOwnerId ==. bruce] []
+    liftIO $ print cars
+

Using this technique, you can define one-to-many relationships. To define +many-to-many relationships, we need a join entity, which has a one-to-many +relationship with each of the original tables. It is also a good idea to use +uniqueness constraints on these. For example, to model a situation where we +want to track which people have shopped in which stores:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+Store
+    name String
+PersonStore
+    personId PersonId
+    storeId StoreId
+    UniquePersonStore personId storeId
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    bruce <- insert $ Person "Bruce Wayne"
+    michael <- insert $ Person "Michael"
+
+    target <- insert $ Store "Target"
+    gucci <- insert $ Store "Gucci"
+    sevenEleven <- insert $ Store "7-11"
+
+    insert $ PersonStore bruce gucci
+    insert $ PersonStore bruce sevenEleven
+
+    insert $ PersonStore michael target
+    insert $ PersonStore michael sevenEleven
+
+    return ()
+
+
+

Closer look at types

+

So far, we’ve spoken about Person and PersonId without really explaining +what they are. In the simplest sense, for a SQL-only system, the PersonId +could just be type PersonId = Int64. However, that means there is nothing +binding a PersonId at the type level to the Person entity. As a result, you +could accidentally use a PersonId and get a Car. In order to model this +relationship, we could use phantom types. So, our next naive step would be:

+
newtype Key entity = Key Int64
+type PersonId = Key Person
+

And that works out really well, until you get to a backend that doesn’t use +Int64 for its IDs. And that’s not just a theoretical question; MongoDB uses +ByteStrings instead. So what we need is a key value that can contain an +Int and a ByteString. Seems like a great time for a sum type:

+
data Key entity = KeyInt Int64 | KeyByteString ByteString
+

But that’s just asking for trouble. Next we’ll have a backend that uses +timestamps, so we’ll need to add another constructor to Key. This could go on +for a while. Fortunately, we already have a sum type intended for representing +arbitrary data: PersistValue:

+
newtype Key entity = Key PersistValue
+

And this is (more or less) what Persistent did until version 2.0. However, this +has a different problem: it throws away data. For example, when dealing with a +SQL database, we know that the key type will be an Int64 (assuming defaults +are being used). However, you can’t assert that at the type level with this +construction. So instead, starting with Persistent 2.0, we now use an +associated datatype inside the PersistEntity class:

+
class PersistEntity record where
+    data Key record
+    ...
+

When you’re working with a SQL backend, and aren’t using a custom key type, +this becomes a newtype wrapper around an Int64, and the +toSqlKey/fromSqlKey functions can perform that type-safe conversion for +you. With MongoDB, on the other hand, it’s a wrapper around a ByteString.

+
+

More complicated, more generic

+

By default, Persistent will hard-code your datatypes to work with a specific +database backend. When using sqlSettings, this is the SqlBackend type. But +if you want to write Persistent code that can be used on multiple backends, you +can enable more generic types by replacing sqlSettings with sqlSettings { +mpsGeneric = True }.

+

To understand why this is necessary, consider relations. Let’s say we want to +represent blogs and blog posts. We would use the entity definition:

+
Blog
+    title Text
+Post
+    title Text
+    blogId BlogId
+

We know that BlogId is just a type synonym for Key Blog, but how will Key +Blog be defined? We can’t use an Int64, since that won’t work for MongoDB. +And we can’t use ByteString, since that won’t work for SQL databases.

+

To allow for this, once mpsGeneric is set to True, our resulting datatypes have a type parameter to indicate the database backend they use, so that keys can be properly encoded. This looks like:

+
data BlogGeneric backend = Blog { blogTitle :: Text }
+data PostGeneric backend = Post
+    { postTitle  :: Text
+    , postBlogId :: Key (BlogGeneric backend)
+    }
+

Notice that we still keep the short names for the constructors and the records. +Finally, to give a simple interface for normal code, we define some type +synonyms:

+
type Blog   = BlogGeneric SqlBackend
+type BlogId = Key Blog
+type Post   = PostGeneric SqlBackend
+type PostId = Key Post
+

And no, SqlBackend isn’t hard-coded into Persistent anywhere. That +sqlSettings parameter you’ve been passing to mkPersist is what tells us to +use SqlBackend. Mongo code will use mongoSettings instead.

+

This might be quite complicated under the surface, but user code hardly ever +touches this. Look back through this whole chapter: not once did we need to +deal with the Key or Generic stuff directly. The most common place for it +to pop up is in compiler error messages. So it’s important to be aware that +this exists, but it shouldn’t affect you on a day-to-day basis.

+
+
+
+

Custom Fields

+

Occasionally, you will want to define a custom field to be used in your +datastore. The most common case is an enumeration, such as employment status. +For this, Persistent provides a helper Template Haskell function:

+
-- @Employment.hs
+{-# LANGUAGE TemplateHaskell #-}
+module Employment where
+
+import Database.Persist.TH
+
+data Employment = Employed | Unemployed | Retired
+    deriving (Show, Read, Eq)
+derivePersistField "Employment"
+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Employment
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    employment Employment
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    insert $ Person "Bruce Wayne" Retired
+    insert $ Person "Peter Parker" Unemployed
+    insert $ Person "Michael" Employed
+
+    return ()
+

derivePersistField stores the data in the database using a string field, and +performs marshaling using the Show and Read instances of the datatype. This +may not be as efficient as storing via an integer, but it is much more future +proof: even if you add extra constructors in the future, your data will still +be valid.

+ +
+
+

Persistent: Raw SQL

+

The Persistent package provides a type safe interface to data stores. It tries +to be backend-agnostic, such as not relying on relational features of SQL. My +experience has been you can easily perform 95% of what you need to do with the +high-level interface. (In fact, most of my web apps use the high level +interface exclusively.)

+

But occasionally you’ll want to use a feature that’s specific to a backend. One feature I’ve used in the past is full text search. In this case, we’ll use the SQL "LIKE" operator, which is not modeled in Persistent. We’ll get all people with the last name "Snoyman" and print the records out.

+ +
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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
+    name Text
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael Snoyman"
+    insert $ Person "Miriam Snoyman"
+    insert $ Person "Eliezer Snoyman"
+    insert $ Person "Gavriella Snoyman"
+    insert $ Person "Greg Weber"
+    insert $ Person "Rick Richardson"
+
+    -- 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)
+

There is also higher-level support that allows for automated data marshaling. +Please see the Haddock API docs for more details.

+
+
+

Integration with Yesod

+

So you’ve been convinced of the power of Persistent. How do you integrate it +with your Yesod application? If you use the scaffolding, most of the work is +done for you already. But as we normally do, we’ll build up everything manually +here to point out how it works under the surface.

+

The yesod-persistent package provides the meeting point between Persistent and +Yesod. It provides the YesodPersist typeclass, which standardizes access to +the database via the runDB method. Let’s see this in action.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import Yesod
+import Database.Persist.Sqlite
+import Control.Monad.Trans.Resource (runResourceT)
+import Control.Monad.Logger (runStderrLoggingT)
+
+-- Define our entities as usual
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    firstName String
+    lastName String
+    age Int
+    deriving Show
+|]
+
+-- We keep our connection pool in the foundation. At program initialization, we
+-- create our initial pool, and each time we need to perform an action we check
+-- out a single connection from the pool.
+data PersistTest = PersistTest ConnectionPool
+
+-- We'll create a single route, to access a person. It's a very common
+-- occurrence to use an Id type in routes.
+mkYesod "PersistTest" [parseRoutes|
+/ HomeR GET
+/person/#PersonId PersonR GET
+|]
+
+-- Nothing special here
+instance Yesod PersistTest
+
+-- Now we need to define a YesodPersist instance, which will keep track of
+-- which backend we're using and how to run an action.
+instance YesodPersist PersistTest where
+    type YesodPersistBackend PersistTest = SqlBackend
+
+    runDB action = do
+        PersistTest pool <- getYesod
+        runSqlPool action pool
+
+-- List all people in the database
+getHomeR :: Handler Html
+getHomeR = do
+    people <- runDB $ selectList [] [Asc PersonAge]
+    defaultLayout
+        [whamlet|
+            <ul>
+                $forall Entity personid person <- people
+                    <li>
+                        <a href=@{PersonR personid}>#{personFirstName person}
+        |]
+
+-- We'll just return the show value of a person, or a 404 if the Person doesn't
+-- exist.
+getPersonR :: PersonId -> Handler String
+getPersonR personId = do
+    person <- runDB $ get404 personId
+    return $ show person
+
+openConnectionCount :: Int
+openConnectionCount = 10
+
+main :: IO ()
+main = runStderrLoggingT $ withSqlitePool "test.db3" openConnectionCount $ \pool -> liftIO $ do
+    runResourceT $ flip runSqlPool pool $ do
+        runMigration migrateAll
+        insert $ Person "Michael" "Snoyman" 26
+    warp 3000 $ PersistTest pool
+

There are two important pieces here for general use. runDB is used to run a +DB action from within a Handler. Within the runDB, you can use any of the +functions we’ve spoken about so far, such as insert and selectList.

+ +

The other new feature is get404. It works just like get, but instead of +returning a Nothing when a result can’t be found, it returns a 404 message +page. The getPersonR function is a very common approach used in real-world +Yesod applications: get404 a value and then return a response based on it.

+
+
+

More complex SQL

+

Persistent strives to be backend-agnostic. The advantage of this approach is +code which easily moves from different backend types. The downside is that you +lose out on some backend-specific features. Probably the biggest casualty is +SQL join support.

+

Fortunately, thanks to Felipe Lessa and Chris Allen, you can have your cake and eat it too. The +Esqueleto library provides +support for writing type safe SQL queries, using the existing Persistent +infrastructure. The Haddocks for that package provide a good introduction to +its usage. And since it uses many Persistent concepts, most of your existing +Persistent knowledge should transfer over easily.

+

For a simple example of using Esqueleto, please see the SQL Joins chapter.

+
+
+

Something besides SQLite

+

To keep the examples in this chapter simple, we’ve used the SQLite backend. Just to round things out, here’s our original synopsis rewritten to work with PostgreSQL:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Control.Monad.Logger    (runStderrLoggingT)
+import           Database.Persist
+import           Database.Persist.Postgresql
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+connStr = "host=localhost dbname=test user=test password=test port=5432"
+
+main :: IO ()
+main = runStderrLoggingT $ withPostgresqlPool connStr 10 $ \pool -> liftIO $ do
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        johnId <- insert $ Person "John Doe" $ Just 35
+        janeId <- insert $ Person "Jane Doe" Nothing
+
+        insert $ BlogPost "My fr1st p0st" johnId
+        insert $ BlogPost "One more for good measure" johnId
+
+        oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+        liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+        john <- get johnId
+        liftIO $ print (john :: Maybe Person)
+
+        delete janeId
+        deleteWhere [BlogPostAuthorId ==. johnId]
+
+
+

Summary

+

Persistent brings the type safety of Haskell to your data access layer. Instead +of writing error-prone, untyped data access, or manually writing boilerplate +marshal code, you can rely on Persistent to automate the process for you.

+

The goal is to provide everything you need, most of the time. For the times +when you need something a bit more powerful, Persistent gives you direct access +to the underlying data store, so you can write whatever 5-way joins you want.

+

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

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/restful-content.html b/public/book-1.6/restful-content.html new file mode 100644 index 00000000..5d7db0f3 --- /dev/null +++ b/public/book-1.6/restful-content.html @@ -0,0 +1,600 @@ + RESTful Content :: Yesod Web Framework Book- Version 1.6 +
+

RESTful Content

+ + +

One of the stories from the early days of the web is how search engines wiped +out entire websites. When dynamic web sites were still a new concept, +developers didn’t appreciate the difference between a GET and POST request. +As a result, they created pages- accessed with the GET method- that would +delete pages. When search engines started crawling these sites, they could wipe +out all the content.

+

If these web developers had followed the HTTP spec properly, this would not +have happened. A GET request is supposed to cause no side effects (you know, +like wiping out a site). Recently, there has been a move in web development to +properly embrace Representational State Transfer, also known as REST. This +chapter describes the RESTful features in Yesod and how you can use them to +create more robust web applications.

+
+

Request methods

+

In many web frameworks, you write one handler function per resource. In Yesod, +the default is to have a separate handler function for each request method. The +two most common request methods you will deal with in creating web sites are +GET and POST. These are the most well-supported methods in HTML, since they +are the only ones supported by web forms. However, when creating RESTful APIs, +the other methods are very useful.

+

Technically speaking, you can create whichever request methods you like, but it +is strongly recommended to stick to the ones spelled out in the HTTP spec. The +most common of these are:

+
+
+GET +
+

+Read-only requests. Assuming no other changes occur on the server, +calling a GET request multiple times should result in the same response, +barring such things as "current time" or randomly assigned results. +

+
+
+POST +
+

+A general mutating request. A POST request should never be submitted +twice by the user. A common example of this would be to transfer funds from one +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. +

+
+
+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 +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.

+

See the routing and handlers chapter for more information on the syntax +of defining handler functions for each request method.

+
+
+

Representations

+

Suppose we have a Haskell datatype and value:

+
data Person = Person { name :: String, age :: Int }
+michael = Person "Michael" 25
+

We could represent that data as HTML:

+
<table>
+    <tr>
+        <th>Name</th>
+        <td>Michael</td>
+    </tr>
+    <tr>
+        <th>Age</th>
+        <td>25</td>
+    </tr>
+</table>
+

or we could represent it as JSON:

+
{"name":"Michael","age":25}
+

or as XML:

+
<person>
+    <name>Michael</name>
+    <age>25</age>
+</person>
+

Often times, web applications will use a different URL to get each of these +representations; perhaps /person/michael.html, /person/michael.json, etc. +Yesod follows the RESTful principle of a single URL for each resource. So in +Yesod, all of these would be accessed from /person/michael.

+

Then the question becomes how do we determine which representation to serve. +The answer is the HTTP Accept header: it gives a prioritized list of content +types the client is expecting. Yesod provides a pair of functions to abstract +away the details of parsing that header directly, and instead allows you to +talk at a much higher level of representations. Let’s make that last sentence +a bit more concrete with some code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideRep $ return $ object
+        [ "name" .= name
+        , "age" .= age
+        ]
+  where
+    name = "Michael" :: Text
+    age = 28 :: Int
+
+main :: IO ()
+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 +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 +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 +little bit.

+

To test this out, start up the server and then try running the following +different curl commands:

+
curl http://localhost:3000 --header "accept: application/json"
+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 +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 +"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 +from your browser. To try this out, you can visit +http://localhost:3000/?_accept=application/json.

+
+

JSON conveniences

+

Since JSON is such a commonly used data format in web applications today, we +have some built-in helper functions for providing JSON representations. These +are built off of the wonderful aeson library, so let’s start off with a quick +explanation of how that library works.

+

aeson has a core datatype, Value, which represents any valid JSON value. It +also provides two typeclasses- ToJSON and FromJSON- to automate +marshaling to and from JSON values, respectively. For our purposes, we’re +currently interested in ToJSON. Let’s look at a quick example of creating a +ToJSON instance for our ever-recurring Person data type examples.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+import           Data.Aeson
+import qualified Data.ByteString.Lazy.Char8 as L
+import           Data.Text                  (Text)
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+main :: IO ()
+main = L.putStrLn $ encode $ Person "Michael" 28
+

I won’t go into further detail on aeson, as +the Haddock documentation +already provides a great introduction to the library. What I’ve described so +far is enough to understand our convenience functions.

+

Let’s suppose that you have such a Person datatype, with a corresponding +value, and you’d like to use it as the representation for your current page. +For that, you can use the returnJson function.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Value
+getHomeR = returnJson $ Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

returnJson is actually a trivial function; it is implemented as return . +toJSON. However, it makes things just a bit more convenient. Similarly, if you +would like to provide a JSON value as a representation inside a selectRep, +you can use provideJson.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideJson person
+  where
+    person@Person {..} = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

provideJson is similarly trivial, in this case provideRep . return . toEncoding.

+
+
+

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 +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.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+getHomeR :: Handler TypedContent
+getHomeR =
+    return $ TypedContent mimeType $ toContent $ show person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

There are a few important things to note here.

+
    +
  • +

    +We’ve used the toContent function. This is a typeclass function that can + convert a number of data types to raw data ready to be sent over the wire. In + this case, we’ve used the instance for String, which uses UTF8 encoding. + Other common data types with instances are Text, ByteString, Html, and + 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 + alias for a strict ByteString. +

    +
  • +
+

That’s all well and good, but it bothers me that the type signature for +getHomeR is so uninformative. Also, the implementation of getHomeR looks +pretty boilerplate. I’d rather just have a datatype representing "Haskell Show" +data, and provide some simple means of creating such values. Let’s try this on +for size:

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+
+getHomeR :: Handler HaskellShow
+getHomeR =
+    return $ HaskellShow person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

The magic here lies in two typeclasses. As we mentioned before, ToContent +tells how to convert a value into a raw response. In our case, we would like to +show the original value to get a String, and then convert that String +into the raw content. Often times, instances of ToContent will build on each +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.

+

Finally, let’s make this a bit more complicated, and get this to play well with +selectRep.

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE RecordWildCards           #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+instance HasContentType HaskellShow where
+    getContentType _ = mimeType
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return $ HaskellShow person
+    provideJson person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+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. +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.

+ +
+
+
+

Other request headers

+

There are a great deal of other request headers available. Some of them only +affect the transfer of data between the server and client, and should not +affect the application at all. For example, Accept-Encoding informs the +server which compression schemes the client understands, and Host informs the +server which virtual host to serve up.

+

Other headers do affect the application, but are automatically read by Yesod. +For example, the Accept-Language header specifies which human language +(English, Spanish, German, Swiss-German) the client prefers. See the i18n +chapter for details on how this header is used.

+
+
+

Summary

+

Yesod adheres to the following tenets of REST:

+
    +
  • +

    +Use the correct request method. +

    +
  • +
  • +

    +Each resource should have precisely one URL. +

    +
  • +
  • +

    +Allow multiple representations of data on the same URL. +

    +
  • +
  • +

    +Inspect request headers to determine extra information about what the client wants. +

    +
  • +
+

This makes it easy to use Yesod not just for building websites, but for +building APIs. In fact, using techniques such as selectRep/provideRep, you +can serve both a user-friendly, HTML page and a machine-friendly, JSON page +from the same URL.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/route-attributes.html b/public/book-1.6/route-attributes.html new file mode 100644 index 00000000..2c39a058 --- /dev/null +++ b/public/book-1.6/route-attributes.html @@ -0,0 +1,321 @@ + Route attributes :: Yesod Web Framework Book- Version 1.6 +
+

Route attributes

+ + +

Route attributes allow you to set some metadata on each of your routes, in the +routes description itself. The syntax is trivial: just an exclamation point +followed by a value. Using it is also trivial: just use the routeAttrs +function.

+

It’s easiest to understand how it all fits together, and when you might want it, with a motivating example. The case I personally most use this for is annotating administrative routes. Imagine having a website with about 12 different admin actions. You could manually add a call to requireAdmin or some such at the beginning of each action, but:

+
    +
  1. +

    +It’s tedious. +

    +
  2. +
  3. +

    +It’s error prone: you could easily forget one. +

    +
  4. +
  5. +

    +Worse yet, it’s not easy to notice that you’ve missed one. +

    +
  6. +
+

Modifying your isAuthorized method with an explicit list of administrative +routes is a bit better, but it’s still difficult to see at a glance when you’ve +missed one.

+

This is why I like to use route attributes for this: you add a single word to +each relevant part of the route definition, and then you just check for that +attribute in isAuthorized. Let’s see the code!

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/unprotected UnprotectedR GET
+/admin1 Admin1R GET !admin
+/admin2 Admin2R GET !admin
+/admin3 Admin3R GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+    isAuthorized route _writable
+        | "admin" `member` routeAttrs route = do
+            muser <- maybeAuthId
+            case muser of
+                Nothing -> return AuthenticationRequired
+                Just ident
+                    -- Just a hack since we're using the dummy module
+                    | ident == "admin" -> return Authorized
+                    | otherwise -> return $ Unauthorized "Admin access only"
+        | otherwise = return Authorized
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Hacky YesodAuth instance for just the dummy auth plugin
+instance YesodAuth App where
+    type AuthId App = Text
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authPlugins _ = [authDummy]
+    maybeAuthId = lookupSession credsKey
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Route attr homepage"
+    [whamlet|
+        <p>
+            <a href=@{UnprotectedR}>Unprotected
+        <p>
+            <a href=@{Admin1R}>Admin 1
+        <p>
+            <a href=@{Admin2R}>Admin 2
+        <p>
+            <a href=@{Admin3R}>Admin 3
+    |]
+
+getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
+getUnprotectedR = defaultLayout [whamlet|Unprotected|]
+getAdmin1R = defaultLayout [whamlet|Admin1|]
+getAdmin2R = defaultLayout [whamlet|Admin2|]
+getAdmin3R = defaultLayout [whamlet|Admin3|]
+
+main :: IO ()
+main = warp 3000 App
+

And it was so glaring, I bet you even caught the security hole about Admin3R.

+
+

Alternative approach: hierarchical routes

+

Another approach that can be used in some cases is hierarchical routes. This +allows you to group a number of related routes under a single parent. If you +want to keep all of your admin routes under a single URL structure (e.g., +/admin), this can be a good solution. Using them is fairly simple. You need +to add a line to your routes declaration with a path, a name, and a colon, +e.g.:

+
/admin AdminR:
+

Then, you place all children routes beneath that line, and indented at least one space, e.g.:

+
    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+

To refer to these routes using type-safe URLs, you simply wrap them with the +AdminR constructor, e.g. AdminR Admin1R. Here is the previous route +attribute example rewritten to use hierarchical routes:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/unprotected UnprotectedR GET
+/admin AdminR:
+    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+    isAuthorized (AdminR _) _writable = do
+        muser <- maybeAuthId
+        case muser of
+            Nothing -> return AuthenticationRequired
+            Just ident
+                -- Just a hack since we're using the dummy module
+                | ident == "admin" -> return Authorized
+                | otherwise -> return $ Unauthorized "Admin access only"
+    isAuthorized _route _writable = return Authorized
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Hacky YesodAuth instance for just the dummy auth plugin
+instance YesodAuth App where
+    type AuthId App = Text
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authPlugins _ = [authDummy]
+    maybeAuthId = lookupSession credsKey
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Route attr homepage"
+    [whamlet|
+        <p>
+            <a href=@{UnprotectedR}>Unprotected
+        <p>
+            <a href=@{AdminR Admin1R}>Admin 1
+        <p>
+            <a href=@{AdminR Admin2R}>Admin 2
+        <p>
+            <a href=@{AdminR Admin3R}>Admin 3
+    |]
+
+getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
+getUnprotectedR = defaultLayout [whamlet|Unprotected|]
+getAdmin1R = defaultLayout [whamlet|Admin1|]
+getAdmin2R = defaultLayout [whamlet|Admin2|]
+getAdmin3R = defaultLayout [whamlet|Admin3|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Hierarchical routes with attributes

+

Of course, you can mix the two approaches. Children of a hierarchical route will inherit the attributes of their parents, e.g.:

+
/admin AdminR !admin:
+    /1 Admin1R GET !1
+    /2 Admin2R GET !2
+    /3 Admin3R GET !3
+

AdminR Admin1R has the admin and 1 attributes.

+

With this technique, you can use the admin attributes in the isAuthorized function, like in the first example. +You are also sure that you won’t forget any attributes as we did with Admin3R. +Compared to the original code corresponding to the hierarchical route, this method has no real benefit : both methods being somehow equivalent. +We replaced the pattern matching on (AdminR _) with "admin" member routeAttrs route. +However, the benefit becomes more obvious when the admin pages are not all grouped under the same url structures but belong to different subtrees, e.g:

+
/admin AdminR !admin:
+    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+
+/a AR !a:
+  /1 A1R GET
+  /2 A2R GET
+  /admin AAdminR !admin:
+    /1 AAdmin1R GET
+    /2 AAdmin2R GET
+

The pages under /admin and /a/admin have all the admin attribute and can be checked using "admin" member routeAttrs route. Pattern matching on (AdminR _) will not work for this example and only match /admin/* routes but not /a/admin/\*

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/routing-and-handlers.html b/public/book-1.6/routing-and-handlers.html new file mode 100644 index 00000000..c7a4f1f2 --- /dev/null +++ b/public/book-1.6/routing-and-handlers.html @@ -0,0 +1,814 @@ + Routing and Handlers :: Yesod Web Framework Book- Version 1.6 +
+

Routing and Handlers

+ + +

If we look at Yesod as a Model-View-Controller framework, routing and handlers +make up the controller. For contrast, let’s describe two other routing +approaches used in other web development environments:

+
    +
  • +

    +Dispatch based on file name. This is how PHP and ASP work, for example. +

    +
  • +
  • +

    +Have a centralized routing function that parses routes based on regular + expressions. Django and Rails follow this approach. +

    +
  • +
+

Yesod is closer in principle to the latter technique. Even so, there are +significant differences. Instead of using regular expressions, Yesod matches on +pieces of a route. Instead of having a one-way route-to-handler mapping, Yesod +has an intermediate data type (called the route datatype, or a type-safe URL) +and creates two-way conversion functions.

+

Coding this more advanced system manually is tedious and error prone. +Therefore, Yesod defines a Domain Specific Language (DSL) for specifying +routes, and provides Template Haskell functions to convert this DSL to Haskell +code. This chapter will explain the syntax of the routing declarations, give +you a glimpse of what code is generated for you, and explain the interaction +between routing and handler functions.

+
+

Route Syntax

+

Instead of trying to shoe-horn route declarations into an existing syntax, +Yesod’s approach is to use a simplified syntax designed just for routes. This +has the advantage of making the code not only easy to write, but simple enough +for someone with no Yesod experience to read and understand the sitemap of your +application.

+

A basic example of this syntax is:

+
/             HomeR     GET
+/blog         BlogR     GET POST
+/blog/#BlogId BlogPostR GET POST
+
+/static       StaticR   Static getStatic
+

The next few sections will explain the full details of what goes on in the +route declaration.

+
+

Pieces

+

One of the first things Yesod does when it gets a request is split up the +requested path into pieces. The pieces are tokenized at all forward slashes. +For example:

+
toPieces "/" = []
+toPieces "/foo/bar/baz/" = ["foo", "bar", "baz", ""]
+

You may notice that there are some funny things going on with trailing slashes, +or double slashes ("/foo//bar//"), or a few other things. Yesod believes in +having canonical URLs; if users request a URL with a trailing slash, or with a +double slash, they are automatically redirected to the canonical version. This +ensures you have one URL for one resource, and can help with your search +rankings.

+

What this means for you is that you needn’t concern yourself with the exact +structure of your URLs: you can safely think about pieces of a path, and Yesod +automatically handles intercalating the slashes and escaping problematic +characters.

+

If, by the way, you want more fine-tuned control of how paths are split into +pieces and joined together again, you’ll want to look at the cleanPath and +joinPath methods in the Yesod typeclass chapter.

+
+

Types of Pieces

+

When you are declaring your routes, you have three types of pieces at your +disposal:

+
+
+Static +
+

+This is a plain string that must be matched against precisely in the URL. +

+
+
+Dynamic single +
+

+This is a single piece (ie, between two forward slashes), but +represents a user-submitted value. This is the primary method of receiving +extra user input on a page request. These pieces begin with a hash (#) and are +followed by a data type. The datatype must be an instance of PathPiece. +

+
+
+Dynamic multi +
+

+The same as before, but can receive multiple pieces of the URL. +This must always be the last piece in a resource pattern. It is specified by an +asterisk (*) followed by a datatype, which must be an instance of +PathMultiPiece. Multi pieces are not as common as the other two, though they +are very important for implementing features like static trees representing +file structure or wikis with arbitrary hierarchies. +

+
+
+ +

Let us take a look at some standard kinds of resource patterns you may want to +write. Starting simply, the root of an application will just be /. Similarly, +you may want to place your FAQ at /page/faq.

+

Now let’s say you are going to write a Fibonacci website. You may construct +your URLs like /fib/#Int. But there’s a slight problem with this: we do not +want to allow negative numbers or zero to be passed into our application. +Fortunately, the type system can protect us:

+
newtype Natural = Natural Int
+    deriving (Eq, Show, Read)
+
+instance PathPiece Natural where
+    toPathPiece (Natural i) = T.pack $ show i
+    fromPathPiece s =
+        case reads $ T.unpack s of
+            (i, ""):_
+                | i < 1 -> Nothing
+                | otherwise -> Just $ Natural i
+            [] -> Nothing
+

On line 1 we define a simple newtype wrapper around Int to protect ourselves +from invalid input. We can see that PathPiece is a typeclass with two +methods. toPathPiece does nothing more than convert to a Text. +fromPathPiece attempts to convert a Text to our datatype, returning +Nothing when this conversion is impossible. By using this datatype, we can +ensure that our handler function is only ever given natural numbers, allowing +us to once again use the type system to battle the boundary issue.

+ +

Defining a PathMultiPiece is just as simple. Let’s say we want to have a Wiki +with at least two levels of hierarchy; we might define a datatype such as:

+
data Page = Page Text Text [Text] -- 2 or more
+    deriving (Eq, Show, Read)
+
+instance PathMultiPiece Page where
+    toPathMultiPiece (Page x y z) = x : y : z
+    fromPathMultiPiece (x:y:z) = Just $ Page x y z
+    fromPathMultiPiece _ = Nothing
+
+
+

Overlap checking

+

By default, Yesod will ensure that no two routes have the potential to overlap +with each other. So, for example, consider the following routes:

+
/foo/bar   Foo1R GET
+/foo/#Text Foo2R GET
+

This route declaration will be rejected as overlapping, since /foo/bar will +match both routes. However, there are two cases where we may wish to allow +overlapping:

+
    +
  1. +

    +We know by the definition of our datatype that the overlap can never happen. For example, if you replace Text with Int above, it’s easy to convince yourself that there’s no route that exists that will overlap. Yesod is currently not capable of performing such an analysis. +

    +
  2. +
  3. +

    +You have some extra knowledge about how your application operates, and know that such a situation should never be allowed. For example, if the Foo2R route should never be allowed to receive the parameter bar. +

    +
  4. +
+

You can turn off overlap checking by using the exclamation mark at the +beginning of your route. For example, the following will be accepted by Yesod:

+
/foo/bar    Foo1R GET
+!/foo/#Int  Foo2R GET
+!/foo/#Text Foo3R GET
+ +

One issue that overlapping routes introduce is ambiguity. In the example above, +should /foo/bar route to Foo1R or Foo3R? And should /foo/42 route to +Foo2R or Foo3R? Yesod’s rule for this is simple: first route wins.

+
+
+

Empty #String or #Text as dynamic piece

+

Consider the following route declaration:

+
/hello          HelloR     GET
+/hello/#String  HelloNameR GET
+

Let’s say a user requests the path /hello/ – which handler would respond to the request?

+

It will be HelloR because Yesod’s dispatch mechanism removes trailing slashes and +redirects to the canonical form of the URL.

+

If users actually want to address the HelloNameR handler with an +empty string as argument they need to request the path /hello/- +instead. Yesod automatically converts the Minus sign to the empty string.

+

Likewise, the resulting URL for @{HelloNameR ""} would be /hello/-.

+

Also, to disambiguate a single actual -, Yesod prefixes that piece with another Minus sign when rendering the URL. +Consequently, Yesod also prefixes any string consisting only of Minus signs with one single Minus sign.

+ +
+
+
+

Resource name

+

Each resource pattern also has a name associated with it. That name will become +the constructor for the type safe URL datatype associated with your +application. Therefore, it has to start with a capital letter. By convention, +these resource names all end with a capital R. There is nothing forcing you to +do this, it is just common practice.

+

The exact definition of our constructor depends upon the resource pattern it is +attached to. Whatever datatypes are used as single-pieces or multi-pieces of the +pattern become arguments to the datatype. This gives us a 1-to-1 correspondence +between our type-safe URL values and valid URLs in our application.

+ +

Let’s get some real examples going here. If you had the resource patterns +/person/#Text named PersonR, /year/#Int named YearR and /page/faq +named FaqR, you would end up with a route data type roughly looking like:

+
data MyRoute = PersonR Text
+             | YearR Int
+             | FaqR
+

If a user requests /year/2009, Yesod will convert it into the value YearR +2009. /person/Michael becomes PersonR "Michael" and /page/faq becomes +FaqR. On the other hand, /year/two-thousand-nine, /person/michael/snoyman +and /page/FAQ would all result in 404 errors without ever seeing your code.

+
+
+

Handler specification

+

The last piece of the puzzle when declaring your resources is how they will be +handled. There are three options in Yesod:

+
    +
  • +

    +A single handler function for all request methods on a given route. +

    +
  • +
  • +

    +A separate handler function for each request method on a given route. Any + other request method will generate a 405 Method Not Allowed response. +

    +
  • +
  • +

    +You want to pass off to a subsite. +

    +
  • +
+

The first two can be easily specified. A single handler function will be a line +with just a resource pattern and the resource name, such as /page/faq FaqR. +In this case, the handler function must be named handleFaqR.

+

A separate handler for each request method will be the same, plus a list of +request methods. The request methods must be all capital letters. For example, +/person/#String PersonR GET POST DELETE. In this case, you would need to +define three handler functions: getPersonR, postPersonR and +deletePersonR.

+

Subsites are a very useful— but more complicated— topic in Yesod. We will cover +writing subsites later, but using them is not too difficult. The most commonly +used subsite is the static subsite, which serves static files for your +application. In order to serve static files from /static, you would need a +resource line like:

+
/static StaticR Static getStatic
+

In this line, /static just says where in your URL structure to serve the +static files from. There is nothing magical about the word static, you could +easily replace it with /my/non-dynamic/files.

+

The next word, StaticR, gives the resource name. The next two words +specify that we are using a subsite. Static is the name of the subsite +foundation datatype, and getStatic is a function that gets a Static value +from a value of your master foundation datatype.

+

Let’s not get too caught up in the details of subsites now. We will look more +closely at the static subsite in the scaffolded site chapter.

+
+
+
+

Dispatch

+

Once you have specified your routes, Yesod will take care of all the pesky +details of URL dispatch for you. You just need to make sure to provide the +appropriate handler functions. For subsite routes, you do not need to write any +handler functions, but you do for the other two. We mentioned the naming rules +above (MyHandlerR GET becomes getMyHandlerR, MyOtherHandlerR becomes +handleMyOtherHandlerR).

+

Now that we know which functions we need to write, let’s figure out what their +type signatures should be.

+
+

Return Type

+

Let’s look at a simple handler function:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

There are two components to this return type: Handler and Html. Let’s +analyze each in more depth.

+
+

Handler monad

+

Like the Widget type, the Handler data type is not defined anywhere in the +Yesod libraries. Instead, the libraries provide the data type:

+
data HandlerT site m a
+

And like WidgetT, this has three arguments: a base monad m, a monadic value +a, and the foundation data type site. Each application defines a Handler +synonym which constrains site to that application’s foundation data type, and +sets m to IO. If your foundation is MyApp, in other words, you’d have the +synonym:

+
type Handler = HandlerT MyApp IO
+

We need to be able to modify the underlying monad when writing subsites, but +otherwise we’ll use IO.

+

The HandlerT monad provides access to information about the user request +(e.g. query-string parameters), allows modifying the response (e.g., response +headers), and more. This is the monad that most of your Yesod code will live +in.

+

In addition, there’s a type class called MonadHandler. Both HandlerT and +WidgetT are instances of this type class, allowing many common functions to +be used in both monads. If you see MonadHandler in any API documentation, you +should remember that the function can be used in your Handler functions.

+
+
+

Html

+

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 +can be returned?

+

In order to generate a response, we need to know two pieces of information: +the content type (e.g., text/html, image/png) and how to serialize it to a +stream of bytes. This is represented by the TypedContent data type:

+
data TypedContent = TypedContent !ContentType !Content
+

We also have a type class for all data types which can be converted to a +TypedContent:

+
class ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

Many common data types are instances of this type class, including Html, +Value (from the aeson package, representing JSON), Text, and even () (for +representing an empty response).

+
+
+
+

Arguments

+

Let’s return to our simple example from above:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

Not every route is as simple as this HomeR. Take for instance our PersonR +route from earlier. The name of the person needs to be passed to the handler +function. This translation is very straight-forward, and hopefully intuitive. +For example:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/person/#Text PersonR GET
+/year/#Integer/month/#Text/day/#Int DateR
+/wiki/*Texts WikiR GET
+|]
+
+instance Yesod App
+
+getPersonR :: Text -> Handler Html
+getPersonR name = defaultLayout [whamlet|<h1>Hello #{name}!|]
+
+handleDateR :: Integer -> Text -> Int -> Handler Text -- text/plain
+handleDateR year month day =
+    return $
+        T.concat [month, " ", T.pack $ show day, ", ", T.pack $ show year]
+
+getWikiR :: [Text] -> Handler Text
+getWikiR = return . T.unwords
+
+main :: IO ()
+main = warp 3000 App
+

The arguments have the types of the dynamic pieces for each route, in the order +specified. Also, notice how we are able to use both Html and Text return +values.

+
+
+
+

The Handler functions

+

Since the majority of your code will live in the Handler monad, it’s +important to invest some time in understanding it better. The remainder of this +chapter will give a brief introduction to some of the most common functions +living in the Handler monad. I am specifically not covering any of the +session functions; that will be addressed in the sessions chapter.

+
+

Application Information

+

There are a number of functions that return information about your application +as a whole, and give no information about individual requests. Some of these +are:

+
+
+getYesod +
+

+Returns your application foundation value. If you store configuration +values in your foundation, you will probably end up using this function a lot. +(If you’re so inclined, you can also use ask from Control.Monad.Reader; +getYesod is simply a type-constrained synonym for it.) +

+
+
+getUrlRender +
+

+Returns the URL rendering function, which converts a type-safe +URL into a Text. Most of the time- like with Hamlet- Yesod calls this +function for you, but you may occasionally need to call it directly. +

+
+
+getUrlRenderParams +
+

+A variant of getUrlRender that converts both a type-safe +URL and a list of query-string parameters. This function handles all +percent-encoding necessary. +

+
+
+
+
+

Request Information

+

The most common information you will want to get about the current request is +the requested path, the query string parameters and POSTed form data. The +first of those is dealt with in the routing, as described above. The other two +are best dealt with using the forms module.

+

That said, you will sometimes need to get the data in a more raw format. For +this purpose, Yesod exposes the YesodRequest datatype along with the +getRequest function to retrieve it. This gives you access to the full list of +GET parameters, cookies, and preferred languages. There are some convenient +functions to make these lookups easier, such as lookupGetParam, +lookupCookie and languages. For raw access to the POST parameters, you +should use runRequestBody.

+

If you need even more raw data, like request headers, you can use waiRequest +to access the Web Application Interface (WAI) request value. See the WAI +appendix for more details.

+
+
+

Short Circuiting

+

The following functions immediately end execution of a handler function and +return a result to the user.

+
+
+redirect +
+

+Sends a redirect response to the user (a 303 response). If you want to use a different response code (e.g., a permanent 301 redirect), you can use redirectWith. +

+
+
+ +
+
+notFound +
+

+Return a 404 response. This can be useful if a user requests a +database value that doesn’t exist. +

+
+
+permissionDenied +
+

+Return a 403 response with a specific error message. +

+
+
+invalidArgs +
+

+A 400 response with a list of invalid arguments. +

+
+
+sendFile +
+

+Sends a file from the filesystem with a specified content type. This +is the preferred way to send static files, since the underlying WAI handler may +be able to optimize this to a sendfile system call. Using readFile for +sending static files should not be necessary. +

+
+
+sendResponse +
+

+Send a normal response with a 200 status code. This is really +just a convenience for when you need to break out of some deeply nested code +with an immediate response. Any instance of ToTypedContent may be used. +

+
+
+sendWaiResponse +
+

+When you need to get low-level and send out a raw WAI +response. This can be especially useful for creating streaming responses or a +technique like server-sent events. +

+
+
+
+
+

Response Headers

+
+
+setCookie +
+

+Set a cookie on the client. Instead of taking an expiration date, +this function takes a cookie duration in minutes. Remember, you won’t see this +cookie using lookupCookie until the following request. +

+
+
+deleteCookie +
+

+Tells the client to remove a cookie. Once again, lookupCookie +will not reflect this change until the next request. +

+
+
+addHeader +
+

+Set an arbitrary response header. +

+
+
+setLanguage +
+

+Set the preferred user language, which will show up in the result +of the languages function. +

+
+
+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 +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. +

+
+
+alreadyExpired +
+

+Sets the Expires header to the past. +

+
+
+expiresAt +
+

+Sets the Expires header to the specified date/time. +

+
+
+
+
+
+

I/O and debugging

+

The HandlerT and WidgetT monad transformers are both instances of a number +of typeclasses. For this section, the important typeclasses are MonadIO and +MonadLogger. The former allows you to perform arbitrary IO actions inside +your handler, such as reading from a file. In order to achieve this, you just +need to prepend liftIO to the call.

+

MonadLogger provides a built-in logging system. There are many ways you can +customize this system, including what messages get logged and where logs are +sent. By default, logs are sent to standard output, in development all messages +are logged, and in production, warnings and errors are logged.

+

Often times when logging, we want to know where in the source code the logging +occured. For this, MonadLogger provides a number of convenience Template +Haskell functions which will automatically insert source code location into the +log messages. These functions are $logDebug, $logInfo, $logWarn, and +$logError. Let’s look at a short example of some of these functions.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Exception (IOException, try)
+import           Control.Monad     (when)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    -- This function controls which messages are logged
+    shouldLogIO App src level =
+        return True -- good for development
+        -- level == LevelWarn || level == LevelError -- good for production
+
+getHomeR :: Handler Html
+getHomeR = do
+    $logDebug "Trying to read data file"
+    edata <- liftIO $ try $ readFile "datafile.txt"
+    case edata :: Either IOException String of
+        Left e -> do
+            $logError "Could not read datafile.txt"
+            defaultLayout [whamlet|An error occurred|]
+        Right str -> do
+            $logInfo "Reading of data file succeeded"
+            let ls = lines str
+            when (length ls < 5) $ $logWarn "Less than 5 lines of data"
+            defaultLayout
+                [whamlet|
+                    <ol>
+                        $forall l <- ls
+                            <li>#{l}
+                |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Query string and hash fragments

+

We’ve looked at a number of functions which work on URL-like things, such as redirect. These functions all work with type-safe URLs, but what else do they work with? There’s a typeclass called RedirectUrl which contains the logic for converting some type into a textual URL. This includes type-safe URLs, textual URLs, and two special instances:

+
    +
  1. +

    +A tuple of a URL and a list of key/value pairs of query string parameters. +

    +
  2. +
  3. +

    +The Fragment datatype, used for adding a hash fragment to the end of a URL. +

    +
  4. +
+

Both of these instances allow you to "add on" extra information to a type-safe +URL. Let’s see some examples of how these can be used:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text        (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+/link1 Link1R GET
+/link2 Link2R GET
+/link3 Link3R GET
+/link4 Link4R GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Redirects"
+    [whamlet|
+        <p>
+            <a href=@{Link1R}>Click to start the redirect chain!
+    |]
+
+getLink1R, getLink2R, getLink3R :: Handler ()
+getLink1R = redirect Link2R -- /link2
+getLink2R = redirect (Link3R, [("foo", "bar")]) -- /link3?foo=bar
+getLink3R = redirect $ Link4R :#: ("baz" :: Text) -- /link4#baz
+
+getLink4R :: Handler Html
+getLink4R = defaultLayout
+    [whamlet|
+        <p>You made it!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Of course, inside a Hamlet template this is usually not necessary, as you can simply include the hash after the URL directly, e.g.:

+
<a href=@{Link1R}#somehash>Link to hash
+
+
+

Summary

+

Routing and dispatch is arguably the core of Yesod: it is from here that our +type-safe URLs are defined, and the majority of our code is written within the +Handler monad. This chapter covered some of the most important and central +concepts of Yesod, so it is important that you properly digest it.

+

This chapter also hinted at a number of more complex Yesod topics that we will +be covering later. But you should be able to write some very sophisticated web +applications with just the knowledge you have learned up until here.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/scaffolding-and-the-site-template.html b/public/book-1.6/scaffolding-and-the-site-template.html new file mode 100644 index 00000000..a1392b3e --- /dev/null +++ b/public/book-1.6/scaffolding-and-the-site-template.html @@ -0,0 +1,545 @@ + Scaffolding and the Site Template :: Yesod Web Framework Book- Version 1.6 +
+

Scaffolding and the Site Template

+ + +

So you’re tired of running small examples, and ready to write a real site? Then +you’re at the right chapter. Even with the entire Yesod library at your +fingertips, there are still a lot of steps you need to go through to get a +production-quality site setup:

+
    +
  • +

    +Config file parsing +

    +
  • +
  • +

    +Signal handling (*nix) +

    +
  • +
  • +

    +More efficient static file serving +

    +
  • +
  • +

    +A good file layout +

    +
  • +
+

The scaffolded site is a combination of many Yesoders' best practices combined +together into a ready-to-use skeleton for your sites. It is highly recommended +for all sites. This chapter will explain the overall structure of the +scaffolding, how to use it, and some of its less-than-obvious features.

+

For the most part, this chapter will not contain code samples. It is +recommended that you follow along with an actual scaffolded site.

+ +
+

How to Scaffold

+

The yesod-bin package installs an executable (conveniently named yesod as +well). This executable provides a few commands (run stack exec -- yesod +--help to get a list). In order to generate a scaffolding, the command is +stack new my-project yesodweb/postgres && cd my-project. This will generate a +scaffolding site with a postgres database backend in a directory named +my-project. You can see the other available templates using the command +stack templates.

+ +

The key thing differing in various available templates (from the +stack templates command) is the database backend. You get a few +choices here, including SQL backends, as well as +"yesodweb/simple" template to include no database support. This last +option also turns off a few extra dependencies, giving you a leaner +overall site. The remainder of this chapter will focus on the +scaffoldings for one of the database backends. There will be minor +differences for the yesodweb/simple backend.

+

After creating your files, install the yesod command line tool inside +the project: stack install yesod-bin --install-ghc. Then do a stack +build inside the directory. In particular, the commands provided +will ensure that any missing dependencies are built and installed.

+

Finally, to launch your development site, you would use stack exec -- yesod devel. +This site will automatically rebuild and reload whenever you change your code.

+
+
+

File Structure

+

The scaffolded site is built as a fully cabalized Haskell package. In addition +to source files, config files, templates, and static files are produced as +well.

+
+

Cabal file

+

Whether directly using stack, or indirectly using stack exec -- yesod devel, building +your code will always go through the cabal file. If you open the file, you’ll +see that there are both library and executable blocks. If the library-only +flag is turned on, then the executable block is not built. This is how yesod +devel calls your app. Otherwise, the executable is built.

+

The library-only flag should only be used by yesod devel; you should never +be explicitly passing it into cabal. There is an additional flag, dev, that +allows cabal to build an executable, but turns on some of the same features as +the library-only flag, i.e., no optimizations and reload versions of the +Shakespearean template functions.

+

In general, you will build as follows:

+
    +
  • +

    +When developing, use yesod devel exclusively. +

    +
  • +
  • +

    +When building a production build, perform stack clean && + stack build. This will produce an optimized executable in your + dist folder. (You can also use the yesod keter command for + this.) +

    +
  • +
+

You might be surprised to see the NoImplicitPrelude extension. We turn this +on since the site includes its own module, Import, with a few changes to the +Prelude that make working with Yesod a little more convenient.

+

The last thing to note is the exposed-modules list. If you add any modules to +your application, you must update this list to get yesod devel to work +correctly. Unfortunately, neither Cabal nor GHC will give you a warning if you +forgot to make this update, and instead you’ll get a very scary-looking error +message from yesod devel.

+
+
+

Routes and entities

+

Multiple times in this book, you’ve seen a comment like “We’re declaring our +routes/entities with quasiquotes for convenience. In a production site, you +should use an external file.” The scaffolding uses such an external file.

+

Routes are defined in config/routes, and entities in config/models. They +have the exact same syntax as the quasiquoting you’ve seen throughout the book, +and yesod devel knows to automatically recompile the appropriate modules when +these files change.

+

The models files is referenced by Model.hs. You are free to declare +whatever you like in this file, but here are some guidelines:

+
    +
  • +

    +Any data types used in entities must be imported/declared in Model.hs, + above the persistFileWith call. +

    +
  • +
  • +

    +Helper utilities should either be declared in Import.hs or, if very + model-centric, in a file within the Model folder and imported into + Import.hs. +

    +
  • +
+
+
+

Foundation and Application modules

+

The mkYesod function which we have used throughout the book declares a few +things:

+
    +
  • +

    +Route type +

    +
  • +
  • +

    +Route render function +

    +
  • +
  • +

    +Dispatch function +

    +
  • +
+

The dispatch function refers to all of the handler functions, and therefore all +of those must either be defined in the same file as the dispatch function, or +be imported into the module containing the dispatch function.

+

Meanwhile, the handler functions will almost certainly refer to the route type. +Therefore, they must be either in the same file where the route type is +defined, or must import that file. If you follow the logic here, your entire +application must essentially live in a single file!

+

Clearly this isn’t what we want. So instead of using mkYesod, the scaffolding +site uses a decomposed version of the function. Foundation.hs calls +mkYesodData, which declares the route type and render function. Since it does +not declare the dispatch function, the handler functions need not be in scope. +Import.hs imports Foundation.hs, and all the handler modules import +Import.hs.

+

In Application.hs, we call mkYesodDispatch, which creates our dispatch +function. For this to work, all handler functions must be in scope, so be sure +to add an import statement for any new handler modules you create.

+

Other than that, Application.hs is pretty simple. It provides two primary +functions: getApplicationDev is used by yesod devel to launch your app, and +makeApplication is used by the executable to launch.

+

Foundation.hs is much more exciting. It:

+
    +
  • +

    +Declares your foundation datatype +

    +
  • +
  • +

    +Declares a number of instances, such as Yesod, YesodAuth, and + YesodPersist +

    +
  • +
  • +

    +Imports the messages files. If you look for the line starting with + mkMessage, you will see that it specifies the folder containing the + messages (messages/) and the default language (en, for English). +

    +
  • +
+

This is the right file for adding extra instances for your foundation, such as +YesodAuthEmail or YesodBreadcrumbs.

+

We’ll be referring back to this file later, as we discussed some of the special +implementations of Yesod typeclass methods.

+
+
+

Import

+

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’m always adding the same five import statements (Data.Text, + Control.Applicative, etc) to every handler module. +

    +
  • +
  • +

    +I want to make sure I never use some evil function (head, readFile, …) from Prelude. +

    +
  • +
+ +

The solution is to turn on the NoImplicitPrelude language extension, +re-export the parts of Prelude we want, add in all the other stuff we want, +define our own functions as well, and then import this file in all handlers.

+ +
+
+

Handler modules

+

Handler modules should go inside the Handler folder. The site template +includes one module: Handler/Home.hs. How you split up your handler functions +into individual modules is your decision, but a good rule of thumb is:

+
    +
  • +

    +Different methods for the same route should go in the same file, e.g. + getBlogR and postBlogR. +

    +
  • +
  • +

    +Related routes can also usually go in the same file, e.g., getPeopleR and + getPersonR. +

    +
  • +
+

Of course, it’s entirely up to you. When you add a new handler file, make sure +you do the following:

+
    +
  • +

    +Add it to version control (you are using version control, right?). +

    +
  • +
  • +

    +Add it to the cabal file. +

    +
  • +
  • +

    +Add it to the Application.hs file. +

    +
  • +
  • +

    +Put a module statement at the top, and an import Import line below it. +

    +
  • +
+

You can use the stack exec -- yesod add-handler command to automate the last three steps.

+
+
+
+

widgetFile

+

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.

+

If you have a handler function:

+
getHomeR = defaultLayout $(widgetFile "homepage")
+

, Yesod will look for the following files:

+
    +
  • +

    +templates/homepage.hamlet +

    +
  • +
  • +

    +templates/homepage.lucius +

    +
  • +
  • +

    +templates/homepage.cassius +

    +
  • +
  • +

    +templates/homepage.julius +

    +
  • +
+

If any of those files are present, they will be automatically included in the +output.

+ +
+
+

defaultLayout

+

One of the first things you’re going to want to customize is the look of your +site. The layout is actually broken up into two files:

+
    +
  • +

    +templates/default-layout-wrapper.hamlet contains just the basic shell of a + page. This file is interpreted as plain Hamlet, not as a Widget, and + therefore cannot refer to other widgets, embed i18n strings, or add extra + CSS/JS. +

    +
  • +
  • +

    +templates/default-layout.hamlet is where you would put the bulk of your + page. You must remember to include the widget value in the page, as that + contains the per-page contents. This file is interpreted as a Widget. +

    +
  • +
+

Also, since default-layout is included via the widgetFile function, any +Lucius, Cassius, or Julius files named default-layout.* will automatically be +included as well.

+
+
+

Static files

+

The scaffolded site automatically includes the static file subsite, optimized +for serving files that will not change over the lifetime of the current build. +What this means is that:

+
    +
  • +

    +When your static file identifiers are generated (e.g., static/mylogo.png + becomes mylogo_png), a query-string parameter is added to it with a hash of + the contents of the file. All of this happens at compile time. +

    +
  • +
  • +

    +When yesod-static serves your static files, it sets expiration headers far + in the future, and includes an etag based on a hash of your content. +

    +
  • +
  • +

    +Whenever you embed a link to mylogo_png, the rendering includes the + query-string parameter. If you change the logo, recompile, and launch your + new app, the query string will have changed, causing users to ignore the + cached copy and download a new version. +

    +
  • +
+

Additionally, you can set a specific static root in your Settings.hs file to +serve from a different domain name. This has the advantage of not requiring +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 +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:

+
    +
  1. +

    +Caching works properly. +

    +
  2. +
  3. +

    +Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with the same hash already exists. +

    +
  4. +
+

Finally, all of your Javascript is automatically minified via hjsmin.

+
+
+

Environment variables

+

Scaffolded apps are preconfigured to support these environment variables :

+
    +
  • +

    +YESOD_STATIC_DIR : Path of static directory. +

    +
  • +
  • +

    +YESOD_HOST : Host/interface the server should bind to. +

    +
  • +
  • +

    +YESOD_PORT : Port to listen on. +

    +
  • +
  • +

    +YESOD_IP_FROM_HEADER : Get the IP address from the header when logging. Useful when sitting behind a reverse proxy. +

    +
  • +
+

They are listed in file /config/settings.yml, where their default values can be defined. +Other variables can be added by uncommenting dedicated lines :

+
    +
  • +

    +YESOD_APPROOT : Explicit base for all generated URLs. +

    +
  • +
  • +

    +PORT : Port for Keter. +

    +
  • +
+
+
+

Conclusion

+

The purpose of this chapter was not to explain every line that exists in the +scaffolded site, but instead to give a general overview to how it works. The +best way to become more familiar with it is to jump right in and start writing +a Yesod site with it.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/sessions.html b/public/book-1.6/sessions.html new file mode 100644 index 00000000..7774868c --- /dev/null +++ b/public/book-1.6/sessions.html @@ -0,0 +1,487 @@ + Sessions :: Yesod Web Framework Book- Version 1.6 +
+

Sessions

+ + +

HTTP is a stateless protocol. While some view this as a disadvantage, advocates +of RESTful web development laud this as a plus. When state is removed from the +picture, we get some automatic benefits, such as easier scalability and +caching. You can draw many parallels with the non-mutable nature of Haskell in +general.

+

As much as possible, RESTful applications should avoid storing state about an +interaction with a client. However, it is sometimes unavoidable. Features like +shopping carts are the classic example, but other more mundane interactions +like proper login handling can be greatly enhanced by correct usage of sessions.

+

This chapter will describe how Yesod stores session data, how you can access +this data, and some special functions to help you make the most of sessions.

+
+

Clientsession

+

One of the earliest packages spun off from Yesod was clientsession. This +package uses encryption and signatures to store data in a client-side cookie. +The encryption prevents the user from inspecting the data, and the signature +ensures that the session cannot be tampered with.

+

It might sound like a bad idea from an efficiency standpoint to store data in a +cookie. After all, this means that the data must be sent on every request. +However, in practice, clientsession can be a great boon for performance.

+
    +
  • +

    +No server side database lookup is required to service a request. +

    +
  • +
  • +

    +We can easily scale horizontally: each request contains all the information + we need to send a response. +

    +
  • +
  • +

    +To avoid undue bandwidth overhead, production sites can serve their static + content from a separate domain name, thereby skipping transmission of the + session cookie for each request. +

    +
  • +
+

Storing megabytes of information in the session will be a bad idea. But for +that matter, most session implementations recommend against such practices. If +you really need massive storage for a user, it is best to store a lookup key in +the session, and put the actual data in a database.

+

All of the interaction with clientsession is handled by Yesod internally, but +there are a few spots where you can tweak the behavior just a bit.

+
+
+

Controlling sessions

+

By default, your Yesod application will use clientsession for its session +storage, getting the encryption key from the client client-session-key.aes +and giving a session a two hour timeout. (Note: timeout is measured from the +last time the client sent a request to the site, not from when then session +was first created.) However, all of those points can be modified by overriding +the makeSessionBackend method in the Yesod typeclass.

+

One simple way to override this method is to simply turn off session handling; +to do so, return Nothing. If your app has absolutely no session needs, +disabling them can give a bit of a performance increase. But be careful about +disabling sessions: this will also disable such features as Cross-Site Request +Forgery protection.

+
instance Yesod App where
+    makeSessionBackend _ = return Nothing
+

Another common approach is to modify the filepath or timeout value, but +continue using client-session. To do so, use the defaultClientSessionBackend +helper function:

+
instance Yesod App where
+    makeSessionBackend _ =
+        fmap Just $ defaultClientSessionBackend minutes filepath
+      where minutes = 24 * 60 -- 1 day
+            filepath = "mykey.aes"
+

There are a few other functions to grant you more fine-grained control of +client-session, but they will rarely be necessary. Please see Yesod.Core's +documentation if you are interested. It’s also possible to implement some other +form of session, such as a server side session. To my knowledge, at the time of +writing, no other such implementations exist.

+ +
+
+

Hardening via SSL

+

Client sessions over HTTP have an inherent hijacking vulnerability: an attacker +can read the unencrypted traffic, obtain the session cookie from it, and then +make requests to the site with that same cookie to impersonate the user. This +vulnerability is particularly severe if the sessions include any personally +identifiable information or authentication material.

+

The only sure way to defeat this threat is to run your entire site over SSL, +and prevent browsers from attempting to access it over HTTP. You can achieve +the first part of this at the webserver level, either via an SSL solution in +Haskell such as warp-tls, or by using an SSL-enabled load balancer like +Amazon Elastic Load Balancer.

+

To prevent your site from sending cookies over insecure connections, you should +augment your application’s sessions as well as the default yesodMiddleware +implementation with some additional behavior: Apply the sslOnlySessions +transformation to your makeSessionBackend, and compose the +sslOnlyMiddleware transformation with your yesodMiddleware implementation.

+
instance Yesod App where
+    makeSessionBackend _ = sslOnlySessions $
+        fmap Just $ defaultClientSessionBackend 120 "mykey.aes"
+    yesodMiddleware = (sslOnlyMiddleware 120) . defaultYesodMiddleware
+

sslOnlySessions causes all session cookies to be set with the Secure bit on, +so that browsers will not transmit them over HTTP. sslOnlyMiddleware adds a +Strict-Transport-Security header to all responses, which instructs browsers not +to make HTTP requests to your domain or its subdomains for the specified number +of minutes. Be sure to set the timeout for the sslOnlyMiddleware to be at +least as long as your session timeout. Used together, these measures will +ensure that session cookies are not transmitted in the clear.

+
+
+

Session Operations

+

Like most frameworks, a session in Yesod is a key-value store. The base session +API boils down to four functions: lookupSession gets a value for a key (if +available), getSession returns all of the key/value pairs, setSession sets +a value for a key, and deleteSession clears a value for a key.

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+import           Control.Applicative ((<$>), (<*>))
+import qualified Web.ClientSession   as CS
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+getHomeR :: Handler Html
+getHomeR = do
+    sess <- getSession
+    defaultLayout
+        [whamlet|
+            <form method=post>
+                <input type=text name=key>
+                <input type=text name=val>
+                <input type=submit>
+            <h1>#{show sess}
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    (key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
+    case mval of
+        Nothing -> deleteSession key
+        Just val -> setSession key val
+    liftIO $ print (key, mval)
+    redirect HomeR
+
+instance Yesod App where
+    -- Make the session timeout 1 minute so that it's easier to play with
+    makeSessionBackend _ = do
+        backend <- defaultClientSessionBackend 1 "keyfile.aes"
+        return $ Just backend
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Messages

+

One usage of sessions previously alluded to is messages. They come to solve a +common problem in web development: the user performs a POST request, the web +app makes a change, and then the web app wants to simultaneously redirect the +user to a new page and send the user a success message. (This is known as +Post/Redirect/Get.)

+

Yesod provides a pair of functions to enable this workflow: setMessage stores +a value in the session, and getMessage both reads the value most recently put +into the session, and clears the old value so it is not displayed twice.

+

It is recommended to have a call to getMessage in defaultLayout so that any +available message is shown to a user immediately, without having to add +getMessage calls to every handler.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/            HomeR       GET
+/set-message SetMessageR POST
+|]
+
+instance Yesod App where
+    defaultLayout widget = do
+        pc <- widgetToPageContent widget
+        mmsg <- getMessage
+        withUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <p>Your message was: #{msg}
+                        ^{pageBody pc}
+            |]
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{SetMessageR}>
+            My message is: #
+            <input type=text name=message>
+            <button>Go
+    |]
+
+postSetMessageR :: Handler ()
+postSetMessageR = do
+    msg <- runInputPost $ ireq textField "message"
+    setMessage $ toHtml msg
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+

Initial page load, no message

+ + + + + + +
+

New message entered in text box

+ + + + + + +
+

After form submit, message appears at top of page

+ + + + + + +
+

After refresh, the message is cleared

+ + + + + + +
+
+
+

Ultimate Destination

+

Not to be confused with a horror film, ultimate destination is a technique +originally developed for Yesod’s authentication framework, but which has more +general usefulness. Suppose a user requests a page that requires +authentication. If the user is not yet logged in, you need to send him/her to +the login page. A well-designed web app will then send them back to the first +page they requested. That’s what we call the ultimate destination.

+

redirectUltDest sends the user to the ultimate destination set in his/her +session, clearing that value from the session. It takes a default destination +as well, in case there is no destination set. For setting the session, there +are three options:

+
    +
  • +

    +setUltDest sets the destination to the given URL, which can be given + either as a textual URL or a type-safe URL. +

    +
  • +
  • +

    +setUltDestCurrent sets the destination to the currently requested URL. +

    +
  • +
  • +

    +setUltDestReferer sets the destination based on the Referer header (the + page that led the user to the current page). +

    +
  • +
+

Additionally, there is the clearUltDest function, to drop the ultimate +destination value from the session if present.

+

Let’s look at a small sample app. It will allow the user to set his/her name in +the session, and then tell the user his/her name from another route. If the +name hasn’t been set yet, the user will be redirected to the set name page, +with an ultimate destination set to come back to the current page.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/         HomeR     GET
+/setname  SetNameR  GET POST
+/sayhello SayHelloR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{SetNameR}>Set your name
+        <p>
+            <a href=@{SayHelloR}>Say hello
+    |]
+
+-- Display the set name form
+getSetNameR :: Handler Html
+getSetNameR = defaultLayout
+    [whamlet|
+        <form method=post>
+            My name is #
+            <input type=text name=name>
+            . #
+            <input type=submit value="Set name">
+    |]
+
+-- Retrieve the submitted name from the user
+postSetNameR :: Handler ()
+postSetNameR = do
+    -- Get the submitted name and set it in the session
+    name <- runInputPost $ ireq textField "name"
+    setSession "name" name
+
+    -- After we get a name, redirect to the ultimate destination.
+    -- If no destination is set, default to the homepage
+    redirectUltDest HomeR
+
+getSayHelloR :: Handler Html
+getSayHelloR = do
+    -- Lookup the name value set in the session
+    mname <- lookupSession "name"
+    case mname of
+        Nothing -> do
+            -- No name in the session, set the current page as
+            -- the ultimate destination and redirect to the
+            -- SetName page
+            setUltDestCurrent
+            setMessage "Please tell me your name"
+            redirect SetNameR
+        Just name -> defaultLayout [whamlet|<p>Welcome #{name}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Summary

+

Sessions are the primary means by which we bypass the statelessness imposed by +HTTP. We shouldn’t consider this an escape hatch to perform whatever actions we +want: statelessness in web applications is a virtue, and we should respect it +whenever possible. However, there are specific cases where it is vital to +retain some state.

+

The session API in Yesod is very simple. It provides a key-value store, and a +few convenience functions built on top for common use cases. If used properly, +with small payloads, sessions should be an unobtrusive part of your web +development.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/settings-types.html b/public/book-1.6/settings-types.html new file mode 100644 index 00000000..b679b858 --- /dev/null +++ b/public/book-1.6/settings-types.html @@ -0,0 +1,195 @@ + Settings Types :: Yesod Web Framework Book- Version 1.6 +
+

Settings Types

+ + +

Let’s say you’re writing a webserver. You want the server to take a port to +listen on, and an application to run. So you create the following function:

+
run :: Int -> Application -> IO ()
+

But suddenly you realize that some people will want to customize their timeout +durations. So you modify your API:

+
run :: Int -> Int -> Application -> IO ()
+

So, which Int is the timeout, and which is the port? Well, you could create +some type aliases, or comment your code. But there’s another problem creeping +into our code: this run function is getting unmanageable. Soon we’ll need to +take an extra parameter to indicate how exceptions should be handled, and then +another one to control which host to bind to, and so on.

+

A more extensible solution is to introduce a settings datatype:

+
data Settings = Settings
+    { settingsPort :: Int
+    , settingsHost :: String
+    , settingsTimeout :: Int
+    }
+

And this makes the calling code almost self-documenting:

+
run Settings
+    { settingsPort = 8080
+    , settingsHost = "127.0.0.1"
+    , settingsTimeout = 30
+    } myApp
+

Great, couldn’t be clearer, right? True, but what happens when you have 50 +settings to your webserver. Do you really want to have to specify all of those +each time? Of course not. So instead, the webserver should provide a set of +defaults:

+
defaultSettings = Settings 3000 "127.0.0.1" 30
+

And now, instead of needing to write that long bit of code above, we can get +away with:

+
run defaultSettings { settingsPort = 8080 } myApp -- (1)
+

This is great, except for one minor hitch. Let’s say we now decide to add an +extra record to Settings. Any code out in the wild looking like this:

+
run (Settings 8080 "127.0.0.1" 30) myApp -- (2)
+

will be broken, since the Settings constructor now takes 4 arguments. The +proper thing to do would be to bump the major version number so that dependent +packages don’t get broken. But having to change major versions for every minor +setting you add is a nuisance. The solution? Don’t export the Settings +constructor:

+
module MyServer
+    ( Settings
+    , settingsPort
+    , settingsHost
+    , settingsTimeout
+    , run
+    , defaultSettings
+    ) where
+

With this approach, no one can write code like (2), so you can freely add new +records without any fear of code breaking.

+

The one downside of this approach is that it’s not immediately obvious from the +Haddocks that you can actually change the settings via record syntax. That’s +the point of this chapter: to clarify what’s going on in the libraries that use +this technique.

+

I personally use this technique in a few places, feel free to have a look at +the Haddocks to see what I mean.

+
    +
  • +

    +Warp: Settings +

    +
  • +
  • +

    +http-conduit: Request and ManagerSettings +

    +
  • +
  • +

    +xml-conduit +

    +
  • +
  • +

    +Parsing: ParseSettings +

    +
  • +
  • +

    +Rendering: RenderSettings +

    +
  • +
+

As a tangential issue, http-conduit and xml-conduit actually create +instances of the Default typeclass instead of declaring a brand new +identifier. This means you can just type def instead of +defaultParserSettings.

+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/shakespearean-templates.html b/public/book-1.6/shakespearean-templates.html new file mode 100644 index 00000000..11623718 --- /dev/null +++ b/public/book-1.6/shakespearean-templates.html @@ -0,0 +1,1067 @@ + Shakespearean Templates :: Yesod Web Framework Book- Version 1.6 +
+

Shakespearean Templates

+ + +

Yesod uses the Shakespearean family of template languages as its standard +approach to HTML, CSS and Javascript creation. This language family shares some +common syntax, as well as overarching principles:

+
    +
  • +

    +As little interference to the underlying language as possible, while +providing conveniences where unobtrusive. +

    +
  • +
  • +

    +Compile-time guarantees on well-formed content. +

    +
  • +
  • +

    +Static type safety, greatly helping the prevention of + XSS (cross-site + scripting) attacks. +

    +
  • +
  • +

    +Automatic validation of interpolated links, whenever possible, through type-safe + URLs. +

    +
  • +
+

There is nothing inherently tying Yesod to these languages, or the other way +around: each can be used independently of the other. This chapter will address +these template languages on their own, while the remainder of the book will use +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 +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 +Javascript; the only added feature is variable interpolation.

+ +
+

Hamlet (HTML)

+
$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle} - My Site
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1 .page-title>#{pageTitle}
+        <p>Here is a list of your friends:
+        $if null friends
+            <p>Sorry, I lied, you don't have any friends.
+        $else
+            <ul>
+                $forall Friend name age <- friends
+                    <li>#{name} (#{age} years old)
+        <footer>^{copyright}
+
+
+

Lucius (CSS)

+
section.blog {
+    padding: 1em;
+    border: 1px solid #000;
+    h1 {
+        color: #{headingColor};
+        background-image: url(@{MyBackgroundR});
+    }
+}
+
+
+

Cassius (CSS)

+

The following is equivalent to the Lucius example above.

+
section.blog
+    padding: 1em
+    border: 1px solid #000
+    h1
+        color: #{headingColor}
+        background-image: url(@{MyBackgroundR})
+
+
+

Julius (Javascript)

+

String interpolation works slightly differently in Julius. This is important +from a security standpoint — all interpolated values are valid JSON by default +which helps prevent XSS attacks. You’ll need to use the rawJS function to get +a string that isn’t wrapped in double-quotes.

+

Be sure not to use rawJS with any input you don’t trust, e.g., anything +submitted by a user.

+
-- import the rawJS function by importing its typeclass
+import Text.Julius (RawJS (..))
+
$(function(){
+    $("section.#{rawJS sectionClass}").hide();
+    $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
+    ^{addBling}
+});
+
+
+
+

Types

+

Before we jump into syntax, let’s take a look at the various types involved. We +mentioned in the introduction that types help protect us from XSS attacks. For +example, let’s say that we have an HTML template that should display someone’s +name. It might look like this:

+
<p>Hello, my name is #{name}
+ +

What should happen to name, and what should its datatype be? A naive approach +would be to use a Text value, and insert it verbatim. But that would give us +quite a problem when name is equal to something like:

+
<script src='http://nefarious.com/evil.js'></script>
+

What we want is to be able to entity-encode the name, so that < becomes &lt;.

+

An equally naive approach is to simply entity-encode every piece of text that +gets embedded. What happens when you have some preexisting HTML generated from +another process? For example, on the Yesod website, all Haskell code snippets +are run through a colorizing function that wraps up words in appropriate span +tags. If we entity escaped everything, code snippets would be completely +unreadable!

+

Instead, we have an Html datatype. In order to generate an Html value, we +have two options for APIs: the ToMarkup typeclass provides a way to convert +String and Text values into Html, via its toHtml function, +automatically escaping entities along the way. This would be the approach we’d +want for the name above. For the code snippet example, we would use the +preEscapedToMarkup function.

+

When you use variable interpolation in Hamlet (the HTML Shakespeare language), +it automatically applies a toHtml call to the value inside. So if you +interpolate a String, it will be entity-escaped. But if you provide an Html +value, it will appear unmodified. In the code snippet example, we might +interpolate with something like #{preEscapedToMarkup myHaskellHtml}.

+ +

Similarly, we have Css/ToCss, as well as Javascript/ToJavascript. These +provide some compile-time sanity checks that we haven’t accidentally stuck some +HTML in our CSS.

+ +
+

Type-safe URLs

+

Possibly the most unique feature in Yesod is type-safe URLs, and the ability to +use them conveniently is provided directly by Shakespeare. Usage is nearly +identical to variable interpolation; we just use the at-sign (@) instead of the +hash (#). We’ll cover the syntax later; first, let’s clarify the intuition.

+

Suppose we have an application with two routes: +http://example.com/profile/home is the homepage, and +http://example.com/display/time displays the current time. And let’s say we +want to link from the homepage to the time. I can think of three different ways +of constructing the URL:

+
    +
  1. +

    +As a relative link: ../display/time +

    +
  2. +
  3. +

    +As an absolute link, without a domain: /display/time +

    +
  4. +
  5. +

    +As an absolute link, with a domain: http://example.com/display/time +

    +
  6. +
+

There are problems with each approach: the first will break if either URL +changes. Also, it’s not suitable for all use cases; RSS and Atom feeds, for +instance, require absolute URLs. The second is more resilient to change than +the first, but still won’t be acceptable for RSS and Atom. And while the third +works fine for all use cases, you’ll need to update every single URL in your +application whenever your domain name changes. You think that doesn’t happen +often? Just wait till you move from your development to staging and finally +production server.

+

But more importantly, there is one huge problem with all approaches: if you +change your routes at all, the compiler won’t warn you about the broken links. +Not to mention that typos can wreak havoc as well.

+

The goal of type-safe URLs is to let the compiler check things for us as much +as possible. In order to facilitate this, our first step must be to move away +from plain old text, which the compiler doesn’t understand, to some well +defined datatypes. For our simple application, let’s model our routes with a +sum type:

+
data MyRoute = Home | Time
+

Instead of placing a link like /display/time in our template, we can use the +Time constructor. But at the end of the day, HTML is made up of text, not +data types, so we need some way to convert these values to text. We call this a +URL rendering function, and a simple one is:

+
renderMyRoute :: MyRoute -> Text
+renderMyRoute Home = "http://example.com/profile/home"
+renderMyRoute Time = "http://example.com/display/time"
+ +

OK, we have our render function, and we have type-safe URLs embedded in the +templates. How does this fit together exactly? Instead of generating an Html +(or Css or Javascript) value directly, Shakespearean templates actually +produce a function, which takes this render function and produces HTML. To see +this better, let’s have a quick (fake) peek at how Hamlet would work under the +surface. Supposing we had a template:

+
<a href=@{Time}>The time
+

this would translate roughly into the Haskell code:

+
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
+
+
+
+

Syntax

+

All Shakespearean languages share the same interpolation syntax, and are able +to utilize type-safe URLs. They differ in the syntax specific for their target +language (HTML, CSS, or Javascript). Let’s explore each language in turn.

+
+

Hamlet Syntax

+

Hamlet is the most sophisticated of the languages. Not only does it provide +syntax for generating HTML, it also allows for basic control structures: +conditionals, looping, and maybes.

+
+

Tags

+

Obviously tags will play an important part of any HTML template language. In +Hamlet, we try to stick very close to existing HTML syntax to make the language +more comfortable. However, instead of using closing tags to denote nesting, we +use indentation. So something like this in HTML:

+
<body>
+<p>Some paragraph.</p>
+<ul>
+<li>Item 1</li>
+<li>Item 2</li>
+</ul>
+</body>
+

would be

+
<body>
+    <p>Some paragraph.
+    <ul>
+        <li>Item 1
+        <li>Item 2
+

In general, we find this to be easier to follow than HTML once you get +accustomed to it. The only tricky part comes with dealing with whitespace +before and after tags. For example, let’s say you want to create the HTML

+
<p>Paragraph <i>italic</i> end.</p>
+

We want to make sure that whitespace is preserved after the word +"Paragraph" and before the word "end". To do so, we use two simple escape +characters:

+
<p>
+    Paragraph #
+    <i>italic
+    \ end.
+

The whitespace escape rules are actually quite simple:

+
    +
  1. +

    +If the first non-space character in a line is a backslash, the backslash is ignored. (Note: this will also cause any tag on this line to be treated as plain text.) +

    +
  2. +
  3. +

    +If the last character in a line is a hash, it is ignored. +

    +
  4. +
+

One other thing. Hamlet does not escape entities within its content. This is +done on purpose to allow existing HTML to be more easily copied in. So the +example above could also be written as:

+
<p>Paragraph <i>italic</i> end.
+

Notice that the first tag will be automatically closed by Hamlet, while the +inner "i" tag will not. You are free to use whichever approach you want, there +is no penalty for either choice. Be aware, however, that the only time you +use closing tags in Hamlet is for such inline tags; normal tags are not closed.

+

Another outcome of this is that any tags after the first tag do not have +special treatment for IDs and classes. For example, the Hamlet snippet:

+
<p #firstid>Paragraph <i #secondid>italic</i> end.
+

generates the HTML:

+
<p id="firstid">Paragraph <i #secondid>italic</i> end.</p>
+

Notice how the p tag is automatically closed, and its attributes get special +treatment, whereas the i tag is treated as plain text.

+
+
+

Interpolation

+

What we have so far is a nice, simplified HTML, but it doesn’t let us interact +with our Haskell code at all. How do we pass in variables? Simple: with +interpolation:

+
<head>
+    <title>#{title}
+

The hash followed by a pair of braces denotes variable interpolation. In the +case above, the title variable from the scope in which the template was +called will be used. Let me state that again: Hamlet automatically has access +to the variables in scope when it’s called. There is no need to specifically +pass variables in.

+

You can apply functions within an interpolation. You can use string and numeric +literals in an interpolation. You can use qualified modules. Both parentheses +and the dollar sign can be used to group statements together. And at the end, +the toHtml function is applied to the result, meaning any instance of +ToMarkup can be interpolated. Take, for instance, the following code.

+
-- Just ignore the quasiquote stuff for now, and that shamlet thing.
+-- It will be explained later.
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (shamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Char (toLower)
+import Data.List (sort)
+
+data Person = Person
+    { name :: String
+    , age  :: Int
+    }
+
+main :: IO ()
+main = putStrLn $ renderHtml [shamlet|
+<p>Hello, my name is #{name person} and I am #{show $ age person}.
+<p>
+    Let's do some funny stuff with my name: #
+    <b>#{sort $ map toLower (name person)}
+<p>Oh, and in 5 years I'll be #{show ((+) 5 (age person))} years old.
+|]
+  where
+    person = Person "Michael" 26
+

What about our much-touted type-safe URLs? They are almost identical to +variable interpolation in every way, except they start with an at-sign (@) +instead. In addition, there is embedding via a caret (^) which allows you to +embed another template of the same type. The next code sample demonstrates both +of these.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text)
+
+data MyRoute = Home
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+
+footer :: HtmlUrl MyRoute
+footer = [hamlet|
+<footer>
+    Return to #
+    <a href=@{Home}>Homepage
+    .
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ [hamlet|
+<body>
+    <p>This is my page.
+    ^{footer}
+|] render
+

Additionally, there is a variant of URL interpolation which allows you to embed +query string parameters. This can be useful, for example, for creating +paginated responses. Instead of using @{…}, you add a question mark +(@?{…}) to indicate the presence of a query string. The value you provide +must be a two-tuple with the first value being a type-safe URL and the second +being a list of query string parameter pairs. See the next code snippet for an +example.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text, append, pack)
+import Control.Arrow (second)
+import Network.HTTP.Types (renderQueryText)
+import Data.Text.Encoding (decodeUtf8)
+import Blaze.ByteString.Builder (toByteString)
+
+data MyRoute = SomePage
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render SomePage params = "/home" `append`
+    decodeUtf8 (toByteString $ renderQueryText True (map (second Just) params))
+
+main :: IO ()
+main = do
+    let currPage = 2 :: Int
+    putStrLn $ renderHtml $ [hamlet|
+<p>
+    You are currently on page #{currPage}.
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage - 1)])}>Previous
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage + 1)])}>Next
+|] render
+

This generates the expected HTML:

+
<p>You are currently on page 2.
+<a href="/home?page=1">Previous</a>
+<a href="/home?page=3">Next</a>
+</p>
+
+
+

Attributes

+

In that last example, we put an href attribute on the "a" tag. Let’s elaborate on the syntax:

+
    +
  • +

    +You can have interpolations within the attribute value. +

    +
  • +
  • +

    +The equals sign and value for an attribute are optional, just like in HTML. + So <input type=checkbox checked> is perfectly valid. +

    +
  • +
  • +

    +There are two convenience attributes: for id, you can use the hash, and for + classes, the period. In other words, <p #paragraphid .class1 .class2>. +

    +
  • +
  • +

    +While quotes around the attribute value are optional, they are required if + you want to embed spaces. +

    +
  • +
  • +

    +You can add an attribute optionally by using colons. To make a checkbox only + checked if the variable isChecked is True, you would write + <input type=checkbox :isChecked:checked>. To have a paragraph be optionally red, + you could use <p :isRed:style="color:red">. (This also works for class names, e.g., + <p :isCurrent:.current> will set the class current if isCurrent is True.) +

    +
  • +
  • +

    +Arbitrary key-value pairs can also be interpolated using the *{…} + syntax. The interpolated variable must be a tuple, or list of + tuples, of Text or String. For example: if we have a variable + attrs = [("foo", "bar")], we could interpolate that into an + element like: <p *{attrs}> to get <p foo="bar">. +

    +
  • +
+
+
+

Conditionals

+

Eventually, you’ll want to put in some logic in your page. The goal of Hamlet +is to make the logic as minimalistic as possible, pushing the heavy lifting +into Haskell. As such, our logical statements are very basic… so basic, that +it’s if, elseif, and else.

+
$if isAdmin
+    <p>Welcome to the admin section.
+$elseif isLoggedIn
+    <p>You are not the administrator.
+$else
+    <p>I don't know who you are. Please log in so I can decide if you get access.
+

All the same rules of normal interpolation apply to the content of the conditionals.

+
+
+

Maybe

+

Similarly, we have a special construct for dealing with Maybe values. This +could technically be dealt with using if, isJust and fromJust, but this +is more convenient and avoids partial functions.

+
$maybe name <- maybeName
+    <p>Your name is #{name}
+$nothing
+    <p>I don't know your name.
+

In addition to simple identifiers, you can use a few other, more complicated +values on the left hand side, such as constructors and tuples.

+
$maybe Person firstName lastName <- maybePerson
+    <p>Your name is #{firstName} #{lastName}
+

The right-hand-side follows the same rules as interpolations, allow variables, +function application, and so on.

+
+
+

Forall

+

And what about looping over lists? We have you covered there too:

+
$if null people
+    <p>No people.
+$else
+    <ul>
+        $forall person <- people
+            <li>#{person}
+
+
+

Case

+

Pattern matching is one of the great strengths of Haskell. Sum types let you +cleanly model many real-world types, and case statements let you safely +match, letting the compiler warn you if you missed a case. Hamlet gives you the +same power.

+
$case foo
+    $of Left bar
+        <p>It was left: #{bar}
+    $of Right baz
+        <p>It was right: #{baz}
+
+
+

With

+

Rounding out our statements, we have with. It’s basically just a convenience +for declaring a synonym for a long expression.

+
$with foo <- some very (long ugly) expression that $ should only $ happen once
+    <p>But I'm going to use #{foo} multiple times. #{foo}
+
+
+

Doctype

+

Last bit of syntactic sugar: the doctype statement. We have support for a +number of different versions of a doctype, though we recommend $doctype 5 +for modern web applications, which generates <!DOCTYPE html>.

+
$doctype 5
+<html>
+    <head>
+        <title>Hamlet is Awesome
+    <body>
+        <p>All done.
+ +
+
+
+

Lucius Syntax

+

Lucius is one of two CSS templating languages in the Shakespeare family. It is +intended to be a superset of CSS, leveraging the existing syntax while adding +in a few more features.

+
    +
  • +

    +Like Hamlet, we allow both variable and URL interpolation. +

    +
  • +
  • +

    +CSS blocks are allowed to nest. +

    +
  • +
  • +

    +You can declare variables in your templates. +

    +
  • +
  • +

    +A set of CSS properties can be created as a mixin, and reused in multiple + declarations. +

    +
  • +
+

Starting with the second point: let’s say you want to have some special styling +for some tags within your article. In plain ol' CSS, you’d have to write:

+
article code { background-color: grey; }
+article p { text-indent: 2em; }
+article a { text-decoration: none; }
+

In this case, there aren’t that many clauses, but having to type out article +each time is still a bit of a nuisance. Imagine if you had a dozen or so of +these. Not the worst thing in the world, but a bit of an annoyance. Lucius +helps you out here:

+
article {
+    code { background-color: grey; }
+    p { text-indent: 2em; }
+    a { text-decoration: none; }
+    > h1 { color: green; }
+}
+

Having Lucius variables allows you to avoid repeating yourself. A simple +example would be to define a commonly used color:

+
@textcolor: #ccc; /* just because we hate our users */
+body { color: #{textcolor} }
+a:link, a:visited { color: #{textcolor} }
+

Mixins are a relatively new addition to Lucius. The idea is to declare a mixin +providing a collection of properties, and then embed that mixin in a template +using caret interpolation (^). The following example demonstrates how we +could use a mixin to deal with vendor prefixes.

+
{-# LANGUAGE QuasiQuotes #-}
+import Text.Lucius
+import qualified Data.Text.Lazy.IO as TLIO
+
+-- Dummy render function.
+render = undefined
+
+-- Our mixin, which provides a number of vendor prefixes for transitions.
+transition val =
+    [luciusMixin|
+        -webkit-transition: #{val};
+        -moz-transition: #{val};
+        -ms-transition: #{val};
+        -o-transition: #{val};
+        transition: #{val};
+    |]
+
+-- Our actual Lucius template, which uses the mixin.
+myCSS =
+    [lucius|
+        .some-class {
+            ^{transition "all 4s ease"}
+        }
+    |]
+
+main = TLIO.putStrLn $ renderCss $ myCSS render
+
+
+

Cassius Syntax

+

Cassius is a whitespace-sensitive alternative to Lucius. As mentioned in the +synopsis, it uses the same processing engine as Lucius, but preprocesses all +input to insert braces to enclose subblocks and semicolons to terminate lines. +This means you can leverage all features of Lucius when writing Cassius. As a +simple example:

+
#banner
+    border: 1px solid #{bannerColor}
+    background-image: url(@{BannerImageR})
+
+
+

Julius Syntax

+

Julius is the simplest of the languages discussed here. In fact, some might +even say it’s really just Javascript. Julius allows the three forms of +interpolation we’ve mentioned so far, and otherwise applies no transformations +to your content.

+ +
+
+
+

Calling Shakespeare

+

The question of course arises at some point: how do I actually use this stuff? +There are three different ways to call out to Shakespeare from your Haskell +code:

+
+
+Quasiquotes +
+

+Quasiquotes allow you to embed arbitrary content within your Haskell, and for it to be converted into Haskell code at compile time. +

+
+
+External file +
+

+In this case, the template code is in a separate file which is referenced via Template Haskell. +

+
+
+Reload mode +
+

+Both of the above modes require a full recompile to see any changes. In reload mode, your template is kept in a separate file and referenced via Template Haskell. But at runtime, the external file is reparsed from scratch each time. +

+
+
+ +

One of the first two approaches should be used in production. They both embed +the entirety of the template in the final executable, simplifying deployment +and increasing performance. The advantage of the quasiquoter is the simplicity: +everything stays in a single file. For short templates, this can be a very good +fit. However, in general, the external file approach is recommended because:

+
    +
  • +

    +It follows nicely in the tradition of separating logic from presentation. +

    +
  • +
  • +

    +You can easily switch between external file and debug mode with some simple + CPP macros, meaning you can keep rapid development and still achieve high + performance in production. +

    +
  • +
+

Since these are special QuasiQuoters and Template Haskell functions, you need +to be sure to enable the appropriate language extensions and use correct +syntax. You can see a simple example of each in the following code snippets.

+

Quasiquoter

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Data.Text (Text)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: Text -> HtmlUrl MyRoute
+template title = [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{title}
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1>#{title}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ template "My Title" render
+

+

External file

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-} -- to control production versus debug
+import Text.Lucius (CssUrl, luciusFile, luciusFileReload, renderCss)
+import Data.Text (Text)
+import qualified Data.Text.Lazy.IO as TLIO
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: CssUrl MyRoute
+#if PRODUCTION
+template = $(luciusFile "template.lucius")
+#else
+template = $(luciusFileReload "template.lucius")
+#endif
+
+main :: IO ()
+main = TLIO.putStrLn $ renderCss $ template render
+

+
/* @template.lucius */
+foo { bar: baz }
+

The naming scheme for the functions is very consistent.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageQuasiquoterExternal fileReload

Hamlet

hamlet

hamletFile

N/A

Cassius

cassius

cassiusFile

cassiusFileReload

Lucius

lucius

luciusFile

luciusFileReload

Julius

julius

juliusFile

juliusFileReload

+
+

Alternate Hamlet Types

+

So far, we’ve seen how to generate an HtmlUrl value from Hamlet, which is a +piece of HTML with embedded type-safe URLs. There are currently three other +values we can generate using Hamlet: plain HTML, HTML with URLs and +internationalized messages, and widgets. That last one will be covered in more +detail in the widgets chapter.

+

To generate plain HTML without any embedded URLs, we use "simplified Hamlet". +There are a few changes:

+
    +
  • +

    +We use a different set of functions, prefixed with an "s". So the quasiquoter + is shamlet and the external file function is shamletFile. How we + pronounce those is still up for debate. +

    +
  • +
  • +

    +No URL interpolation is allowed. Doing so will result in a compile-time + error. +

    +
  • +
  • +

    +Embedding (the caret-interpolator) no longer allows arbitrary HtmlUrl + values. The rule is that the embedded value must have the same type as the + template itself, so in this case it must be Html. That means that for + shamlet, embedding can be completely replaced with normal variable + interpolation (with a hash). +

    +
  • +
+

Dealing with internationalization (i18n) in Hamlet is a bit complicated. Hamlet +supports i18n via a message datatype, very similar in concept and +implementation to a type-safe URL. As a motivating example, let’s say we want +to have an application that tells you hello and how many apples you bought. +We could represent those messages with a datatype.

+
data Msg = Hello | Apples Int
+

Next, we would want to be able to convert that into something human-readable, +so we define some render functions:

+
renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+

Now we want to interpolate those Msg values directly in the template. For that, we use underscore interpolation.

+
$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+

This kind of a template now needs some way to turn those values into HTML. So +just like type-safe URLs, we pass in a render function. To represent this, we +define a new type synonym:

+
type Render url = url -> [(Text, Text)] -> Text
+type Translate msg = msg -> Html
+type HtmlUrlI18n msg url = Translate msg -> Render url -> Html
+

At this point, you can pass renderEnglish, renderSpanish, or +renderKlingon to this template, and it will generate nicely translated output +(depending, of course, on the quality of your translators). The complete +program is:

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Data.Text (Text)
+import qualified Data.Text as T
+import Text.Hamlet (HtmlUrlI18n, ihamlet)
+import Text.Blaze.Html (toHtml)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+renderUrl :: MyRoute -> [(Text, Text)] -> Text
+renderUrl Home _ = "/home"
+renderUrl Time _ = "/time"
+renderUrl Stylesheet _ = "/style.css"
+
+data Msg = Hello | Apples Int
+
+renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+
+template :: Int -> HtmlUrlI18n Msg MyRoute
+template count = [ihamlet|
+$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml
+     $ (template 5) (toHtml . renderEnglish) renderUrl
+
+
+
+

Other Shakespeare

+

In addition to HTML, CSS and Javascript helpers, there is also some more +general-purpose Shakespeare available. shakespeare-text provides a simple way +to create interpolated strings, much like people are accustomed to in scripting +languages like Ruby and Python. This package’s utility is definitely not +limited to Yesod.

+
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
+import Text.Shakespeare.Text
+import qualified Data.Text.Lazy.IO as TLIO
+import Data.Text (Text)
+import Control.Monad (forM_)
+
+data Item = Item
+    { itemName :: Text
+    , itemQty :: Int
+    }
+
+items :: [Item]
+items =
+    [ Item "apples" 5
+    , Item "bananas" 10
+    ]
+
+main :: IO ()
+main = forM_ items $ \item -> TLIO.putStrLn
+    [lt|You have #{show $ itemQty item} #{itemName item}.|]
+

Some quick points about this simple example:

+
    +
  • +

    +Notice that we have three different textual datatypes involved (String, + strict Text and lazy Text). They all play together well. +

    +
  • +
  • +

    +We use a quasiquoter named lt, which generates lazy text. There is also + st. +

    +
  • +
  • +

    +Also, there are longer names for these quasiquoters (ltext and stext). +

    +
  • +
  • +

    +The syntax for variable interpolation for Text.Shakespeare.Text is the same + as described above. Note that ^{..} and @{..} are also recognized in + lt and st. If the output of a template should contain ^{..}, a + backslash can be used to prevent the interpolation, + e.g. [lt|2^\{\23}|]. The backslash is removed from the resulting text. +

    +
  • +
+
+
+

General Recommendations

+

Here are some general hints from the Yesod community on how to get the most out +of Shakespeare.

+
    +
  • +

    +For actual sites, use external files. For libraries, it’s OK to use + quasiquoters, assuming they aren’t too long. +

    +
  • +
  • +

    +Patrick Brisbin has put together a + Vim code + highlighter that can help out immensely. +

    +
  • +
  • +

    +You should almost always start Hamlet tags on their own line instead of + embedding start/end tags after an existing tag. The only exception to this is + the occasional <i> or <b> tag inside a large block of text. +

    +
  • +
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/single-process-pubsub.html b/public/book-1.6/single-process-pubsub.html new file mode 100644 index 00000000..ab0d8056 --- /dev/null +++ b/public/book-1.6/single-process-pubsub.html @@ -0,0 +1,326 @@ + Single process pub-sub :: Yesod Web Framework Book- Version 1.6 +
+

Single process pub-sub

+ + +

The previous example was admittedly quite simple. Let’s build on that +foundation (pun intended) to do something a bit more interesting. Suppose we +have a workflow on our site like the following:

+
    +
  1. +

    +Enter some information on page X, and submit. +

    +
  2. +
  3. +

    +Submission starts a background job, and the user is redirected to a page to view status of that job. +

    +
  4. +
  5. +

    +That second page will subscribe to updates from the background job and display them to the user. +

    +
  6. +
+

The core principle here is the ability to let one thread publish updates, and +have another thread subscribe to receive those updates. This is known generally +as pub/sub, and fortunately is very easy to achieve in Haskell via STM.

+

Like the previous chapter, let me start off with the caveat: this technique +only works properly if you have a single web application process. If you have +two different servers and a load balancer, you’d either need sticky sessions or +some other solution to make sure that the requests from a single user are going +to the same machine. In those situations, you may want to consider using an +external pubsub solution, such as Redis.

+

With that caveat out of the way, let’s get started.

+
+

Foundation datatype

+

We’ll need two different mutable references in our foundation. The first will +keep track of the next "job id" we’ll hand out. Each of these background jobs +will be represented by a unique identifier, which will be used in our URLs. +The second piece of data will be a map from the job ID to the broadcast channel +used for publishing updates. In code:

+
data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+

Notice that our TChan contains Maybe Text values. The reason for the +Maybe wrapper is so that we can indicate that the channel is complete, by +providing a Nothing value.

+
+
+

Allocating a job

+

In order to allocate a job, we need to:

+
    +
  1. +

    +Get a job ID. +

    +
  2. +
  3. +

    +Create a new broadcast channel. +

    +
  4. +
  5. +

    +Add the channel to the channel map. +

    +
  6. +
+

Due to the beauty of STM, this is pretty easy.

+
(jobId, chan) <- liftIO $ atomically $ do
+    jobId <- readTVar nextJob
+    writeTVar nextJob $! jobId + 1
+    chan <- newBroadcastTChan
+    m <- readTVar jobs
+    writeTVar jobs $ IntMap.insert jobId chan m
+    return (jobId, chan)
+
+
+

Fork our background job

+

There are many different ways we could go about this, and they depend entirely +on what the background job is going to be. Here’s a minimal example of a +background job that prints out a few messages, with a 1 second delay between +each message. Note how after our final message, we broadcast a Nothing value +and remove our channel from the map of channels.

+
liftIO $ forkIO $ do
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something\n"
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something else\n"
+    threadDelay 1000000
+    atomically $ do
+        writeTChan chan $ Just "All done\n"
+        writeTChan chan Nothing
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.delete jobId m
+
+
+

View progress

+

For this demonstration, I’ve elected for a very simple progress viewing: a +plain text page with stream response. There are a few other possibilities here: +an HTML page that auto-refreshes every X seconds or using eventsource or +websockets. I encourage you to give those a shot also, but here’s the simplest +implementation I can think of:

+
getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+

We start off by looking up the channel in the map. If we can’t find it, it +means the job either never existed, or has already been completed. In either +event, we return a 404. (Another possible enhancement would be to store some +information on all previously completed jobs and let the user know if they’re +done.)

+

Assuming the channel exists, we use respondSource to start a streaming +response. We then repeatedly call readTChan until we get a Nothing value, +at which point we exit (via return ()). Notice that on each iteration, we +call both sendChunkText and sendFlush. Without that second call, the user +won’t receive any updates until the output buffer completely fills up, which is +not what we want for a real-time update system.

+
+
+

Complete application

+

For completeness, here’s the full source code for this application:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Control.Concurrent     (forkIO, threadDelay)
+import           Control.Concurrent.STM
+import           Data.IntMap            (IntMap)
+import qualified Data.IntMap            as IntMap
+import           Data.Text              (Text)
+import           Yesod
+
+data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+/view-progress/#Int ViewProgressR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "PubSub example"
+    [whamlet|
+        <form method=post>
+            <button>Start new background job
+    |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    App {..} <- getYesod
+    (jobId, chan) <- liftIO $ atomically $ do
+        jobId <- readTVar nextJob
+        writeTVar nextJob $! jobId + 1
+        chan <- newBroadcastTChan
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.insert jobId chan m
+        return (jobId, chan)
+    liftIO $ forkIO $ do
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something\n"
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something else\n"
+        threadDelay 1000000
+        atomically $ do
+            writeTChan chan $ Just "All done\n"
+            writeTChan chan Nothing
+            m <- readTVar jobs
+            writeTVar jobs $ IntMap.delete jobId m
+    redirect $ ViewProgressR jobId
+
+getViewProgressR :: Int -> Handler TypedContent
+getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+
+main :: IO ()
+main = do
+    jobs <- newTVarIO IntMap.empty
+    nextJob <- newTVarIO 1
+    warp 3000 App {..}
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/sql-joins.html b/public/book-1.6/sql-joins.html new file mode 100644 index 00000000..95e7b972 --- /dev/null +++ b/public/book-1.6/sql-joins.html @@ -0,0 +1,560 @@ + SQL Joins :: Yesod Web Framework Book- Version 1.6 +
+

SQL Joins

+ + +

Persistent touts itself as a database-agnostic interface. How, then, are you +supposed to do things which are inherently backend-specific? This most often +comes up in Yesod when you want to join two tables together. There are some +pure-Haskell solutions that are completely backend-agonistic, but there are +also more efficient methods at our disposal. In this chapter, we’ll introduce a +common problem you might want to solve, and then build up more sophisticated +solutions.

+
+

Multi-author blog

+

Since blogs are a well understood problem domain, we’ll use that for our +problem setup. Consider a blog engine that allows you to have multiple authors +in the database, and each blog post will have a single author. In Persistent, +we may model this as:

+
Author
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+

Let’s set up our initial Yesod application to show a blog post index indicating +the blog title and the author:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import           Control.Monad.Logger
+import           Data.Text               (Text)
+import           Database.Persist.Sqlite
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Author
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+|]
+
+data App = App
+    { persistConfig :: SqliteConf
+    , connPool      :: ConnectionPool
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/blog/#BlogId BlogR GET
+|]
+
+instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB = defaultRunDB persistConfig connPool
+instance YesodPersistRunner App where
+    getDBRunner = defaultGetDBRunner connPool
+
+getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ selectList [] []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall Entity blogid blog <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>
+                            #{blogTitle blog} by #{show $ blogAuthor blog}
+        |]
+
+getBlogR :: BlogId -> Handler Html
+getBlogR _ = error "Implementation left as exercise to reader"
+
+main :: IO ()
+main = do
+    -- Use an in-memory database with 1 connection. Terrible for production,
+    -- but useful for testing.
+    let conf = SqliteConf ":memory:" 1
+    pool <- createPoolConfig conf
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        -- Fill in some testing data
+        alice <- insert $ Author "Alice"
+        bob   <- insert $ Author "Bob"
+
+        insert_ $ Blog alice "Alice's first post" "Hello World!"
+        insert_ $ Blog bob "Bob's first post" "Hello World!!!"
+        insert_ $ Blog alice "Alice's second post" "Goodbye World!"
+
+    warp 3000 App
+        { persistConfig = conf
+        , connPool      = pool
+        }
+

That’s all well and good, but let’s look at the output:

+

Authors appear as numeric identifiers

+ + + + + + +
+

All we’re doing is displaying the numeric identifier of each author, instead of +the author’s name. In order to fix this, we need to pull extra information from +the Author table as well. Let’s dive in to getting that done.

+
+
+

Database queries in Widgets

+

I’ll address this one right off the bat, since it catches many users by +surprise. You might think that you can solve this problem in the Hamlet +template itself, e.g.:

+
<ul>
+    $forall Entity blogid blog <- blogs
+        $with author <- runDB $ get404 $ blogAuthor
+            <li>
+                <a href=@{BlogR blogid}>
+                    #{blogTitle blog} by #{authorName author}
+

However, this isn’t allowed, because Hamlet will not allow you to run +database actions inside of it. One of the goals of Shakespearean templates is +to help you keep your pure and impure code separated, with the idea being that +all impure code needs to stay in Haskell.

+

But we can actually tweak the above code to work in Yesod. The idea is to +separate out the code for each blog entry into a Widget function, and then +perform the database action in the Haskell portion of the function:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ selectList [] []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall blogEntity <- blogs
+                    ^{showBlogLink blogEntity}
+        |]
+
+showBlogLink :: Entity Blog -> Widget
+showBlogLink (Entity blogid blog) = do
+    author <- handlerToWidget $ runDB $ get404 $ blogAuthor blog
+    [whamlet|
+        <li>
+            <a href=@{BlogR blogid}>
+                #{blogTitle blog} by #{authorName author}
+    |]
+

We need to use handlerToWidget to turn our Handler action into a Widget +action, but otherwise the code is straightforward. And furthermore, we now get +exactly the output we wanted:

+

Authors appear as names

+ + + + + + +
+
+
+

Joins

+

If we have the exact result we’re looking for, why isn’t this chapter over? The +problem is that this technique is highly inefficient. We’re performing one +database query to load up all of the blog posts, then a separate query for each +blog post to get the author names. This is far less efficient than simply using +a SQL join. The question is: how do we do a join in Persistent? We’ll start off +by writing some raw SQL:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ rawSql
+        "SELECT ??, ?? \
+        \FROM blog INNER JOIN author \
+        \ON blog.author=author.id"
+        []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall (Entity blogid blog, Entity _ author) <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>
+                            #{blogTitle blog} by #{authorName author}
+        |]
+

We pass the rawSql function two parameters: a SQL query, and a list of +additional parameters to replace placeholders in the query. That list is empty, +since we’re not using any placeholders. However, note that we’re using ?? in +our SELECT statement. This is a form of type inspection: rawSql will detect +the type of entities being demanded, and automatically fill in the fields that +are necessary to make the query.

+

rawSql is certainly powerful, but it’s also unsafe. There’s no syntax +checking on your SQL query string, so you can get runtime errors. Also, it’s +easy to end up querying for the wrong type and end up with very confusing +runtime error messages.

+
+
+

Esqueleto

+ +

Persistent has a companion library- Esqueleto- which provides an expressive, +type safe DSL for writing SQL queries. It takes advantage of the Persistent +types to ensure it generates valid SQL queries and produces the results +requested by the program. In order to use Esqueleto, we’re going to add some +imports:

+
import qualified Database.Esqueleto      as E
+import           Database.Esqueleto      ((^.))
+

And then write our query using Esqueleto:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB
+           $ E.select
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall (E.Value blogid, E.Value title, E.Value name) <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>#{title} by #{name}
+        |]
+

Notice how similar the query looks to the SQL we wrote previously. One thing of +particular interest is the ^. operator, which is a projection. blog ^. +BlogAuthor, for example, means "take the author column of the blog table." +And thanks to the type safety of Esqueleto, you could never accidentally +project AuthorName from blog: the type system will stop you!

+

In addition to safety, there’s also a performance advantage to Esqueleto. +Notice the returned tuple; it explicitly lists the three columns that we +need to generate our listing. This can provide a huge performance boost: unlike +all other examples we’ve had, this one does not require transferring the +(potentially quite large) content column of the blog post to generate the +listing.

+ +

Esqueleto is really the gold standard in writing SQL queries in Persistent. The +rule of thumb should be: if you’re doing something that fits naturally into +Persistent’s query syntax, use Persistent, as it’s database agnostic and a bit +easier to use. But if you’re doing something that would be more efficient with +a SQL-specific feature, you should strongly consider Esqueleto.

+
+
+

Streaming

+

There’s still a problem with our Esqueleto approach. If there are thousands of +blog posts, then the workflow will be:

+
    +
  1. +

    +Read thousands of blog posts into memory on the server. +

    +
  2. +
  3. +

    +Render out the entire HTML page. +

    +
  4. +
  5. +

    +Send the HTML page to the client. +

    +
  6. +
+

This has two downsides: it uses a lot of memory, and it gives high latency for the user. If this is a bad approach, why does Yesod gear you towards it out of the box, instead of tending towards a streaming approach? Two reasons:

+
    +
  • +

    +Correctness: imagine if there was an error reading the 243rd record from the database. By doing a non-streaming response, Yesod can catch the exception and send a meaningful 500 error response. If we were already streaming, the streaming body would simply stop in the middle of a misleading 200 OK respond. +

    +
  • +
  • +

    +Ease of use: it’s usually easier to work with non-streaming bodies. +

    +
  • +
+

The standard recommendation I’d give someone who wants to generate listings +that may be large is to use pagination. This allows you to do less work on the +server, write simple code, get the correctness guarantees Yesod provides out of +the box, and reduce user latency. However, there are times when you’ll really +want to do a streaming response, so let’s cover that here.

+

Switching Esqueleto to a streaming response is easy: replace select with +selectSource. The Esqueleto query itself remains unchanged. Then we’ll use +the respondSourceDB function to generate a streaming database response, and +manually construct our HTML to wrap up the listing.

+
getHomeR :: Handler TypedContent
+getHomeR = do
+    let blogsSrc =
+             E.selectSource
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    render <- getUrlRenderParams
+    respondSourceDB typeHtml $ do
+        sendChunkText "<html><head><title>Blog posts</title></head><body><ul>"
+        blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) ->
+            toFlushBuilder $
+            [hamlet|
+                <li>
+                    <a href=@{BlogR blogid}>#{title} by #{name}
+            |] render
+            )
+        sendChunkText "</ul></body></html>"
+

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 +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 +Flush Builder value, as needed by Yesod’s streaming framework.

+

Unfortunately, we’re no longer able to take advantage of Hamlet to do our +overall page layout, since we need to explicit generate start and end tags +separately. This introduces another point for possible bugs, if we accidentally +create unbalanced tags. We also lose the ability to use defaultLayout, for +exactly the same reason.

+

Streaming HTML responses are a powerful tool, and are sometimes necessary. But +generally speaking, I’d recommend sticking to safer options.

+
+
+

Conclusion

+

This chapter covered a number of ways of doing a SQL join:

+
    +
  • +

    +Avoid the join entirely, and manually grab the associated data in Haskell. This is also known as an application level join. +

    +
  • +
  • +

    +Write the SQL explicitly with rawSql. While somewhat convenient, this loses a lot of Persistent’s type safety. +

    +
  • +
  • +

    +Use Esqueleto’s DSL functionality to create a type-safe SQL query. +

    +
  • +
  • +

    +And if you need it, you can even generate a streaming response from Esqueleto. +

    +
  • +
+

For completeness, here’s the entire body of the final, streaming example:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import           Control.Monad.Logger
+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
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+|]
+
+data App = App
+    { persistConfig :: SqliteConf
+    , connPool      :: ConnectionPool
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/blog/#BlogId BlogR GET
+|]
+
+instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB = defaultRunDB persistConfig connPool
+instance YesodPersistRunner App where
+    getDBRunner = defaultGetDBRunner connPool
+
+getHomeR :: Handler TypedContent
+getHomeR = do
+    let blogsSrc =
+             E.selectSource
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    render <- getUrlRenderParams
+    respondSourceDB typeHtml $ do
+        sendChunkText "<html><head><title>Blog posts</title></head><body><ul>"
+        blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) ->
+            toFlushBuilder $
+            [hamlet|
+                <li>
+                    <a href=@{BlogR blogid}>#{title} by #{name}
+            |] render
+            )
+        sendChunkText "</ul></body></html>"
+
+getBlogR :: BlogId -> Handler Html
+getBlogR _ = error "Implementation left as exercise to reader"
+
+main :: IO ()
+main = do
+    -- Use an in-memory database with 1 connection. Terrible for production,
+    -- but useful for testing.
+    let conf = SqliteConf ":memory:" 1
+    pool <- createPoolConfig conf
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        -- Fill in some testing data
+        alice <- insert $ Author "Alice"
+        bob   <- insert $ Author "Bob"
+
+        insert_ $ Blog alice "Alice's first post" "Hello World!"
+        insert_ $ Blog bob "Bob's first post" "Hello World!!!"
+        insert_ $ Blog alice "Alice's second post" "Goodbye World!"
+
+    warp 3000 App
+        { persistConfig = conf
+        , connPool      = pool
+        }
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/understanding-request.html b/public/book-1.6/understanding-request.html new file mode 100644 index 00000000..6b961e96 --- /dev/null +++ b/public/book-1.6/understanding-request.html @@ -0,0 +1,666 @@ + Understanding a Request :: Yesod Web Framework Book- Version 1.6 +
+

Understanding a Request

+ + +

You can often times get away with using Yesod for quite a while without needing +to understand its internal workings. However, such an understanding is often +times advantageous. This chapter will walk you through the request handling +process for a fairly typical Yesod application. Note that a fair amount of this +discussion involves code changes in Yesod 1.2. Most of the concepts are the +same in previous versions, though the data types involved were a bit messier.

+

Yesod’s usage of Template Haskell to bypass boilerplate code can make it +a bit difficult to understand this process sometimes. If beyond the information +in this chapter you wish to further analyze things, it can be useful to view +GHC’s generated code using -ddump-splices.

+ +
+

Handlers

+

When trying to understand Yesod request handling, we need to look at two +components: how a request is dispatched to the appropriate handler code, and +how handler functions are processed. We’ll start off with the latter, and +then circle back to understanding the dispatch process itself.

+
+

Layers

+

Yesod builds itself on top of WAI, which provides a protocol for web servers +(or, more generally, handlers) and applications to communicate with each +other. This is expressed through two datatypes: Request and Response. Then, an +Application is defined as type:

+
type Application = Request
+                -> (Response -> IO ResponseReceived)
+                -> IO ResponseReceived
+

A WAI handler will take an application and run it.

+ +

Request and Response are both very low-level, trying to represent the HTTP +protocol without too much embellishment. This keeps WAI as a generic tool, but +also leaves out a lot of the information we need in order to implement a web +framework. For example, WAI will provide us with the raw data for all request +headers. But Yesod needs to parse that to get cookie information, and then +parse the cookies in order to extract session information.

+

To deal with this dichotomy, Yesod introduces two new data types: +YesodRequest and YesodResponse. YesodRequest contains a WAI Request, and +also adds in such request information as cookies and session variables, and on +the response side can either be a standard WAI Response, or be a higher-level +representation of such a response including such things as updated session +information and extra response headers. To parallel WAI’s Application, we +have:

+
type YesodApp = YesodRequest -> ResourceT IO YesodResponse
+ +

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 +handler functions, you need to have access to three different things:

+
    +
  • +

    +The YesodRequest value for the current request. +

    +
  • +
  • +

    +Some basic environment information, like how to log messages or handle error conditions. This is provided by the datatype RunHandlerEnv. +

    +
  • +
  • +

    +A mutable variable to keep track of updateable information, such as the headers to be returned and the user session state. This is called GHState. (I know that’s not a great name, but it’s there for historical reasons.) +

    +
  • +
+

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 +a step further and converts all the way to a WAI Application.

+
+
+

Content

+

Our example above, and many others you’ve already seen, give a handler +with a type of Handler Html. We’ve just described what the Handler means, +but how does Yesod know how to deal with Html? The answer lies in the +ToTypedContent typeclass. The relevants bit of code are:

+
data Content = ContentBuilder !BBuilder.Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush BBuilder.Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+data TypedContent = TypedContent !ContentType !Content
+
+class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

The Content datatype represents the different ways you can provide a response +body. The first three mirror WAI’s representation directly. The fourth +(ContentDontEvaluate) is used to indicate to Yesod whether response bodies +should be fully evaluated before being returned to users. The advantage to +fully evaluating is that we can provide meaningful error messages if an +exception is thrown from pure code. The downside is possibly increased time and +memory usage.

+

In any event, Yesod knows how to turn a Content into a response body. The +ToContent typeclass provides a way to allow many different datatypes to be +converted into response bodies. Many commonly used types are already instances +of ToContent, including strict and lazy ByteString and Text, and of course +Html.

+

TypedContent adds an extra piece of information: the content type of the value. +As you might expect, there are ToTypedContent instances for a number of common +datatypes, including Html, aeson’s Value (for JSON), and Text (treated as plain text).

+
instance ToTypedContent J.Value where
+    toTypedContent v = TypedContent typeJson (toContent v)
+instance ToTypedContent Html where
+    toTypedContent h = TypedContent typeHtml (toContent h)
+instance ToTypedContent T.Text where
+    toTypedContent t = TypedContent typePlain (toContent t)
+

Putting this all together: a Handler is able to return any value which is an +instance of ToTypedContent, and Yesod will handle turning it into an +appropriate representation and setting the Content-Type response header.

+
+
+

Short-circuit responses

+

One other oddity is how short-circuiting works. For example, you can call +redirect in the middle of a handler function, and the rest of the function will +not be called. The mechanism we use is standard Haskell exceptions. Calling +redirect just throws an exception of type HandlerContents. The runHandler +function will catch any exceptions thrown and produce an appropriate response. +For HandlerContents, each constructor gives a clear action to perform, be it +redirecting or sending a file. For all other exception types, an error message +is displayed to the user.

+
+
+
+

Dispatch

+

Dispatch is the act of taking an incoming request and generating an appropriate +response. We have a few different constraints regarding how we want to handle +dispatch:

+
    +
  • +

    +Dispatch based on path segments (or pieces). +

    +
  • +
  • +

    +Optionally dispatch on request method. +

    +
  • +
  • +

    +Support subsites: packaged collections of functionality providing multiple routes under a specific URL prefix. +

    +
  • +
  • +

    +Support using WAI applications as subsites, while introducing as little + runtime overhead to the process as possible. In particular, we want to avoid + performing any unnecessary parsing to generate a YesodRequest if it + won’t be used. +

    +
  • +
+

The lowest common denominator for this would be to simply use a WAI +Application. However, this doesn’t provide quite enough information: we +need access to the foundation datatype, the logger, and for subsites how a +subsite route is converted to a parent site route. To address this, we have two +helper data types- YesodRunnerEnv and YesodSubRunnerEnv- providing this extra +information for normal sites and subsites.

+

With those types, dispatch now becomes a relatively simple matter: give me an +environment and a request, and I’ll give you a response. This is +represented by the typeclasses YesodDispatch and YesodSubDispatch:

+
class Yesod site => YesodDispatch site where
+    yesodDispatch :: YesodRunnerEnv site -> W.Application
+
+class YesodSubDispatch sub m where
+    yesodSubDispatch :: YesodSubRunnerEnv sub (HandlerSite m) m
+                     -> W.Application
+

We’ll see a bit later how YesodSubDispatch is used. Let’s first +understand how YesodDispatch comes into play.

+
+

toWaiApp, toWaiAppPlain, and warp

+

Let’s assume for the moment that you have a datatype which is an instance +of YesodDispatch. You’ll want to now actually run this thing somehow. To +do this, we need to convert it into a WAI Application and pass it to some kind +of a WAI handler/server. To start this journey, we use toWaiAppPlain. It +performs any appwide initialization necessary. At the time of writing, this +means allocating a logger and setting up the session backend, but more +functionality may be added in the future. Using this data, we can now create a +YesodRunnerEnv. And when that value is passed to yesodDispatch, we get a WAI +Application.

+

We’re almost done. The final remaining modification is path segment +cleanup. The Yesod typeclass includes a member function named cleanPath which +can be used to create canonical URLs. For example, the default implementation +would remove double slashes and redirect a user from /foo//bar to /foo/bar. +toWaiAppPlain adds in some pre-processing to the normal WAI request by +analyzing the requested path and performing cleanup/redirect as necessary.

+

At this point, we have a fully functional WAI Application. There are two other +helper functions included. toWaiApp wraps toWaiAppPlain and additionally +includes some commonly used WAI middlewares, including request logging and GZIP +compression. (Please see the Haddocks for an up-to-date list.) We finally have +the warp function which, as you might guess, runs your application with Warp.

+ +
+
+

Generated code

+

The last remaining black box is the Template Haskell generated code. This +generated code is responsible for handling some of the tedious, error-prone +pieces of your site. If you want to, you can write these all by hand instead. +We’ll demonstrate what that translation would look like, and in the +process elucidate how YesodDispatch and YesodSubDispatch work. Let’s +start with a fairly typical Yesod application.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+
+instance Yesod App
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req sendResponse = sendResponse $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+

For completeness we’ve provided a full listing, but let’s focus on +just the Template Haskell portion:

+
mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+

While this generates a few pieces of code, we only need to replicate three +components to make our site work. Let’s start with the simplest: the +Handler type synonym:

+
type Handler = HandlerT App IO
+

Next is the type-safe URL and its rendering function. The rendering function is +allowed to generate both path segments and query string parameters. Standard +Yesod sites never generate query-string parameters, but it is technically +possible. And in the case of subsites, this often does happen. Notice how we +handle the qs parameter for the MySubsiteR case:

+
instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+

You can see that there’s a fairly simple mapping from the higher-level +route syntax and the RenderRoute instance. Each route becomes a constructor, +each URL parameter becomes an argument to its constructor, we embed a route for +the subsite, and use toPathPiece to render parameters to text.

+

The final component is the YesodDispatch instance. Let’s look at this in +a few pieces.

+
instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+

As described above, yesodDispatch is handed both an environment and a WAI +Request value. We can now perform dispatch based on the requested path, or in +WAI terms, the pathInfo. Referring back to our original high-level route +syntax, we can see that our first route is going to be the single piece +only-get, which we pattern match for.

+

Once that match has succeeded, we additionally pattern match on the request +method; if it’s GET, we use the handler function getOnlyGetR. +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 +to the handler function, and then convert the resulting YesodResponse into a +WAI Response.

+

Wonderful; one down, three to go. The next one is even easier.

+
            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+

Unlike OnlyGetR, AnyMethodR will work for any request method, so we don’t +need to perform any further pattern matching.

+
            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+

We add in one extra complication here: a dynamic parameter. While we used +toPathPiece to render to a textual value above, we now use fromPathPiece to +perform the parsing. Assuming the parse succeeds, we then follow a very similar +dispatch system as was used for OnlyGetR. The prime difference is that our +parameter needs to be passed to both the handler function and the route data +constructor.

+

Next we’ll look at the subsite, which is quite different.

+
            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+

Unlike the other pattern matches, here we just look to see if our pattern +prefix matches. Any route beginning with /my-subsite should be passed off to +the subsite for processing. This is where we finally get to use +yesodSubDispatch. This function closely mirrors yesodDispatch. We need to +construct a new environment to be passed to it. Let’s discuss the four +fields:

+
    +
  • +

    +ysreGetSub demonstrates how to get the subsite foundation type from the + master site. We provide getMySubsite, which is the function we provided in + the high-level route syntax. +

    +
  • +
  • +

    +ysreParentRunner provides a means of running a handler function. It may seem + a bit boring to just provide yesodRunner, but by having a separate parameter + we allow the construction of deeply nested subsites, which will wrap and + unwrap many layers of interleaving subsites. (This is a more advanced concept + which we won’t be covering in this chapter.) +

    +
  • +
  • +

    +ysreToParentRoute will convert a route for the subsite into a route for the + parent site. This is the purpose of the MySubsiteR constructor. This allows + subsites to use functions such as getRouteToParent. +

    +
  • +
  • +

    +ysreParentEnv simply passes on the initial environment, which contains a + number of things the subsite may need (such as logger). +

    +
  • +
+

The other interesting thing is how we modify the pathInfo. This allows subsites +to continue dispatching from where the parent site left off. To demonstrate +how this works, see some screenshots of various requests in the following +figure.

+

Path info in subsite

+ + + + + + +
+

And finally, not all requests will be valid routes. For those cases, we just +want to respond with a 404 not found.

+
            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+
+

Complete code

+

Following is the full code for the non-Template Haskell approach.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+import           Yesod.Core.Types           (YesodSubRunnerEnv (..))
+
+data App = App
+
+instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+
+type Handler = HandlerT App IO
+
+instance Yesod App
+
+instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req sendResponse = sendResponse $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+
+
+
+

Conclusion

+

Yesod abstracts away quite a bit of the plumbing from you as a developer. Most +of this is boilerplate code that you’ll be happy to ignore. But it can be +empowering to understand exactly what’s going on under the surface. At +this point, you should hopefully be able- with help from the Haddocks- to write +a site without any of the autogenerated Template Haskell code. Not that +I’d recommend it; I think using the generated code is easier and safer.

+

One particular advantage of understanding this material is seeing where Yesod +sits in the world of WAI. This makes it easier to see how Yesod will interact +with WAI middleware, or how to include code from other WAI framework in a Yesod +application (or vice-versa!).

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/visitor-counter.html b/public/book-1.6/visitor-counter.html new file mode 100644 index 00000000..ff1b487d --- /dev/null +++ b/public/book-1.6/visitor-counter.html @@ -0,0 +1,163 @@ + Visitor counter :: Yesod Web Framework Book- Version 1.6 +
+

Visitor counter

+ + +

Remember back in the good ol' days of the internet, where no website was +complete without a little "you are visitor number 32" thingy? Ahh, those were +the good times! Let’s recreate that wonderful experience in Yesod!

+

Now, if we wanted to do this properly, we’d store this information in some kind +of persistent storage layer, like a database, so that the information could be +shared across multiple horizontally-scaled web servers, and so that the +information would survive an app restart.

+

But our goal here isn’t to demonstrate good practice (after all, if it was +about good practice, I wouldn’t be demonstrating a visitor counter, right?). +Instead, this is meant to provide a simple example of sharing some state among +multiple handlers. A real-world use case would be caching information across +requests. Just remember that when you use the technique we’ll be showing, you +need to be careful about multiple app servers and app restarts.

+

The technique is simple: we create a new field in the foundation datatype for a +mutable reference to some data, and then access it in each handler. The +technique is so simple, it’s worth just diving into the code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import           Yesod
+
+data App = App
+    { visitors :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    visitorsRef <- fmap visitors getYesod
+    visitors <-
+        liftIO $ atomicModifyIORef visitorsRef $ \i ->
+        (i + 1, i + 1)
+    defaultLayout
+        [whamlet|
+            <p>Welcome, you are visitor number #{visitors}.
+        |]
+
+main :: IO ()
+main = do
+    visitorsRef <- newIORef 0
+    warp 3000 App
+        { visitors = visitorsRef
+        }
+

I used IORef here, since we didn’t need anything more than it provided, but +you’re free to use MVars or TVars as well. In fact, a good exercise for +the reader is to modify the above program to store the visitor count in a +TVar instead.

+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/web-application-interface.html b/public/book-1.6/web-application-interface.html new file mode 100644 index 00000000..cd3043f1 --- /dev/null +++ b/public/book-1.6/web-application-interface.html @@ -0,0 +1,345 @@ + Web Application Interface :: Yesod Web Framework Book- Version 1.6 +
+

Web Application Interface

+ + + +

It is a problem almost every language used for web development has dealt with: +the low level interface between the web server and the application. The +earliest example of a solution is the venerable and battle-worn Common Gateway Interface (CGI), +providing a language-agnostic interface using only standard input, standard +output and environment variables.

+

Back when Perl was becoming the de facto web programming language, a major +shortcoming of CGI became apparent: the process needed to be started anew for +each request. When dealing with an interpretted language and application +requiring database connection, this overhead became unbearable. FastCGI (and +later SCGI) arose as a successor to CGI, but it seems that much of the +programming world went in a different direction.

+

Each language began creating its own standard for interfacing with servers. +mod_perl. mod_python. mod_php. mod_ruby. Within the same language, multiple +interfaces arose. In some cases, we even had interfaces on top of interfaces. +And all of this led to much duplicated effort: a Python application designed to +work with FastCGI wouldn’t work with mod_python; mod_python only exists for +certain webservers; and these programming language specific web server +extensions need to be written for each programming language.

+

Haskell has its own history. We originally had the cgi package, which provided +a monadic interface. The fastcgi package then provided the same interface. +Meanwhile, it seemed that the majority of Haskell web development focused on +the standalone server. The problem is that each server comes with its own +interface, meaning that you need to target a specific backend. This means that +it is impossible to share common features, like GZIP encoding, development +servers, and testing frameworks.

+

WAI attempts to solve this, by providing a generic and efficient interface +between web servers and applications. Any handler supporting the interface +can serve any WAI application, while any application using the interface can +run on any handler.

+

At the time of writing, there are various backends, including Warp, FastCGI, +and development server. There are even more esoteric backends like +wai-handler-webkit for creating desktop apps. wai-extra provides many common +middleware components like GZIP, JSON-P and virtual hosting. wai-test makes it +easy to write unit tests, and wai-handler-devel lets you develop your +applications without worrying about stopping to compile. Yesod targets WAI, as +well as other Haskell web frameworks such as Scotty and MFlow. It’s also used +by some applications that skip the framework entirely, including Hoogle.

+ +
+

The Interface

+

The interface itself is very straight-forward: an application takes a request +and returns a response. A response is an HTTP status, a list of headers and a +response body. A request contains various information: the requested path, +query string, request body, HTTP version, and so on.

+

In order to handle resource management in an exception-safe manner, we use +continuation passing style for returning the response, similar to how the +bracket function works. This makes our definition of an application look +like:

+
type Application =
+    Request ->
+    (Response -> IO ResponseReceived) ->
+    IO ResponseReceived
+

The first argument is a Request, which shouldn’t be too surprising. The +second argument is the continuation, or what we should do with a Response. +Generally speaking, this will just be sending it to the client. We use the +special ResponseReceived type to ensure that the application does in fact +call the continuation.

+

This may seem a little strange, but usage is pretty straight-forward, as we’ll +demonstrate below.

+
+

Response Body

+

Haskell has a datatype known as a lazy bytestring. By utilizing laziness, you +can create large values without exhausting memory. Using lazy I/O, you can do +such tricks as having a value which represents the entire contents of a file, +yet only occupies a small memory footprint. In theory, a lazy bytestring is the +only representation necessary for a response body.

+

In practice, while lazy byte strings are wonderful for generating "pure" +values, the lazy I/O necessary to read a file introduces some non-determinism +into our programs. When serving thousands of small files a second, the limiting +factor is not memory, but file handles. Using lazy I/O, file handles may not be +freed immediately, leading to resource exhaustion. To deal with this, WAI +provides its own streaming data interface.

+

The core of this streaming interface is the Builder. A Builder represents +an action to fill up a buffer with bytes of data. This is more efficient than +simply passing ByteStrings around, as it can avoid multiple copies of data. +In many cases, an application needs only to provide a single Builder value. +And for that simple case, we have the ResponseBuilder constructor.

+

However, there are times when an Application will need to interleave IO +actions with yielding of data to the client. For that case, we have +ResponseStream. With ResponseStream, you provide a function. This +function in turn takes two actions: a "yield more data" action, and a "flush +the buffer" action. This allows you to yield data, perform IO actions, and +flush, as many times as you need, and with any interleaving desired.

+

There is one further optimization: many operating systems provide a sendfile +system call, which sends a file directly to a socket, bypassing a lot of the +memory copying inherent in more general I/O system calls. For that case, we +have a ResponseFile.

+

Finally, there are some cases where we need to break out of the HTTP mode +entirely. Two examples are WebSockets, where we need to upgrade a half-duplex +HTTP connection to a full-duplex connection, and HTTPS proxying, which requires +our proxy server to establish a connection, and then become a dumb data +transport agent. For these cases, we have the ResponseRaw constructor. Note +that not all WAI handlers can in fact support ResponseRaw, though the most +commonly used handler, Warp, does provide this support.

+
+
+

Request Body

+

Like response bodies, we could theoretically use a lazy ByteString for request +bodies, but in practice we want to avoid lazy I/O. Instead, the request body is +represented with a IO ByteString action (ByteString here being a strict +ByteString). Note that this does not return the entire request body, but +rather just the next chunk of data. Once you’ve consumed the entire request +body, further calls to this action will return an empty ByteString.

+

Note that, unlike response bodies, we have no need for using Builders on +the request side, since our purpose is purely for reading.

+

The request body could in theory contain any type of data, but the most common +are URL encoded and multipart form data. The wai-extra package contains +built-in support for parsing these in a memory-efficient manner.

+
+
+
+

Hello World

+

To demonstrate the simplicity of WAI, let’s look at a hello world example. In +this example, we’re going to use the OverloadedStrings language extension to +avoid explicitly packing string values into bytestrings.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.HTTP.Types (status200)
+import Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $
+  responseLBS status200 [("Content-Type", "text/plain")] "Hello World"
+
+main = run 3000 application
+

Lines 2 through 4 perform our imports. Warp is provided by the warp package, +and is the premiere WAI backend. WAI is also built on top of the http-types +package, which provides a number of datatypes and convenience values, including +status200.

+

First we define our application. Since we don’t care about the specific request +parameters, we ignore the first argument to the function, which contains the +request value. The second argument is our "send a response" function, which we +immediately use. The response value we send is built from a lazy ByteString +(thus responseLBS), with status code 200 ("OK"), a text/plain content type, +and a body containing the words "Hello World". Pretty straight-forward.

+
+
+

Resource allocation

+

Let’s make this a little more interesting, and try to allocate a resource for +our response. We’ll create an MVar in our main function to track the number +of requests, and then hold that MVar while sending each response.

+
{-# LANGUAGE OverloadedStrings #-}
+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)
+
+application countRef _ respond = do
+    modifyMVar countRef $ \count -> do
+        let count' = count + 1
+            msg = fromByteString "You are visitor number: " <>
+                  fromShow count'
+        responseReceived <- respond $ responseBuilder
+            status200
+            [("Content-Type", "text/plain")]
+            msg
+        return (count', responseReceived)
+
+main = do
+    visitorCount <- newMVar 0
+    run 3000 $ application visitorCount
+

This is where WAI’s continuation interface shines. We’re able to use the +standard modifyMVar function to acquire the MVar lock and send our +response. Note how we thread the responseReceived value through, though we +never actually use the value for anything. It is merely witness to the fact +that we have, in fact, sent a response.

+

Notice also how we take advantage of Builders in constructing our msg +value. Instead of concatenating two ByteStrings together directly, we +monoidally append two different Builder values. The advantage to this is that +the results will end up being copied directly into the final output buffer, +instead of first being copied into a temporary ByteString buffer to only +later be copied into the final buffer.

+
+
+

Streaming response

+

Let’s give our streaming interface a test as well:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (fromByteString)
+import           Control.Concurrent       (threadDelay)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai
+import           Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $ responseStream status200 [("Content-Type", "text/plain")]
+    $ \send flush -> do
+        send $ fromByteString "Starting the response...\n"
+        flush
+        threadDelay 1000000
+        send $ fromByteString "All done!\n"
+
+main = run 3000 application
+

We use responseStream, and our third argument is a function which takes our +"send a builder" and "flush the buffer" functions. Notice how we flush after +our first chunk of data, to make sure the client sees the data immediately. +However, there’s no need to flush at the end of a response. WAI requires that +the handler automatically flush at the end of a stream.

+
+
+

Middleware

+

In addition to allowing our applications to run on multiple backends without +code changes, the WAI allows us another benefits: middleware. Middleware is +essentially an application transformer, taking one application and returning +another one.

+

Middleware components can be used to provide lots of services: cleaning up +URLs, authentication, caching, JSON-P requests. But perhaps the most useful and +most intuitive middleware is gzip compression. The middleware works very +simply: it parses the request headers to determine if a client supports +compression, and if so compresses the response body and adds the appropriate +response header.

+

The great thing about middlewares is that they are unobtrusive. Let’s see how +we would apply the gzip middleware to our hello world application.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.Wai.Handler.Warp (run)
+import Network.Wai.Middleware.Gzip (gzip, def)
+import Network.HTTP.Types (status200)
+
+application _ respond = respond $ responseLBS status200 [("Content-Type", "text/plain")]
+                       "Hello World"
+
+main = run 3000 $ gzip def application
+

We added an import line to actually have access to the middleware, and then +simply applied gzip to our application. You can also chain together multiple +middlewares: a line such as gzip False $ jsonp $ othermiddleware $ +myapplication is perfectly valid. One word of warning: the order the +middleware is applied can be important. For example, jsonp needs to work on +uncompressed data, so if you apply it after you apply gzip, you’ll have +trouble.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/widgets.html b/public/book-1.6/widgets.html new file mode 100644 index 00000000..63e3eca6 --- /dev/null +++ b/public/book-1.6/widgets.html @@ -0,0 +1,635 @@ + Widgets :: Yesod Web Framework Book- Version 1.6 +
+

Widgets

+ + +

One of the challenges in web development is that we have to coordinate three +different client-side technologies: HTML, CSS and Javascript. Worse still, we +have to place these components in different locations on the page: CSS in a +style tag in the head, Javascript in a script tag before the closing body tag, and HTML in the +body. And never mind if you want to put your CSS and Javascript in separate +files!

+

In practice, this works out fairly nicely when building a single page, because +we can separate our structure (HTML), style (CSS) and logic (Javascript). But +when we want to build modular pieces of code that can be easily composed, it +can be a headache to coordinate all three pieces separately. Widgets are +Yesod’s solution to the problem. They also help with the issue of including +libraries, such as jQuery, one time only.

+

Our four template languages- Hamlet, Cassius, Lucius and Julius- provide the +raw tools for constructing your output. Widgets provide the glue that allows +them to work together seamlessly.

+
+

Synopsis

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR = defaultLayout $ do
+    setTitle "My Page Title"
+    toWidget [lucius| h1 { color: green; } |]
+    addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
+    toWidget
+        [julius|
+            $(function() {
+                $("h1").click(function(){
+                    alert("You clicked on the heading!");
+                });
+            });
+        |]
+    toWidgetHead
+        [hamlet|
+            <meta name=keywords content="some sample keywords">
+        |]
+    toWidget
+        [hamlet|
+            <h1>Here's one way of including content
+        |]
+    [whamlet|<h2>Here's another |]
+    toWidgetBody
+        [julius|
+            alert("This is included in the body itself");
+        |]
+
+main = warp 3000 App
+

This produces the following HTML (indentation added):

+
<!DOCTYPE html>
+<html>
+  <head>
+    <title>My Page Title</title>
+    <meta name="keywords" content="some sample keywords">
+    <style>h1{color:green}</style>
+  </head>
+  <body>
+    <h1>Here's one way of including content</h1>
+    <h2>Here's another</h2>
+    <script>
+      alert("This is included in the body itself");
+    </script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">
+    </script><script>
+      $(function() {
+        $('h1').click(function() {
+          alert("You clicked on the heading!");
+        });
+      });
+    </script>
+  </body>
+</html>
+
+
+

What’s in a Widget?

+

At a very superficial level, an HTML document is just a bunch of nested tags. +This is the approach most HTML generation tools take: you define hierarchies of +tags and are done with it. But let’s imagine that I want to write a component +of a page for displaying the navbar. I want this to be "plug and play": I call +the function at the right time, and the navbar is inserted at the correct point +in the hierarchy.

+

This is where our superficial HTML generation breaks down. Our navbar likely +consists of some CSS and JavaScript in addition to HTML. By the time we call +the navbar function, we have already rendered the <head> tag, so it is too +late to add a new <style> tag for our CSS declarations. Under normal +strategies, we would need to break up our navbar function into three parts: +HTML, CSS and JavaScript, and make sure that we always call all three pieces.

+

Widgets take a different approach. Instead of viewing an HTML document as a +monolithic tree of tags, widgets see a number of distinct components in the +page. In particular:

+
    +
  • +

    +The title +

    +
  • +
  • +

    +External stylesheets +

    +
  • +
  • +

    +External Javascript +

    +
  • +
  • +

    +CSS declarations +

    +
  • +
  • +

    +Javascript code +

    +
  • +
  • +

    +Arbitrary <head> content +

    +
  • +
  • +

    +Arbitrary <body> content +

    +
  • +
+

Different components have different semantics. For example, there can only be +one title, but there can be multiple external scripts and stylesheets. However, +those external scripts and stylesheets should only be included once. Arbitrary +head and body content, on the other hand, has no limitation (someone may want +to have five lorem ipsum blocks after all).

+

The job of a widget is to hold onto these disparate components and apply proper +logic for combining different widgets together. This consists of things like +taking the last title set and ignoring others, filtering duplicates from the +list of external scripts and stylesheets, and concatenating head and body +content.

+
+
+

Constructing Widgets

+

In order to use widgets, you’ll obviously need to be able to get your hands on +them. The most common way will be via the ToWidget typeclass, and its +toWidget method. This allows you to convert your Shakespearean templates +directly to a Widget: Hamlet code will appear in the body, Julius scripts +inside a <script>, and Cassius and Lucius in a <style> tag.

+ +

But what if you want to add some <meta> tags, which need to appear in +the head? Or if you want some Javascript to appear in the body instead of the +head? For these purposes, Yesod provides two additional type classes: +ToWidgetHead and ToWidgetBody. These work exactly as they seem they should. One example use case for this is to have fine-grained control of where your <script> tags end up getting inserted.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "toWidgetHead and toWidgetBody"
+    toWidgetBody
+        [hamlet|<script src=/included-in-body.js>|]
+    toWidgetHead
+        [hamlet|<script src=/included-in-head.js>|]
+
+main :: IO ()
+main = warp 3000 App
+

Note that even though toWidgetHead was called after toWidgetBody, the +latter <script> tag appears first in the generated HTML.

+

In addition, there are a number of other functions for creating specific kinds +of Widgets:

+
+
+setTitle +
+

+Turns some HTML into the page title. +

+
+
+toWidgetMedia +
+

+Works the same as toWidget, but takes an +additional parameter to indicate what kind of media this applies to. Useful for +creating print stylesheets, for instance. +

+
+
+addStylesheet +
+

+Adds a reference, via a <link> tag, to an external +stylesheet. Takes a type-safe URL. +

+
+
+addStylesheetRemote +
+

+Same as addStylesheet, but takes a normal URL. Useful +for referring to files hosted on a CDN, like Google’s jQuery UI CSS files. +

+
+
+addScript +
+

+Adds a reference, via a <script> tag, to an external script. +Takes a type-safe URL. +

+
+
+addScriptRemote +
+

+Same as addScript, but takes a normal URL. Useful for +referring to files hosted on a CDN, like Google’s jQuery. +

+
+
+
+
+

Combining Widgets

+

The whole idea of widgets is to increase composability. You can take these +individual pieces of HTML, CSS and Javascript, combine them together into +something more complicated, and then combine these larger entities into +complete pages. This all works naturally through the Monad instance of +Widget, meaning you can use do-notation to compose pieces together.

+
myWidget1 = do
+    toWidget [hamlet|<h1>My Title|]
+    toWidget [lucius|h1 { color: green } |]
+
+myWidget2 = do
+    setTitle "My Page Title"
+    addScriptRemote "http://www.example.com/script.js"
+
+myWidget = do
+    myWidget1
+    myWidget2
+
+-- or, if you want
+myWidget' = myWidget1 >> myWidget2
+ +
+
+

Generate IDs

+

If we’re really going for true code reuse here, we’re eventually going to run +into name conflicts. Let’s say that there are two helper libraries that both +use the class name “foo” to affect styling. We want to avoid such a +possibility. Therefore, we have the newIdent function. This function +automatically generates a word that is unique for this handler.

+
getRootR = defaultLayout $ do
+    headerClass <- newIdent
+    toWidget [hamlet|<h1 .#{headerClass}>My Header|]
+    toWidget [lucius| .#{headerClass} { color: green; } |]
+
+
+

whamlet

+

Let’s say you’ve got a fairly standard Hamlet template, that embeds another +Hamlet template to represent the footer:

+
page =
+    [hamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+
+footer =
+    [hamlet|
+        <footer>
+            <p>That's all folks!
+    |]
+

That works fine if the footer is plain old HTML, but what if we want to add +some style? Well, we can easily spice up the footer by turning it into a +Widget:

+
footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+

But now we’ve got a problem: a Hamlet template can only embed another Hamlet +template; it knows nothing about a Widget. This is where whamlet comes in. It +takes exactly the same syntax as normal Hamlet, and variable (#{…}) and URL +(@{…}) interpolation are unchanged. But embedding (^{…}) takes a Widget, +and the final result is a Widget. To use it, we can just do:

+
page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

There is also whamletFile, if you would prefer to keep your template in a +separate file.

+ +
+

Types

+

You may have noticed that I’ve been avoiding type signatures so far. The simple +answer is that each widget is a value of type Widget. But if you look through +the Yesod libraries, you’ll find no definition of the Widget type. What +gives?

+

Yesod defines a very similar type: data WidgetT site m a. This data type is a +monad transformer. The last two arguments are the underlying monad and the +monadic value, respectively. The site parameter is the specific foundation +type for your individual application. Since this type varies for each and every +site, it’s impossible for the libraries to define a single Widget datatype +which would work for every application.

+

Instead, the mkYesod Template Haskell function generates this type synonym +for you. Assuming your foundation data type is called MyApp, your Widget +synonym is defined as:

+
type Widget = WidgetT MyApp IO ()
+

We set the monadic value to be (), since a widget’s value will ultimately be +thrown away. IO is the standard base monad, and will be used in almost all +cases. The only exception is when writing a subsite. Subsites are a more +advanced topic, and will be covered later in their own chapter.

+

Once we know about our Widget type synonym, it’s easy to add signatures to +our previous code samples:

+
footer :: Widget
+footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+
+page :: Widget
+page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

When we start digging into handler functions some more, we’ll encounter a +similar situation with the HandlerT and Handler types.

+
+
+
+

Using Widgets

+

It’s all well and good that we have these beautiful Widget datatypes, but how +exactly do we turn them into something the user can interact with? The most +commonly used function is defaultLayout, which essentially has the type +signature Widget → Handler Html.

+

defaultLayout is actually a typeclass method, which can be overridden for +each application. This is how Yesod apps are themed. So we’re still left with +the question: when we’re inside defaultLayout, how do we unwrap a Widget? +The answer is widgetToPageContent. Let’s look at some (simplified) types:

+
data PageContent url = PageContent
+    { pageTitle :: Html
+    , pageHead :: HtmlUrl url
+    , pageBody :: HtmlUrl url
+    }
+widgetToPageContent :: Widget -> Handler (PageContent url)
+

This is getting closer to what we need. We now have direct access to the HTML +making up the head and body, as well as the title. At this point, we can use +Hamlet to combine them all together into a single document, along with our site +layout, and we use withUrlRenderer to convert that Hamlet result into actual +HTML that’s ready to be shown to the user. The next example demonstrates this +process.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent widget
+    withUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    <style>body { font-family: verdana }
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

There’s still one thing that bothers me: that style tag. There are a few +problems with it:

+
    +
  • +

    +Unlike Lucius or Cassius, it doesn’t get compile-time checked for + correctness. +

    +
  • +
  • +

    +Granted that the current example is very simple, but in something more + complicated we could get into character escaping issues. +

    +
  • +
  • +

    +We’ll now have two style tags instead of one: the one produced by myLayout, + and the one generated in the pageHead based on the styles set in the + widget. +

    +
  • +
+

We have one more trick in our bag to address this: we apply some last-minute +adjustments to the widget itself before calling widgetToPageContent. It’s +actually very easy to do: we just use do-notation again.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent $ do
+        widget
+        toWidget [lucius| body { font-family: verdana } |]
+    withUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Using handler functions

+

We haven’t covered too much of the handler functionality yet, but once we do, +the question arises: how do we use those functions in a widget? For example, +what if your widget needs to look up a query string parameter using +lookupGetParam?

+

The first answer is the function handlerToWidget, which can convert a +Handler action into a Widget answer. However, in many cases, this won’t be +necessary. Consider the type signature of lookupGetParam:

+
lookupGetParam :: MonadHandler m => Text -> m (Maybe Text)
+

This function will live in any instance of MonadHandler. And conveniently, +Widget is also a MonadHandler instance. This means that most code can be +run in either Handler or Widget. And if you need to explicitly convert from +Handler to Widget, you can always use handlerToWidget.

+ +
+
+

Summary

+

The basic building block of each page is a widget. Individual snippets of HTML, +CSS, and Javascript can be turned into widgets via the polymorphic toWidget +function. Using do-notation, you can combine these individual widgets into +larger widgets, eventually containing all the content of your page.

+

Unwrapping these widgets is usually performed within the defaultLayout +function, which can be used to apply a unified look-and-feel to all your pages.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/wiki-chat-example.html b/public/book-1.6/wiki-chat-example.html new file mode 100644 index 00000000..f098d8d7 --- /dev/null +++ b/public/book-1.6/wiki-chat-example.html @@ -0,0 +1,594 @@ + Wiki: markdown, chat subsite, event source :: Yesod Web Framework Book- Version 1.6 +
+

Wiki: markdown, chat subsite, event source

+ + +

This example will tie together a few different ideas. We’ll start with a chat +subsite, which allows us to embed a chat widget on any page. We’ll use the HTML +5 event source API to handle sending events from the server to the client.

+
+

Subsite: data

+

In order to define a subsite, we first need to create a foundation type for the +subsite, the same as we would do for a normal Yesod application. In our case, +we want to keep a channel of all the events to be sent to the individual +participants of a chat. This ends up looking like:

+
-- @Chat/Data.hs
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+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
+import           Yesod
+import           Yesod.Core.Types (SubHandlerFor)
+
+-- | Our subsite foundation. We keep a channel of events that all connections
+-- will share.
+data Chat = Chat (Chan ServerEvent)
+

We also need to define our subsite routes in the same module. We need to have +two commands: one to send a new message to all users, and another to receive +the stream of messages.

+
-- @Chat/Data.hs
+mkYesodSubData "Chat" [parseRoutes|
+/send SendR POST
+/recv ReceiveR GET
+|]
+
+
+

Subsite: handlers

+

Now that we’ve defined our foundation and routes, we need to create a separate +module for providing the subsite dispatch functionality. We’ll call this +module Chat, and it’s where we’ll start to see how a subsite functions.

+

A subsite always sits as a layer on top of some master site, which will be +provided by the user. In many cases, a subsite will require specific +functionality to be present in the master site. In the case of our chat +subsite, we want user authentication to be provided by the master site. The +subsite needs to be able to query whether the current user is logged into the +site, and to get the user’s name.

+

The way we represent this concept is to define a typeclass that encapsulates +the necessary functionality. Let’s have a look at our YesodChat typeclass:

+
-- @Chat/Data.hs
+class (Yesod master, RenderMessage master FormMessage)
+        => YesodChat master where
+    getUserName :: HandlerFor master Text
+    isLoggedIn :: HandlerFor master Bool
+

Any master site which wants to use the chat subsite will need to provide a +YesodChat instance. (We’ll see in a bit how this requirement is enforced.) +There are a few interesting things to note:

+
    +
  • +

    +We can put further constraints on the master site, such as providing a + Yesod instance and allowing rendering of form messages. The former allows + us to use defaultLayout, while the latter allows us to use standard form + widgets. +

    +
  • +
  • +

    +Previously in the book, we’ve used the Handler monad quite a bit. Remember + that Handler is just an application-specific type synonym around + HandlerFor. Since this code is intended to work with many different + applications, we use the full HandlerFor form of the transformer. +

    +
  • +
+

Speaking of the Handler type synonym, we’re going to want to have +something similar for our subsite. The question is: what does this +monad look like? In a subsite situation, we use SubHandlerFor with +both the subsite data type and the master site type. We’ll define a +helper synonym for this which requires a YesodChat instance on the +master site type, so we end up with:

+
-- @Chat/Data.hs
+type ChatHandler a =
+    forall master. YesodChat master =>
+    SubHandlerFor Chat master a
+

Now that we have our machinery out of the way, it’s time to write our subsite +handler functions. We had two routes: one for sending messages, and one for +receiving messages. Let’s start with sending. We need to:

+
    +
  1. +

    +Get the username for the person sending the message. +

    +
  2. +
  3. +

    +Parse the message from the incoming parameters. (Note that we’re going to use GET parameters for simplicity of the client-side Ajax code.) +

    +
  4. +
  5. +

    +Write the message to the Chan. +

    +
  6. +
+

The trickiest bit of all this code is to know when to use lift. Let’s look at +the implementation, and then discuss those lift usages:

+
-- @Chat/Data.hs
+postSendR :: ChatHandler ()
+postSendR = do
+    from <- liftHandler getUserName
+    body <- runInputGet $ ireq textField "message"
+    Chat chan <- getSubYesod
+    liftIO $ writeChan chan $ ServerEvent Nothing Nothing $ return $
+        fromText from <> fromText ": " <> fromText body
+

getUserName is the function we defined in our YesodChat typeclass earlier. +If we look at that type signature, we see that it lives in the master site’s +Handler monad. Therefore, we need to lift that call out of the subsite.

+

The next call to getSubYesod is not lifted. The reasoning here is simple: +we want to get the subsite’s foundation type in order to access the message +channel. If we instead lifted that call, we’d get the master site’s +foundation type instead, which is not what we want in this case.

+

The final line puts the new message into the channel. Since this is an IO +action, we use liftIO. ServerEvent is part of the wai-eventsource +package, and is the means by which we’re providing server-sent events in this +example.

+

The receiving side is similarly simple:

+
-- @Chat/Data.hs
+getReceiveR :: ChatHandler ()
+getReceiveR = do
+    Chat chan <- getSubYesod
+    sendWaiApplication $ eventSourceAppChan chan
+

The last line in our function exposes the underlying wai-eventsource +application as a Yesod handler, using the sendWaiApplication function to +promote a WAI application to a Yesod handler. eventSourceAppChan duplicates +the chan under the hood, which is a standard method in concurrent Haskel +of creating broadcast channels.

+

Now that we’ve defined our handler functions, we can set up our dispatch. In a +normal application, dispatching is handled by calling mkYesod, which creates +the appropriate YesodDispatch instance. In subsites, things are a little bit +more complicated, since you’ll often want to place constraints on the master +site. The formula we use is the following:

+
-- @Chat.hs
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+module Chat where
+
+import           Chat.Data
+import           Yesod
+
+instance YesodChat master => YesodSubDispatch Chat master where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesChat)
+

We’re stating that our Chat subsite can live on top of any master site which +is an instance of YesodChat. We then use the mkYesodSubDispatch Template +Haskell function to generate all of our dispatching logic. While this is a bit +more difficult to write than mkYesod, it provides necessary flexibility, and +is mostly identical for any subsite you’ll write.

+
+
+

Subsite: widget

+

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.

+

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 +URLs. Let’s start off our widget:

+
-- @Chat.hs
+chatWidget :: YesodChat master
+           => (Route Chat -> Route master)
+           -> WidgetFor master ()
+chatWidget toMaster = do
+

Next, we’re going to generate some identifiers to be used by our widget. It’s +always good practice to let Yesod generate unique identifiers for you instead +of creating them manually to avoid name collisions.

+
-- @Chat.hs
+    chat <- newIdent   -- the containing div
+    output <- newIdent -- the box containing the messages
+    input <- newIdent  -- input field from the user
+

And next we need to check if the user is logged in, using the isLoggedIn +function in our YesodChat typeclass. Since we’re in a Widget and that +function lives in the Handler monad, we need to use handlerToWidget:

+
-- @Chat.hs
+    ili <- handlerToWidget isLoggedIn  -- check if we're already logged in
+

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 +client-side code wrapped in a Widget:

+
-- @Chat.hs
+    if ili
+        then do
+            -- Logged in: show the widget
+            [whamlet|
+                <div ##{chat}>
+                    <h2>Chat
+                    <div ##{output}>
+                    <input ##{input} type=text placeholder="Enter Message">
+            |]
+            -- Just some CSS
+            toWidget [lucius|
+                ##{chat} {
+                    position: absolute;
+                    top: 2em;
+                    right: 2em;
+                }
+                ##{output} {
+                    width: 200px;
+                    height: 300px;
+                    border: 1px solid #999;
+                    overflow: auto;
+                }
+            |]
+            -- And now that Javascript
+            toWidgetBody [julius|
+                // Set up the receiving end
+                var output = document.getElementById(#{toJSON output});
+                var src = new EventSource("@{toMaster ReceiveR}");
+                src.onmessage = function(msg) {
+                    // This function will be called for each new message.
+                    var p = document.createElement("p");
+                    p.appendChild(document.createTextNode(msg.data));
+                    output.appendChild(p);
+
+                    // And now scroll down within the output div so the most recent message
+                    // is displayed.
+                    output.scrollTop = output.scrollHeight;
+                };
+
+                // Set up the sending end: send a message via Ajax whenever the user hits
+                // enter.
+                var input = document.getElementById(#{toJSON input});
+                input.onkeyup = function(event) {
+                    var keycode = (event.keyCode ? event.keyCode : event.which);
+                    if (keycode == '13') {
+                        var xhr = new XMLHttpRequest();
+                        var val = input.value;
+                        input.value = "";
+                        var params = "?message=" + encodeURI(val);
+                        xhr.open("POST", "@{toMaster SendR}" + params);
+                        xhr.send(null);
+                    }
+                }
+            |]
+

And finally, if the user isn’t logged in, we’ll ask them to log in to use the +chat app.

+
-- @Chat.hs
+        else do
+            -- User isn't logged in, give a not-logged-in message.
+            master <- getYesod
+            [whamlet|
+                <p>
+                    You must be #
+                    $maybe ar <- authRoute master
+                        <a href=@{ar}>logged in
+                    $nothing
+                        logged in
+                    \ to chat.
+            |]
+
+
+

Master site: data

+

Now we can proceed with writing our main application. This application will +include the chat subsite and a wiki. The first thing we need to consider is how +to store the wiki contents. Normally, we’d want to put this in some kind of a +Persistent database. For simplicity, we’ll just use an in-memory +representation. Each Wiki page is indicated by a list of names, and the contents of each page is going to be a piece of Text. So our full foundation datatype is:

+
-- @ChatMain.hs
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE ViewPatterns          #-}
+module ChatMain where
+
+import           Chat
+import           Chat.Data
+import           Control.Concurrent.Chan (newChan)
+import           Data.IORef
+import           Data.Map                (Map)
+import qualified Data.Map                as Map
+import           Data.Text               (Text)
+import qualified Data.Text.Lazy          as TL
+import           Text.Markdown
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+import           System.SetEnv
+
+data App = App
+    { getChat     :: Chat
+    , wikiContent :: IORef (Map [Text] Text)
+    }
+

Next we want to set up our routes:

+
-- @ChatMain.hs
+mkYesod "App" [parseRoutes|
+/            HomeR GET      -- the homepage
+/wiki/*Texts WikiR GET POST -- note the multipiece for the wiki hierarchy
+
+/chat        ChatR Chat getChat    -- the chat subsite
+/auth        AuthR Auth getAuth    -- the auth subsite
+|]
+
+
+

Master site: instances

+

We need to make two modifications to the default Yesod instance. Firstly, we +want to provide an implementation of authRoute, so that our chat subsite +widget can provide a proper link to a login page. Secondly, we’ll provide a +override to the defaultLayout. Besides providing login/logout links, this +function will add in the chat widget on every page.

+
-- @ChatMain.hs
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR -- get a working login link
+
+    -- Our custom defaultLayout will add the chat widget to every page.
+    -- We'll also add login and logout links to the top.
+    defaultLayout widget = do
+        pc <- widgetToPageContent $ do
+            widget
+            chatWidget ChatR
+        mmsg <- getMessage
+        withUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <div .message>#{msg}
+                        <nav>
+                            <a href=@{AuthR LoginR}>Login
+                            \ | #
+                            <a href=@{AuthR LogoutR}>Logout
+                        ^{pageBody pc}
+            |]
+

Since we’re using the chat subsite, we have to provide an instance of +YesodChat.

+
-- @ChatMain.hs
+instance YesodChat App where
+    getUserName = do
+        muid <- maybeAuthId
+        case muid of
+            Nothing -> do
+                setMessage "Not logged in"
+                redirect $ AuthR LoginR
+            Just uid -> return uid
+    isLoggedIn = do
+        ma <- maybeAuthId
+        return $ maybe False (const True) ma
+

Our YesodAuth and RenderMessage instances, as well as the homepage handler, +are rather bland:

+
-- @ChatMain.hs
+-- Fairly standard YesodAuth instance. We'll use the dummy plugin so that you
+-- can create any name you want, and store the login name as the AuthId.
+instance YesodAuth App where
+    type AuthId App = Text
+    authPlugins _ = [authDummy]
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Nothing special here, just giving a link to the root of the wiki.
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Welcome to the Wiki!
+        <p>
+            <a href=@{wikiRoot}>Wiki root
+    |]
+  where
+    wikiRoot = WikiR []
+
+
+

Master site: wiki handlers

+

Now it’s time to write our wiki handlers: a GET for displaying a page, and a +POST for updating a page. We’ll also define a wikiForm function to be used on +both handlers:

+
-- @ChatMain.hs
+-- A form for getting wiki content
+wikiForm :: Maybe Textarea -> Html -> MForm Handler (FormResult Textarea, Widget)
+wikiForm mtext = renderDivs $ areq textareaField "Page body" mtext
+
+-- Show a wiki page and an edit form
+getWikiR :: [Text] -> Handler Html
+getWikiR page = do
+    -- Get the reference to the contents map
+    icontent <- fmap wikiContent getYesod
+
+    -- And read the map from inside the reference
+    content <- liftIO $ readIORef icontent
+
+    -- Lookup the contents of the current page, if available
+    let mtext = Map.lookup page content
+
+    -- Generate a form with the current contents as the default value.
+    -- Note that we use the Textarea wrapper to get a <textarea>.
+    (form, _) <- generateFormPost $ wikiForm $ fmap Textarea mtext
+    defaultLayout $ do
+        case mtext of
+            -- We're treating the input as markdown. The markdown package
+            -- automatically handles XSS protection for us.
+            Just text -> toWidget $ markdown def $ TL.fromStrict text
+            Nothing -> [whamlet|<p>Page does not yet exist|]
+        [whamlet|
+            <h2>Edit page
+            <form method=post>
+                ^{form}
+                <div>
+                    <input type=submit>
+        |]
+
+-- Get a submitted wiki page and updated the contents.
+postWikiR :: [Text] -> Handler Html
+postWikiR page = do
+    icontent <- fmap wikiContent getYesod
+    content <- liftIO $ readIORef icontent
+    let mtext = Map.lookup page content
+    ((res, form), _) <- runFormPost $ wikiForm $ fmap Textarea mtext
+    case res of
+        FormSuccess (Textarea t) -> do
+            liftIO $ atomicModifyIORef icontent $
+                \m -> (Map.insert page t m, ())
+            setMessage "Page updated"
+            redirect $ WikiR page
+        _ -> defaultLayout
+                [whamlet|
+                    <form method=post>
+                        ^{form}
+                        <div>
+                            <input type=submit>
+                |]
+
+
+

Master site: running

+

Finally, we’re ready to run our application. Unlike many of our previous +examples in this book, we need to perform some real initialization in the +main function. The Chat subsite requires an empty Chan to be created, and +we need to create a mutable variable to hold the wiki contents. Once we have +those values, we can create an App value and pass it to the warp function.

+
-- @ChatMain.hs
+main :: IO ()
+main = do
+    -- Create our server event channel
+    chan <- newChan
+
+    -- Initially have a blank database of wiki pages
+    icontent <- newIORef Map.empty
+
+    -- Set web server's listening port required by warpEnv function
+    -- This env var is set up automatically if 'yesod devel' is used
+    setEnv "PORT" "3000"
+
+    -- Run our app
+    warpEnv App
+        { getChat = Chat chan
+        , wikiContent = icontent
+        }
+
+
+

Conclusion

+

This example demonstrated creation of a non-trivial subsite. Some important +points to notice were the usage of typeclasses to express constraints on the +master site, how data initialization was performed in the main function, and +how lifting allowed us to operate in either the subsite or master site +context.

+

If you’re looking for a way to test out your subsite skills, I’d recommend +modifying this example so that the Wiki code also lived in its own subsite.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/xml.html b/public/book-1.6/xml.html new file mode 100644 index 00000000..80e4c64d --- /dev/null +++ b/public/book-1.6/xml.html @@ -0,0 +1,833 @@ + xml-conduit :: Yesod Web Framework Book- Version 1.6 +
+

xml-conduit

+ + +

Many developers cringe at the thought of dealing with XML files. XML has the +reputation of having a complicated data model, with obfuscated libraries and +huge layers of complexity sitting between you and your goal. I’d like to posit +that a lot of that pain is actually a language and library issue, not inherent +to XML.

+

Once again, Haskell’s type system allows us to easily break down the problem to +its most basic form. The xml-types package neatly deconstructs the XML data +model (both a streaming and DOM-based approach) into some simple ADTs. +Haskell’s standard immutable data structures make it easier to apply transforms +to documents, and a simple set of functions makes parsing and rendering a +breeze.

+

We’re going to be covering the xml-conduit package. Under the surface, this +package uses a lot of the approaches Yesod in general does for high +performance: blaze-builder, text, conduit and attoparsec. But from a user +perspective, it provides everything from the simplest APIs +(readFile/writeFile) through full control of XML event streams.

+

In addition to xml-conduit, there are a few related packages that come into +play, like xml-hamlet and xml2html. We’ll cover both how to use all these +packages, and when they should be used.

+
+

Synopsis

+
<!-- Input XML file -->
+<document title="My Title">
+    <para>This is a paragraph. It has <em>emphasized</em> and <strong>strong</strong> words.</para>
+    <image href="myimage.png"/>
+</document>
+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import qualified Data.Map        as M
+import           Prelude         hiding (readFile, writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = do
+    -- readFile will throw any parse errors as runtime exceptions
+    -- def uses the default settings
+    Document prologue root epilogue <- readFile def "input.xml"
+
+    -- root is the root element of the document, let's modify it
+    let root' = transform root
+
+    -- And now we write out. Let's indent our output
+    writeFile def
+        { rsPretty = True
+        } "output.html" $ Document prologue root' epilogue
+
+-- We'll turn out <document> into an XHTML document
+transform :: Element -> Element
+transform (Element _name attrs children) = Element "html" M.empty
+    [xml|
+        <head>
+            <title>
+                $maybe title <- M.lookup "title" attrs
+                    \#{title}
+                $nothing
+                    Untitled Document
+        <body>
+            $forall child <- children
+                ^{goNode child}
+    |]
+
+goNode :: Node -> [Node]
+goNode (NodeElement e) = [NodeElement $ goElem e]
+goNode (NodeContent t) = [NodeContent t]
+goNode (NodeComment _) = [] -- hide comments
+goNode (NodeInstruction _) = [] -- and hide processing instructions too
+
+-- convert each source element to its XHTML equivalent
+goElem :: Element -> Element
+goElem (Element "para" attrs children) =
+    Element "p" attrs $ concatMap goNode children
+goElem (Element "em" attrs children) =
+    Element "i" attrs $ concatMap goNode children
+goElem (Element "strong" attrs children) =
+    Element "b" attrs $ concatMap goNode children
+goElem (Element "image" attrs _children) =
+    Element "img" (fixAttr attrs) [] -- images can't have children
+  where
+    fixAttr mattrs
+        | "href" `M.member` mattrs  = M.delete "href" $ M.insert "src" (mattrs M.! "href") mattrs
+        | otherwise                 = mattrs
+goElem (Element name attrs children) =
+    -- don't know what to do, just pass it through...
+    Element name attrs $ concatMap goNode children
+
<?xml version="1.0" encoding="UTF-8"?>
+<!-- Output XHTML -->
+<html>
+    <head>
+        <title>
+            My Title
+        </title>
+    </head>
+    <body>
+        <p>
+            This is a paragraph. It has
+            <i>
+                emphasized
+            </i>
+            and
+            <b>
+                strong
+            </b>
+            words.
+        </p>
+        <img src="myimage.png"/>
+    </body>
+</html>
+
+
+

Types

+

Let’s take a bottom-up approach to analyzing types. This section will also +serve as a primer on the XML data model itself, so don’t worry if you’re not +completely familiar with it.

+

I think the first place where Haskell really shows its strength is with the +Name datatype. Many languages (like Java) struggle with properly expressing +names. The issue is that there are in fact three components to a name: its +local name, its namespace (optional), and its prefix (also optional). Let’s +look at some XML to explain:

+
<no-namespace/>
+<no-prefix xmlns="first-namespace" first-attr="value1"/>
+<foo:with-prefix xmlns:foo="second-namespace" foo:second-attr="value2"/>
+

The first tag has a local name of no-namespace, and no namespace or prefix. +The second tag (local name: no-prefix) also has no prefix, but it does have +a namespace (first-namespace). first-attr, however, does not inherit that +namespace: attribute namespaces must always be explicitly set with a prefix.

+ +

The third tag has a local name of with-prefix, a prefix of foo and a +namespace of second-namespace. Its attribute has a second-attr local name +and the same prefix and namespace. The xmlns and xmlns:foo attributes are +part of the namespace specification, and are not considered attributes of their +respective elements.

+

So let’s review what we need from a name: every name has a local name, and it +can optionally have a prefix and namespace. Seems like a simple fit for a +record type:

+
data Name = Name
+    { nameLocalName :: Text
+    , nameNamespace :: Maybe Text
+    , namePrefix    :: Maybe Text
+    }
+

According the XML namespace standard, two names are considered equivalent +if they have the same localname and namespace. In other words, the prefix is +not important. Therefore, xml-types defines Eq and Ord instances that +ignore the prefix.

+

The last class instance worth mentioning is IsString. It would be very +tedious to have to manually type out Name "p" Nothing Nothing every time we +want a paragraph. If you turn on OverloadedStrings, "p" will resolve to +that all by itself! In addition, the IsString instance recognizes something +called Clark notation, which allows you to prefix the namespace surrounded in +curly brackets. In other words:

+
"{namespace}element" == Name "element" (Just "namespace") Nothing
+"element" == Name "element" Nothing Nothing
+
+

The Four Types of Nodes

+

XML documents are a tree of nested nodes. There are in fact four different +types of nodes allowed: elements, content (i.e., text), comments, and +processing instructions.

+ +

Since processing instructions have two pieces of text associated with them (the +target and the data), we have a simple data type:

+
data Instruction = Instruction
+    { instructionTarget :: Text
+    , instructionData :: Text
+    }
+

Comments have no special datatype, since they are just text. But content is an +interesting one: it could contain either plain text or unresolved entities +(e.g., &copyright-statement;). xml-types keeps those unresolved entities +in all the data types in order to completely match the spec. However, in +practice, it can be very tedious to program against those data types. And in +most use cases, an unresolved entity is going to end up as an error anyway.

+

Therefore, the Text.XML module defines its own set of datatypes for nodes, +elements and documents that removes all unresolved entities. If you need to +deal with unresolved entities instead, you should use the Text.XML.Unresolved +module. From now on, we’ll be focusing only on the Text.XML data types, +though they are almost identical to the xml-types versions.

+

Anyway, after that detour: content is just a piece of text, and therefore it +too does not have a special datatype. The last node type is an element, which +contains three pieces of information: a name, a map of attribute name/value +pairs, and a list of children nodes. (In xml-types, this value could contain +unresolved entities as well.) So our Element is defined as:

+
data Element = Element
+    { elementName :: Name
+    , elementAttributes :: Map Name Text
+    , elementNodes :: [Node]
+    }
+

Which of course begs the question: what does a Node look like? This is where +Haskell really shines: its sum types model the XML data model perfectly.

+
data Node
+    = NodeElement Element
+    | NodeInstruction Instruction
+    | NodeContent Text
+    | NodeComment Text
+
+
+

Documents

+

So now we have elements and nodes, but what about an entire document? Let’s +just lay out the datatypes:

+
data Document = Document
+    { documentPrologue :: Prologue
+    , documentRoot :: Element
+    , documentEpilogue :: [Miscellaneous]
+    }
+
+data Prologue = Prologue
+    { prologueBefore :: [Miscellaneous]
+    , prologueDoctype :: Maybe Doctype
+    , prologueAfter :: [Miscellaneous]
+    }
+
+data Miscellaneous
+    = MiscInstruction Instruction
+    | MiscComment Text
+
+data Doctype = Doctype
+    { doctypeName :: Text
+    , doctypeID :: Maybe ExternalID
+    }
+
+data ExternalID
+    = SystemID Text
+    | PublicID Text Text
+

The XML spec says that a document has a single root element (documentRoot). +It also has an optional doctype statement. Before and after both the doctype +and the root element, you are allowed to have comments and processing +instructions. (You can also have whitespace, but that is ignored in the +parsing.)

+

So what’s up with the doctype? Well, it specifies the root element of the +document, and then optional public and system identifiers. These are used to +refer to DTD files, which give more information about the file (e.g., +validation rules, default attributes, entity resolution). Let’s see some +examples:

+
<!DOCTYPE root> <!-- no external identifier -->
+<!DOCTYPE root SYSTEM "root.dtd"> <!-- a system identifier -->
+<!DOCTYPE root PUBLIC "My Root Public Identifier" "root.dtd"> <!-- public identifiers have a system ID as well -->
+

And that, my friends, is the entire XML data model. For many parsing purposes, +you’ll be able to simply ignore the entire Document datatype and go +immediately to the documentRoot.

+
+
+

Events

+

In addition to the document API, xml-types defines an Event datatype. This +can be used for constructing streaming tools, which can be much more memory +efficient for certain kinds of processing (eg, adding an extra attribute to all +elements). We will not be covering the streaming API currently, though it +should look very familiar after analyzing the document API.

+ +
+
+
+

Text.XML

+

The recommended entry point to xml-conduit is the Text.XML module. This module +exports all of the datatypes you’ll need to manipulate XML in a DOM fashion, as +well as a number of different approaches for parsing and rendering XML content. +Let’s start with the simple ones:

+
readFile  :: ParseSettings  -> FilePath -> IO Document
+writeFile :: RenderSettings -> FilePath -> Document -> IO ()
+

This introduces the ParseSettings and RenderSettings datatypes. You can use +these to modify the behavior of the parser and renderer, such as adding +character entities and turning on pretty (i.e., indented) output. Both these +types are instances of the Default typeclass, so you can simply use def when +these need to be supplied. That is how we will supply these values through the +rest of the chapter; please see the API docs for more information.

+

It’s worth pointing out that in addition to the file-based API, there is also a +text- and bytestring-based API. The bytestring-powered functions all perform +intelligent encoding detections, and support UTF-8, UTF-16 and UTF-32, in +either big or little endian, with and without a Byte-Order Marker (BOM). All +output is generated in UTF-8.

+

For complex data lookups, we recommend using the higher-level cursors API. The +standard Text.XML API not only forms the basis for that higher level, but is +also a great API for simple XML transformations and for XML generation. See the +synopsis for an example.

+
+

A note about file paths

+

In the type signature above, we have a type FilePath. However, this isn’t +Prelude.FilePath. The standard Prelude defines a type synonym type FilePath += [Char]. Unfortunately, there are many limitations to using such an +approach, including confusion of filename character encodings and differences +in path separators.

+

Instead, xml-conduit uses the system-filepath package, which defines an +abstract FilePath type. I’ve personally found this to be a much nicer +approach to work with. The package is fairly easy to follow, so I won’t go into +details here. But I do want to give a few quick explanations of how to use it:

+
    +
  • +

    +Since a FilePath is an instance of IsString, you can type in regular + strings and they will be treated properly, as long as the OverloadedStrings + extension is enabled. (I highly recommend enabling it anyway, as it makes + dealing with Text values much more pleasant.) +

    +
  • +
  • +

    +If you need to explicitly convert to or from Prelude's FilePath, you + should use the encodeString and decodeString, respectively. This takes into + account file path encodings. +

    +
  • +
  • +

    +Instead of manually splicing together directory names and file names with + extensions, use the operators in the Filesystem.Path.CurrentOS module, e.g. + myfolder </> filename <.> extension. +

    +
  • +
+
+
+
+

Cursor

+

Suppose you want to pull the title out of an XHTML document. You could do so +with the Text.XML interface we just described, using standard pattern +matching on the children of elements. But that would get very tedious, very +quickly. Probably the gold standard for these kinds of lookups is XPath, where +you would be able to write /html/head/title. And that’s exactly what inspired +the design of the Text.XML.Cursor combinators.

+

A cursor is an XML node that knows its location in the tree; it’s able to +traverse upwards, sideways, and downwards. (Under the surface, this is achieved +by tying the knot.) +There are two functions available for creating cursors from Text.XML types: +fromDocument and fromNode.

+

We also have the concept of an Axis, defined as type Axis = Cursor -> +[Cursor]. It’s easiest to get started by looking at example axes: child +returns zero or more cursors that are the child of the current one, parent +returns the single parent cursor of the input, or an empty list if the input is +the root element, and so on.

+

In addition, there are some axes that take predicates. element is a commonly +used function that filters down to only elements which match the given name. +For example, element "title" will return the input element if its name is +"title", or an empty list otherwise.

+

Another common function which isn’t quite an axis is content :: Cursor +-> [Text]. For all content nodes, it returns the contained text; +otherwise, it returns an empty list.

+

And thanks to the monad instance for lists, it’s easy to string all of these +together. For example, to do our title lookup, we would write the following +program:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+            child cursor >>= element "head" >>= child
+                         >>= element "title" >>= descendant >>= content
+

What this says is:

+
    +
  1. +

    +Get me all the child nodes of the root element +

    +
  2. +
  3. +

    +Filter down to only the elements named "head" +

    +
  4. +
  5. +

    +Get all the children of all those head elements +

    +
  6. +
  7. +

    +Filter down to only the elements named "title" +

    +
  8. +
  9. +

    +Get all the descendants of all those title elements. (A descendant is a + child, or a descendant of a child. Yes, that was a recursive definition.) +

    +
  10. +
  11. +

    +Get only the text nodes. +

    +
  12. +
+

So for the input document:

+
<html>
+    <head>
+        <title>My <b>Title</b></title>
+    </head>
+    <body>
+        <p>Foo bar baz</p>
+    </body>
+</html>
+

We end up with the output My Title. This is all well and good, but it’s much +more verbose than the XPath solution. To combat this verbosity, Aristid +Breitkreuz added a set of operators to the Cursor module to handle many common +cases. So we can rewrite our example as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $/ element "head" &/ element "title" &// content
+

$/ says to apply the axis on the right to the children of the cursor on the +left. &/ is almost identical, but is instead used to combine two axes +together. This is a general rule in Text.XML.Cursor: operators beginning with +$ directly apply an axis, while & will combine two together. &// is +used for applying an axis to all descendants.

+

Let’s go for a more complex, if more contrived, example. We have a document +that looks like:

+
<html>
+    <head>
+        <title>Headings</title>
+    </head>
+    <body>
+        <hgroup>
+            <h1>Heading 1 foo</h1>
+            <h2 class="foo">Heading 2 foo</h2>
+        </hgroup>
+        <hgroup>
+            <h1>Heading 1 bar</h1>
+            <h2 class="bar">Heading 2 bar</h2>
+        </hgroup>
+    </body>
+</html>
+

We want to get the content of all the h1 tags which precede an h2 tag with +a class attribute of "bar". To perform this convoluted lookup, we can write:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test2.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $// element "h2"
+               >=> attributeIs "class" "bar"
+               >=> precedingSibling
+               >=> element "h1"
+               &// content
+

Let’s step through that. First we get all h2 elements in the document. ($// +gets all descendants of the root element.) Then we filter out only those with +class=bar. That >=> operator is actually the standard operator from +Control.Monad; yet another advantage of the monad instance of lists. +precedingSibling finds all nodes that come before our node and share the +same parent. (There is also a preceding axis which takes all elements earlier +in the tree.) We then take just the h1 elements, and then grab their content.

+ +

While the cursor API isn’t quite as succinct as XPath, it has the advantages of +being standard Haskell code, and of type safety.

+
+
+

xml-hamlet

+

Thanks to the simplicity of Haskell’s data type system, creating XML content +with the Text.XML API is easy, if a bit verbose. The following code:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Data.Map (empty)
+import           Prelude  hiding (writeFile)
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty
+        [ NodeElement $ Element "head" empty
+            [ NodeElement $ Element "title" empty
+                [ NodeContent "My "
+                , NodeElement $ Element "b" empty
+                    [ NodeContent "Title"
+                    ]
+                ]
+            ]
+        , NodeElement $ Element "body" empty
+            [ NodeElement $ Element "p" empty
+                [ NodeContent "foo bar baz"
+                ]
+            ]
+        ]
+

produces

+
<?xml version="1.0" encoding="UTF-8"?>
+<html><head><title>My <b>Title</b></title></head><body><p>foo bar baz</p></body></html>
+

This is leaps and bounds easier than having to deal with an imperative, +mutable-value-based API (cough, Java, cough), but it’s far from pleasant, and +obscures what we’re really trying to achieve. To simplify things, we have the +xml-hamlet package, which using Quasi-Quotation to allow you to type in your +XML in a natural syntax. For example, the above could be rewritten as:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map        (empty)
+import           Prelude         hiding (writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>
+        My #
+        <b>Title
+<body>
+    <p>foo bar baz
+|]
+

Let’s make a few points:

+
    +
  • +

    +The syntax is almost identical to normal Hamlet, except URL-interpolation + (@{…}) has been removed. As such: +

    +
      +
    • +

      +No close tags. +

      +
    • +
    • +

      +Whitespace-sensitive. +

      +
    • +
    • +

      +If you want to have whitespace at the end of a line, use a # at the end. At + the beginning, use a backslash. +

      +
    • +
    +
  • +
  • +

    +An xml interpolation will return a list of Nodes. So you still need to + wrap up the output in all the normal Document and root Element + constructs. +

    +
  • +
  • +

    +There is no support for the special .class and #id attribute forms. +

    +
  • +
+

And like normal Hamlet, you can use variable interpolation and control +structures. So a slightly more complex example would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+import Data.Text (Text, pack)
+import Data.Map (empty)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Michael" 26
+    , Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Gavriella" 1
+    ]
+
+main :: IO ()
+main =
+    writeFile def "people.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>Some People
+<body>
+    <h1>Some People
+    $if null people
+        <p>There are no people.
+    $else
+        <dl>
+            $forall person <- people
+                ^{personNodes person}
+|]
+
+personNodes :: Person -> [Node]
+personNodes person = [xml|
+<dt>#{personName person}
+<dd>#{pack $ show $ personAge person}
+|]
+

A few more notes:

+
    +
  • +

    +The caret-interpolation (^{…}) takes a list of nodes, and so can easily + embed other xml-quotations. +

    +
  • +
  • +

    +Unlike Hamlet, hash-interpolations (#{…}) are not polymorphic, and can + only accept Text values. +

    +
  • +
+
+
+

xml2html

+

So far in this chapter, our examples have revolved around XHTML. I’ve done that +so far simply because it is likely to be the most familiar form of XML for most +of our readers. But there’s an ugly side to all this that we must acknowledge: +not all XHTML will be correct HTML. The following discrepancies exist:

+
    +
  • +

    +There are some void tags (e.g., img, br) in HTML which do not need to + have close tags, and in fact are not allowed to. +

    +
  • +
  • +

    +HTML does not understand self-closing tags, so + <script></script> and <script/> mean very different + things. +

    +
  • +
  • +

    +Combining the previous two points: you are free to self-close void tags, + though to a browser it won’t mean anything. +

    +
  • +
  • +

    +In order to avoid quirks mode, you should start your HTML documents with a + DOCTYPE statement. +

    +
  • +
  • +

    +We do not want the XML declaration <?xml …?> at the top of an HTML + page. +

    +
  • +
  • +

    +We do not want any namespaces used in HTML, while XHTML is fully namespaced. +

    +
  • +
  • +

    +The contents of <style> and <script> tags should not be + escaped. +

    +
  • +
+

Fortunately, xml-conduit provides ToHtml instances for Nodes, +Documents, and Elements which respect these discrepancies. So by just +using toHtml, we can get the correct output.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map                        (empty)
+import           Text.Blaze.Html                 (toHtml)
+import           Text.Blaze.Html.Renderer.String (renderHtml)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = putStr $ renderHtml $ toHtml $ Document (Prologue [] Nothing []) root []
+
+root :: Element
+root = Element "html" empty [xml|
+<head>
+    <title>Test
+    <script>if (5 < 6 || 8 > 9) alert("Hello World!");
+    <style>body > h1 { color: red }
+<body>
+    <h1>Hello World!
+|]
+

Outputs: (whitespace added)

+
<!DOCTYPE HTML>
+<html>
+    <head>
+        <title>Test</title>
+        <script>if (5 < 6 || 8 > 9) alert("Hello World!");</script>
+        <style>body > h1 { color: red }</style>
+    </head>
+    <body>
+        <h1>Hello World!</h1>
+    </body>
+</html>
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/yesod-for-haskellers.html b/public/book-1.6/yesod-for-haskellers.html new file mode 100644 index 00000000..8111761b --- /dev/null +++ b/public/book-1.6/yesod-for-haskellers.html @@ -0,0 +1,1235 @@ + Yesod for Haskellers :: Yesod Web Framework Book- Version 1.6 +
+

Yesod for Haskellers

+ + +

The majority of this book is built around giving practical information on how +to get common tasks done, without drilling too much into the details of what’s +going on under the surface. While the book presumes knowledge of Haskell, it +does not follow the typical style of many Haskell libraries introductions. Many +seasoned Haskellers are put off by this hiding of implementation details. The +purpose of this chapter is to address those concerns.

+

In this chapter, we’ll start off from a bare minimum web application, and +build up to more complicated examples, explaining the components and their +types along the way.

+
+

Hello Warp

+

Let’s start off with the most bare minimum application we can think of:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseLBS
+    status200
+    [("Content-Type", "text/plain")]
+    "Hello Warp!"
+

Wait a minute, there’s no Yesod in there! Don’t worry, we’ll get there. +Remember, we’re building from the ground up, and in Yesod, the ground floor is +WAI, the Web Application Interface. WAI sits between a web handler, such as a +web server or a test framework, and a web application. In our case, the +handler is Warp, a high performance web server, and our application is the +app function.

+

What’s this mysterious Application type? It’s a type synonym defined as:

+
type Application = Request
+                -> (Response -> IO ResponseReceived)
+                -> IO ResponseReceived
+

The Request value contains information such as the requested path, query +string, request headers, request body, and the IP address of the client. The +second argument is the “send response” function. Instead of simply having the +application return an IO Response, WAI uses continuation passing style to +allow for full exception safety, similar to how the bracket function works.

+

We can use this to do some simple dispatching:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, pathInfo, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req sendResponse =
+    case pathInfo req of
+        ["foo", "bar"] -> sendResponse $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested /foo/bar"
+        _ -> sendResponse $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested something else"
+

WAI mandates that the path be split into individual fragments (the stuff +between forward slashes) and converted into text. This allows for easy pattern +matching. If you need the original, unmodified ByteString, you can use +rawPathInfo. For more information on the available fields, please see the WAI +Haddocks.

+

That addresses the request side; what about responses? We’ve already seen +responseLBS, which is a convenient way of creating a response from a lazy +ByteString. That function takes three arguments: the status code, a list of +response headers (as key/value pairs), and the body itself. But responseLBS +is just a convenience wrapper. Under the surface, WAI uses blaze-builder’s +Builder data type to represent the raw bytes. Let’s dig down another level +and use that directly:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (Builder, fromByteString)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseBuilder)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseBuilder
+    status200
+    [("Content-Type", "text/plain")]
+    (fromByteString "Hello from blaze-builder!" :: Builder)
+

This opens up some nice opportunities for efficiently building up response +bodies, since Builder allows for O(1) append operations. We’re also able to +take advantage of blaze-html, which sits on top of blaze-builder. Let’s see our +first HTML application.

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (Application, responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import           Text.Blaze.Html5              (Html, docTypeHtml)
+import qualified Text.Blaze.Html5              as H
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseBuilder
+    status200
+    [("Content-Type", "text/html")] -- yay!
+    (renderHtmlBuilder myPage)
+
+myPage :: Html
+myPage = docTypeHtml $ do
+    H.head $ do
+        H.title "Hello from blaze-html and Warp"
+    H.body $ do
+        H.h1 "Hello from blaze-html and Warp"
+

But there’s a limitation with using a pure Builder value: we need to create +the entire response body before returning the Response value. With lazy +evaluation, that’s not as bad as it sounds, since not all of the body will live in +memory at once. However, if we need to perform some I/O to generate our +response body (such as reading data from a database), we’ll be in trouble.

+

To deal with that situation, WAI provides a means for generating streaming +response bodies. It also allows explicit control of flushing the stream. Let’s +see how this works.

+
{-# LANGUAGE OverloadedStrings #-}
+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)
+import           Network.Wai.Handler.Warp           (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseStream
+    status200
+    [("Content-Type", "text/plain")]
+    myStream
+
+myStream :: (Builder -> IO ()) -> IO () -> IO ()
+myStream send flush = do
+    send $ fromByteString "Starting streaming response.\n"
+    send $ fromByteString "Performing some I/O.\n"
+    flush
+    -- pretend we're performing some I/O
+    threadDelay 1000000
+    send $ fromByteString "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        send $ fromByteString "Got the value: " <>
+               fromShow i <>
+               fromByteString "\n"
+ +

Another common requirement when dealing with a streaming response is safely +allocating a scarce resource- such as a file handle. By safely, I mean +ensuring that the resource will be released, even in the case of some +exception. This is where the continuation passing style mentioned above comes +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)
+import           System.IO                (IOMode (ReadMode), withFile)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = withFile "index.html" ReadMode $ \handle ->
+    sendResponse $ responseStream
+        status200
+        [("Content-Type", "text/html")]
+        $ \send _flush ->
+            let loop = do
+                    bs <- S.hGet handle 4096
+                    if S.null bs
+                        then return ()
+                        else send (fromByteString bs) >> loop
+             in loop
+

Notice how we’re able to take advantage of existing exception safe functions +like withFile to deal with exceptions properly.

+

But in the case of serving files, it’s more efficient to use responseFile, +which can use the sendfile system call to avoid unnecessary buffer copies:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseFile)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseFile
+    status200
+    [("Content-Type", "text/html")]
+    "index.html"
+    Nothing -- means "serve whole file"
+            -- you can also serve specific ranges in the file
+

There are many aspects of WAI we haven’t covered here. One important topic is WAI middlewares. We also haven’t inspected request bodies at all. But for the purposes of understanding Yesod, we’ve covered enough for the moment.

+
+
+

What about Yesod?

+

In all our excitement about WAI and Warp, we still haven’t seen anything about Yesod! Since we just learnt all about WAI, our first question should be: how does Yesod interact with WAI. The answer to that is one very important function:

+
toWaiApp :: YesodDispatch site => site -> IO Application
+ +

This function takes some site value, which must be an instance of +YesodDispatch, and creates an Application. This function lives in the IO +monad, since it will likely perform actions like allocating a shared logging +buffer. The more interesting question is what this site value is all about.

+

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. +

    +
  • +
  • +

    +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 +Yesod application the hard way, by writing everything manually. We’ll +progressively add more convenience helpers on top as we go along.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import qualified Text.Blaze.Html5              as H
+import           Yesod.Core                    (Html, RenderRoute (..), Yesod,
+                                                YesodDispatch (..), toWaiApp)
+import           Yesod.Core.Types              (YesodRunnerEnv (..))
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+instance YesodDispatch App where
+    yesodDispatch (YesodRunnerEnv _logger site _sessionBackend _ _) _req sendResponse =
+        sendResponse $ responseBuilder
+            status200
+            [("Content-Type", "text/html")]
+            (renderHtmlBuilder $ welcomeMessage site)
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

OK, we’ve added quite a few new pieces here, let’s attack them one at a time. +The first thing we’ve done is created a new datatype, App. This is commonly +used as the foundation data type name for each application, though you’re free +to use whatever name you want. We’ve added one field to this datatype, +welcomeMessage, which will hold the content for our homepage.

+

Next we declare our Yesod instance. We just use the default values for all of +the methods for this example. More interesting is the RenderRoute typeclass. +This is the heart of type-safe URLs. We create an associated data type for +App which lists all of our app’s accepted routes. In this case, we have just +one: the homepage, which we call HomeR. It’s yet another Yesod naming +convention to append R to all of the route data constructors.

+

We also need to create a renderRoute method, which converts each type-safe +route value into a tuple of path pieces and query string parameters. We’ll get +to more interesting examples later, but for now, our homepage has an empty list +for both of those.

+

YesodDispatch determines how our application behaves. It has one method, +yesodDispatch, of type:

+
yesodDispatch :: YesodRunnerEnv site -> Application
+

YesodRunnerEnv provides three 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.

+

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 +our implementation of yesodDispatch is able to use everything we learned +about WAI above. Notice also how we accessed the welcomeMessage from our +foundation data type.

+

Finally, we have the main function. The App value is easy to create and, as +you can see, you could just as easily have performed some I/O to acquire the +welcome message. We use toWaiApp to obtain a WAI application, and then pass +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

+

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.

+

There are many common things you’d want to do when handling a single request, +e.g.:

+
    +
  • +

    +Return some HTML. +

    +
  • +
  • +

    +Redirect to a different URL. +

    +
  • +
  • +

    +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:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.Wai              (pathInfo)
+import           Network.Wai.Handler.Warp (run)
+import qualified Text.Blaze.Html5         as H
+import           Yesod.Core               (HandlerT, Html, RenderRoute (..),
+                                           Yesod, YesodDispatch (..), getYesod,
+                                           notFound, toWaiApp, yesodRunner)
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO Html
+getHomeR = do
+    site <- getYesod
+    return $ welcomeMessage site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

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 +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.

+

Our function body is short. We use the getYesod function to get the +foundation data type value, and then return the welcomeMessage field. We’ll +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 Applications. Let’s look at its type signature:

+
yesodRunner :: (ToTypedContent res, Yesod site)
+            => HandlerT site IO res
+            -> YesodRunnerEnv site
+            -> Maybe (Route site)
+            -> Application
+

We’re already familiar with YesodRunnerEnv from our previous example. As you +can see in our call to yesodRunner above, we pass that value in unchanged. +The Maybe (Route site) is a bit interesting, and gives us more insight into +how type-safe URLs work. Until now, we only saw the rendering side of these +URLs. But just as important is the parsing side: converting a requested path +into a route value. In our example, this code is just a few lines, and we store +the result in maybeRoute.

+ +

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 +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 +template. That template can be overridden in the Yesod typeclass; out of the +box, it’s just a boring HTML page.

+

This almost all makes sense, except for one issue: what’s that ToTypedContent +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 +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, +and sending the response body.

+
+

(To)Content, (To)TypedContent

+

At the very core of Yesod’s content system are the following types:

+
data Content = ContentBuilder !Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+
+type ContentType = ByteString
+data TypedContent = TypedContent !ContentType !Content
+

Content should remind you a bit of the WAI response types. ContentBuilder +is similar to responseBuilder, ContentSource is like responseStream but specialized to conduit, and +ContentFile is like responseFile. Unlike their WAI counterparts, none of +these constructors contain information on the status code or response headers; +that’s handled orthogonally in Yesod.

+

The one completely new constructor is ContentDontEvaluate. By default, when +you create a response body in Yesod, Yesod fully evaluates the body before +generating the response. The reason for this is to ensure that there are no +impure exceptions in your value. Yesod wants to make sure to catch any such +exceptions before starting to send your response so that, if there is an +exception, Yesod can generate a proper 500 internal server error response +instead of simply dying in the middle of sending a non-error response. However, +performing this evaluation can cause more memory usage. Therefore, Yesod +provides a means of opting out of this protection.

+

TypedContent is then a minor addition to Content: it includes the +ContentType as well. Together with a convention that an application returns a +200 OK status unless otherwise specified, we have everything we need from the +TypedContent type to create a response.

+

Yesod could have taken the approach of requiring users to always return +TypedContent from a handler function, but that would have required manually +converting to that type. Instead, Yesod uses a pair of typeclasses for this, +appropriately named ToContent and ToTypedContent. They have exactly the +definitions you’d expect:

+
class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

And Yesod provides instances for many common data types, including Text, +Html, and aeson’s Value type (containing JSON data). That’s how the +getHomeR function was able to return Html: Yesod knows how to convert it to +TypedContent, and from there it can be converted into a WAI response.

+
+
+

HasContentType and representations

+

This typeclass approach allows for one other nice abstraction. For many types, the type system itself lets us know what the content-type for the content should be. For example, Html will always be served with a text/html content-type.

+ +

Some requests to a web application can be displayed with various representation. For example, a request for tabular data could be served with:

+
    +
  • +

    +An HTML table +

    +
  • +
  • +

    +A CSV file +

    +
  • +
  • +

    +XML +

    +
  • +
  • +

    +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 +selectRep/provideRep function combo to provide multiple representations, +and have the framework automatically choose the appropriate one based on the +client headers.

+

The last missing piece to make this all work is the HasContentType typeclass:

+
class ToTypedContent a => HasContentType a where
+    getContentType :: Monad m => m a -> ContentType
+

The parameter m a is just a poor man’s Proxy type. And, in hindsight, we +should have used Proxy, but that would now be a breaking change. There are +instances for this typeclass for most data types supported by ToTypedContent. +Below is our example from above, tweaked just a bit to provide multiple +representations of the data:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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 (..),
+                                           TypedContent, Value, Yesod,
+                                           YesodDispatch (..), getYesod,
+                                           notFound, object, provideRep,
+                                           selectRep, toWaiApp, yesodRunner,
+                                           (.=))
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessageHtml :: !Html
+    , welcomeMessageText :: !Text
+    , welcomeMessageJson :: !Value
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = do
+    site <- getYesod
+    selectRep $ do
+        provideRep $ return $ welcomeMessageHtml site
+        provideRep $ return $ welcomeMessageText site
+        provideRep $ return $ welcomeMessageJson site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+main = do
+    waiApp <- toWaiApp App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+    run 3000 waiApp
+
+
+

Convenience warp function

+

And one minor convenience you’ll see quite a bit in the Yesod world. It’s very +common to call toWaiApp to create a WAI Application, and then pass that to +Warp’s run function. So Yesod provides a convenience warp wrapper function. +We can replace our previous main function with the following:

+
main :: IO ()
+main =
+    warp 3000 App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+

There’s also a warpEnv function which reads the port number from the PORT +environment variable, which is useful for working with platforms such as FP +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.

+

In addition to that standard functionality, the following are some common +categories of functions. The only requirement Yesod places on your handler +functions is that, ultimately, they return a type which is an instance of +ToTypedContent.

+

This section is just a short overview of functionality. For more information, +you should either look through the Haddocks, or read the rest of this book.

+
+

Getting request parameters

+

There are a few pieces of information provided by the client in a request:

+
    +
  • +

    +The requested path. This is usually handled by Yesod’s routing framework, and is not directly queried in a handler function. +

    +
  • +
  • +

    +Query string parameters. This can be queried using lookupGetParam. +

    +
  • +
  • +

    +Request bodies. In the case of URL encoded and multipart bodies, you can use lookupPostParam to get the request parameter. For multipart bodies, there’s also lookupFile for file parameters. +

    +
  • +
  • +

    +Request headers can be queried via lookupHeader. (And response headers can be set with addHeader.) +

    +
  • +
  • +

    +Yesod parses cookies for you automatically, and they can be queried using lookupCookie. (Cookies can be set via the setCookie function.) +

    +
  • +
  • +

    +Finally, Yesod provides a user session framework, where data can be set in a cryptographically secure session and associated with each user. This can be queried and set using the functions lookupSession, setSession and deleteSession. +

    +
  • +
+

While you can use these functions directly for such purposes as processing +forms, you usually will want to use the yesod-form library, which provides a +higher level form abstraction based on applicative functors.

+
+
+

Short circuiting

+

In some cases, you’ll want to short circuit the handling of a request. Reasons +for doing this would be:

+
    +
  • +

    +Send an HTTP redirect, via the redirect function. This will default to using the 303 status code. You can use redirectWith to get more control over this. +

    +
  • +
  • +

    +Return a 404 not found with notFound, or a 405 bad method via badMethod. +

    +
  • +
  • +

    +Indicate some error in the request via notAuthenticated, permissionDenied, or invalidArgs. +

    +
  • +
  • +

    +Send a special response, such as with sendFile or sendResponseStatus (to override the status 200 response code) +

    +
  • +
  • +

    +sendWaiResponse to drop down a level of abstraction, bypass some Yesod abstractions, and use WAI itself. +

    +
  • +
+
+
+

Streaming

+

So far, the examples of ToTypedContent instances I gave all involved +non-streaming responses. Html, Text, and Value all get converted into a +ContentBuilder constructor. As such, they cannot interleave I/O with sending +data to the user. What happens if we want to perform such interleaving?

+

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 +Builder. Yesod also provides a number of convenience functions for creating +that Source, such as sendChunk, sendChunkBS, and sendChunkText.

+

Here’s an example, which just converts our initial responseSource example +from WAI to Yesod.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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,
+                                                     YesodDispatch (..), liftIO,
+                                                     notFound, respondSource,
+                                                     sendChunk, sendChunkBS,
+                                                     sendChunkText, sendFlush,
+                                                     warp, yesodRunner)
+
+-- | Our foundation datatype.
+data App = App
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = respondSource "text/plain" $ do
+    sendChunkBS "Starting streaming response.\n"
+    sendChunkText "Performing some I/O.\n"
+    sendFlush
+    -- pretend we're performing some I/O
+    liftIO $ threadDelay 1000000
+    sendChunkBS "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        sendChunk $ fromByteString "Got the value: " <>
+                    fromShow i <>
+                    fromByteString "\n"
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+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, +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 +Fibonacci number. And if you visit /, it will automatically redirect you to +/fib/1.

+

In the Yesod world, the first question to ask is: how do we model our route +data type? This is pretty straight-forward: data Route App = HomeR | FibR +Int. The question is: how do we want to define our RenderRoute instance? We +need to convert the Int to a Text. What function should we use?

+

Before you answer that, realize that we’ll also need to be able to parse back a Text into an Int for dispatch purposes. So we need to make sure that we have a pair of functions with the property fromText . toText == Just. Show/Read could be a candidate for this, except that:

+
    +
  1. +

    +We’d be required to convert through String. +

    +
  2. +
  3. +

    +The Show/Read instances for Text and String both involve extra escaping, which we don’t want to incur. +

    +
  4. +
+

Instead, the approach taken by Yesod is the path-pieces package, and in +particular the PathPiece typeclass, defined as:

+
class PathPiece s where
+    fromPathPiece :: Text -> Maybe s
+    toPathPiece   :: s    -> Text
+

Using this typeclass, we can write parse and render functions for our route datatype:

+
instance RenderRoute App where
+    data Route App = HomeR | FibR Int
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ([], [])
+    renderRoute (FibR i) = (["fib", toPathPiece i], [])
+
+parseRoute' [] = Just HomeR
+parseRoute' ["fib", i] = FibR <$> fromPathPiece i
+parseRoute' _ = Nothing
+

And then we can write our YesodDispatch typeclass instance:

+
instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute = parseRoute' (pathInfo req)
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+                    Just (FibR i) -> getFibR i
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR i = return $ show $ fibs !! i
+

Notice our call to redirect in getHomeR. We’re able to use the route +datatype as the parameter to redirect, and Yesod takes advantage of our +renderRoute function to create a textual link.

+
+
+

Routing with Template Haskell

+

Now let’s suppose we want to add a new route to our previous application. We’d +have to make the following changes:

+
    +
  1. +

    +Modify the Route datatype itself. +

    +
  2. +
  3. +

    +Add a clause to renderRoute. +

    +
  4. +
  5. +

    +Add a clause to parseRoute', and make sure it corresponds correctly to renderRoute. +

    +
  6. +
  7. +

    +Add a clause to the case statement in yesodDispatch to call our handler function. +

    +
  8. +
  9. +

    +Write our handler function. +

    +
  10. +
+

That’s a lot of changes! And lots of manual, boilerplate changes means lots of +potential for mistakes. Some of the mistakes can be caught by the compiler if +you turn on warnings (forgetting to add a clause in renderRoute or a match in +yesodDispatch's case statement), but others cannot (ensuring that +renderRoute and parseRoute have the same logic, or adding the parseRoute +clause).

+

This is where Template Haskell comes into the Yesod world. Instead of dealing +with all of these changes manually, Yesod declares a high level routing syntax. +This syntax lets you specify your route syntax, dynamic parameters, constructor +names, and accepted request methods, and automatically generates parse, render, +and dispatch functions.

+

To get an idea of how much manual coding this saves, have a look at our +previous example converted to the Template Haskell version:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Yesod.Core (RenderRoute (..), Yesod, mkYesod, parseRoutes,
+                             redirect, warp)
+
+-- | Our foundation datatype.
+data App = App
+
+mkYesod "App" [parseRoutes|
+/         HomeR GET
+/fib/#Int FibR  GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler ()
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> Handler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 App
+

What’s wonderful about this is, as the developer, you can now focus on the +important part of your application, and not get involved in the details of +writing parsers and renderers. There are of course some downsides to the usage +of Template Haskell:

+
    +
  • +

    +Compile times are a bit slower. +

    +
  • +
  • +

    +The details of what’s going on behind the scenes aren’t easily apparent. (Though you can use cabal haddock to see what identifiers have been generated for you.) +

    +
  • +
  • +

    +You don’t have as much fine-grained control. For example, in the Yesod route syntax, each dynamic parameter has to be a separate field in the route constructor, as opposed to bundling fields together. This is a conscious trade-off in Yesod between flexibility and complexity. +

    +
  • +
+

This usage of Template Haskell is likely the most controversial decision in +Yesod. I personally think the benefits definitely justify its usage. But if +you’d rather avoid Template Haskell, you’re free to do so. Every example so far +in this chapter has done so, and you can follow those techniques. We also have +another, simpler approach in the Yesod world: LiteApp.

+
+

LiteApp

+

LiteApp allows you to throw away type safe URLs and Template Haskell. It uses +a simple routing DSL in pure Haskell. Once again, as a simple comparison, let’s +rewrite our Fibonacci example to use it.

+
import           Data.Text  (pack)
+import           Yesod.Core (LiteHandler, dispatchTo, dispatchTo, liteApp,
+                             onStatic, redirect, warp, withDynamic)
+
+getHomeR :: LiteHandler ()
+getHomeR = redirect "/fib/1"
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> LiteHandler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "fib") $ withDynamic $ \i -> dispatchTo (getFibR i)
+

There you go, a simple Yesod app without any language extensions at all! +However, even this application still demonstrates some type safety. Yesod will +use fromPathPiece to convert the parameter for getFibR from Text to an +Int, so any invalid parameter will be got by Yesod itself. It’s just one less +piece of checking that you have to perform.

+
+
+
+

Shakespeare

+

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.

+

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.

+
import           Data.Text  (pack)
+import           Yesod.Core
+
+getHomeR :: LiteHandler TypedContent
+getHomeR = return $ TypedContent typeHtml $ toContent
+    "<html><head><title>Hi There!</title>\
+    \<link rel='stylesheet' href='/style.css'>\
+    \<script src='/script.js'></script></head>\
+    \<body><h1>Hello World!</h1></body></html>"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

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 +content as Strings, convert them to Content using toContent, then wrap +them with a TypedContent constructor to give them the appropriate +content-type headers.

+

But as usual, we can do better. Dealing with Strings is not very efficient, +and it’s tedious to have to manually put in the content type all the time. But +we already know the solution to those problems: use the Html datatype from +blaze-html. Let’s convert our getHomeR function to use it:

+
import           Data.Text                   (pack)
+import           Text.Blaze.Html5            (toValue, (!))
+import qualified Text.Blaze.Html5            as H
+import qualified Text.Blaze.Html5.Attributes as A
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = return $ H.docTypeHtml $ do
+    H.head $ do
+        H.title $ toHtml "Hi There!"
+        H.link ! A.rel (toValue "stylesheet") ! A.href (toValue "/style.css")
+        H.script ! A.src (toValue "/script.js") $ return ()
+    H.body $ do
+        H.h1 $ toHtml "Hello World!"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

Ahh, far nicer. blaze-html provides a convenient combinator library, and will +execute far faster in most cases than whatever String concatenation you might +attempt.

+

If you’re happy with blaze-html combinators, by all means use them. However, +many people like to use a more specialized templating language. Yesod’s +standard provider for this is the Shakespearean languages: Hamlet, Lucius, and +Julius. You are by all means welcome to use a different system if so desired, +the only requirement is that you can get a Content value from the template.

+

Since Shakespearean templates are compile-time checked, their usage requires +either quasiquotation or Template Haskell. We’ll go for the former approach +here. Please see the Shakespeare chapter in the book for more information.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Data.Text   (Text, pack)
+import           Text.Julius (Javascript)
+import           Text.Lucius (Css)
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = withUrlRenderer $
+    [hamlet|
+        $doctype 5
+        <html>
+            <head>
+                <title>Hi There!
+                <link rel=stylesheet href=/style.css>
+                <script src=/script.js>
+            <body>
+                <h1>Hello World!
+    |]
+
+getStyleR :: LiteHandler Css
+getStyleR = withUrlRenderer [lucius|h1 { color: red }|]
+
+getScriptR :: LiteHandler Javascript
+getScriptR = withUrlRenderer [julius|alert('Yay, Javascript works too!');|]
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+
+

URL rendering function

+

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 +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.

+

The recommended approach instead would be to use our type-safe URL datatype in +our template instead of including explicit strings. As mentioned above, +LiteApp doesn’t provide any meaningful type-safe URLs, so we don’t have that +option here. But if you use the Template Haskell generators, you get type-safe +URLs for free.

+

In any event, the Shakespearean templates all expect to receive a function to +handle the rendering of a type-safe URL. Since we don’t actually use any +type-safe URLs, just about any function would work here (the function will be +ignored entirely), but withUrlRenderer is a convenient way of doing this.

+

As we’ll see next, withUrlRenderer isn’t really needed most of the time, +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 +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 +using them.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = defaultLayout $ do
+    setTitle $ toHtml "Hi There!"
+    [whamlet|<h1>Hello World!|]
+    toWidget [lucius|h1 { color: red }|]
+    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 +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 +temporary static files and linking to them).

+

Widgets also have another advantage. The defaultLayout function is a member +of the Yesod typeclass, and can be modified to provide a customized +look-and-feel for your website. Many built-in pieces of Yesod, such as error +messages, take advantage of the widget system, so by using widgets, you get a +consistent feel throughout your site.

+
+
+

Details we won’t cover

+

Hopefully this chapter has pulled back enough of the “magic” in Yesod to let +you understand what’s going on under the surface. We could of course continue +using this approach for analyze the rest of the Yesod ecosystem, but that would +be mostly redundant with the rest of this book. Hopefully you can now feel more +informed as you read chapters on Persistent, forms, sessions, and subsites.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/yesod-typeclass.html b/public/book-1.6/yesod-typeclass.html new file mode 100644 index 00000000..65daeea9 --- /dev/null +++ b/public/book-1.6/yesod-typeclass.html @@ -0,0 +1,676 @@ + Yesod Typeclass :: Yesod Web Framework Book- Version 1.6 +
+

Yesod Typeclass

+ + +

Every one of our Yesod applications requires an instance of the Yesod +typeclass. So far, we’ve just relied on default implementations of these +methods. In this chapter, we’ll explore the meaning of many of the methods of +the Yesod typeclass.

+

The Yesod typeclass gives us a central place for defining settings for our +application. Everything has a default definition which is often the +right thing. But in order to build a powerful, customized application, you’ll +usually end up wanting to override at least a few of these methods.

+ +
+

Rendering and Parsing URLs

+

We’ve already mentioned how Yesod is able to automatically render type-safe +URLs into a textual URL that can be inserted into an HTML page. Let’s say we +have a route definition that looks like:

+
mkYesod "MyApp" [parseRoutes|
+/some/path SomePathR GET
+]
+

If we place SomePathR into a hamlet template, how does Yesod render it? Yesod +always tries to construct absolute URLs. This is especially useful once we +start creating XML sitemaps and Atom feeds, or sending emails. But in order to +construct an absolute URL, we need to know the domain name of the application.

+

You might think we could get that information from the user’s request, but we +still need to deal with ports. And even if we get the port number from the +request, are we using HTTP or HTTPS? And even if you know that, such an +approach would mean that, depending on how the user submitted a request would +generate different URLs. For example, we would generate different URLs +depending if the user connected to "example.com" or "www.example.com". For +Search Engine Optimization, we want to be able to consolidate on a single +canonical URL.

+

And finally, Yesod doesn’t make any assumption about where you host your +application. For example, I may have a mostly static site +(http://static.example.com/), but I’d like to stick a Yesod-powered Wiki at +/wiki/. There is no reliable way for an application to determine what subpath +it is being hosted from. So instead of doing all of this guesswork, Yesod needs +you to tell it the application root.

+

Using the wiki example, you would write your Yesod instance as:

+
instance Yesod MyWiki where
+    approot = ApprootStatic "http://static.example.com/wiki"
+

Notice that there is no trailing slash there. Next, when Yesod wants to +construct a URL for SomePathR, it determines that the relative path for +SomePathR is /some/path, appends that to your approot and creates +http://static.example.com/wiki/some/path.

+

The default value of approot is guessApproot. +This works fine for the common case of a link within your +application, and your application being hosted at the root of your domain. But +if you have any use cases which demand absolute URLs (such as sending an +email), it’s best to use ApprootStatic.

+

In addition to the ApprootStatic constructor demonstrated above, you can also +use the ApprootMaster and ApprootRequest constructors. The former allows +you to determine the approot from the foundation value, which would let you +load up the approot from a config file, for instance. The latter allows you to +additionally use the request value to determine the approot; using this, you +could for example provide a different domain name depending on how the user +requested the site in the first place.

+

The scaffolded site uses ApprootMaster by default, and pulls your approot +from either the APPROOT environment variable or a config file on launch. +Additionally, it loads different settings for testing and +production builds, so you can easily test on one domain- like localhost- and +serve from a different domain. You can modify these values from the config +file.

+
+

joinPath

+

In order to convert a type-safe URL into a text value, Yesod uses two helper +functions. The first is the renderRoute method of the RenderRoute +typeclass. Every type-safe URL is an instance of this typeclass. renderRoute +converts a value into a list of path pieces. For example, our SomePathR from +above would be converted into ["some", "path"].

+ +

The other function is the joinPath method of the Yesod typeclass. This function takes four arguments:

+
    +
  • +

    +The foundation value +

    +
  • +
  • +

    +The application root +

    +
  • +
  • +

    +A list of path segments +

    +
  • +
  • +

    +A list of query string parameters +

    +
  • +
+

It returns a textual URL. The default implementation does the “right thing”: +it separates the path pieces by forward slashes, prepends the application root, +and appends the query string.

+

If you are happy with default URL rendering, you should not need to modify it. +However, if you want to modify URL rendering to do things like append a +trailing slash, this would be the place to do it.

+
+
+

cleanPath

+

The flip side of joinPath is cleanPath. Let’s look at how it gets used in +the dispatch process:

+
    +
  1. +

    +The path info requested by the user is split into a series of path pieces. +

    +
  2. +
  3. +

    +We pass the path pieces to the cleanPath function. +

    +
  4. +
  5. +

    +If cleanPath indicates a redirect (a Left response), then a 301 response +is sent to the client. This is used to force canonical URLs (eg, remove extra +slashes). +

    +
  6. +
  7. +

    +Otherwise, we try to dispatch using the response from cleanPath (a +Right). If this works, we return a response. Otherwise, we return a 404. +

    +
  8. +
+

This combination allows subsites to retain full control of how their URLs +appear, yet allows master sites to have modified URLs. As a simple example, +let’s see how we could modify Yesod to always produce trailing slashes on URLs:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# 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)
+import           Yesod
+
+data Slash = Slash
+
+mkYesod "Slash" [parseRoutes|
+/ RootR GET
+/foo FooR GET
+|]
+
+instance Yesod Slash where
+    joinPath _ ar pieces' qs' =
+        fromText ar `mappend` encodePath pieces qs
+      where
+        qs = map (TE.encodeUtf8 *** go) qs'
+        go "" = Nothing
+        go x = Just $ TE.encodeUtf8 x
+        pieces = pieces' ++ [""]
+
+    -- We want to keep canonical URLs. Therefore, if the URL is missing a
+    -- trailing slash, redirect. But the empty set of pieces always stays the
+    -- same.
+    cleanPath _ [] = Right []
+    cleanPath _ s
+        | dropWhile (not . T.null) s == [""] = -- the only empty string is the last one
+            Right $ init s
+        -- Since joinPath will append the missing trailing slash, we simply
+        -- remove empty pieces.
+        | otherwise = Left $ filter (not . T.null) s
+
+getRootR :: Handler Html
+getRootR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{RootR}>RootR
+        <p>
+            <a href=@{FooR}>FooR
+    |]
+
+getFooR :: Handler Html
+getFooR = getRootR
+
+main :: IO ()
+main = warp 3000 Slash
+

First, let’s look at our joinPath implementation. This is copied almost +verbatim from the default Yesod implementation, with one difference: we append +an extra empty string to the end. When dealing with path pieces, an empty +string will append another slash. So adding an extra empty string will force a +trailing slash.

+

cleanPath is a little bit trickier. First, we check for the empty path like +before, and if so pass it through as-is. We use Right to indicate that a +redirect is not necessary. The next clause is actually checking for two +different possible URL issues:

+
    +
  • +

    +There is a double slash, which would show up as an empty string in the middle + of our paths. +

    +
  • +
  • +

    +There is a missing trailing slash, which would show up as the last piece not + being an empty string. +

    +
  • +
+

Assuming neither of those conditions hold, then only the last piece is empty, +and we should dispatch based on all but the last piece. However, if this is not +the case, we want to redirect to a canonical URL. In this case, we strip out +all empty pieces and do not bother appending a trailing slash, since joinPath +will do that for us.

+
+
+
+

defaultLayout

+

Most websites like to apply some general template to all of their pages. +defaultLayout is the recommended approach for this. While you could just as +easily define your own function and call that instead, when you override +defaultLayout all of the Yesod-generated pages (error pages, authentication +pages) automatically get this style.

+

Overriding is very straight-forward: we use widgetToPageContent to convert a +Widget to a title, head tags and body tags, and then use withUrlRenderer to +convert a Hamlet template into an Html value. We can even add extra widget +components, like a Lucius template, from within defaultLayout. For more +information, see the previous chapter on widgets.

+

If you are using the scaffolded site, you can modify the files +templates/default-layout.hamlet and +templates/default-layout-wrapper.hamlet. The former contains most of the +contents of the <body> tag, while the latter has the rest of the HTML, such +as doctype and <head> tag. See those files for more details.

+
+

getMessage

+

Even though we haven’t covered sessions yet, I’d like to mention getMessage +here. A common pattern in web development is setting a message in one handler +and displaying it in another. For example, if a user POSTs a form, you may +want to redirect him/her to another page along with a “Form submission +complete” message. This is commonly known as +Post/Redirect/Get.

+

To facilitate this, Yesod comes built in with a pair of functions: setMessage +sets a message in the user session, and getMessage retrieves the message (and +clears it, so it doesn’t appear a second time). It’s recommended that you put +the result of getMessage into your defaultLayout. For example:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+import Data.Time (getCurrentTime)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    defaultLayout contents = do
+        PageContent title headTags bodyTags <- widgetToPageContent contents
+        mmsg <- getMessage
+        withUrlRenderer [hamlet|
+            $doctype 5
+
+            <html>
+                <head>
+                    <title>#{title}
+                    ^{headTags}
+                <body>
+                    $maybe msg <- mmsg
+                        <div #message>#{msg}
+                    ^{bodyTags}
+        |]
+
+getHomeR :: Handler Html
+getHomeR = do
+    now <- liftIO getCurrentTime
+    setMessage $ toHtml $ "You previously visited at: " ++ show now
+    defaultLayout [whamlet|<p>Try refreshing|]
+
+main :: IO ()
+main = warp 3000 App
+

We’ll cover getMessage/setMessage in more detail when we discuss sessions.

+
+
+
+

Custom error pages

+

One of the marks of a professional web site is a properly designed error page. +Yesod gets you a long way there by automatically using your defaultLayout for +displaying error pages. But sometimes, you’ll want to go even further. For +this, you’ll want to override the errorHandler method:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/error ErrorR GET
+/not-found NotFoundR GET
+|]
+
+instance Yesod App where
+    errorHandler NotFound = fmap toTypedContent $ defaultLayout $ do
+        setTitle "Request page not located"
+        toWidget [hamlet|
+<h1>Not Found
+<p>We apologize for the inconvenience, but the requested page could not be located.
+|]
+    errorHandler other = defaultErrorHandler other
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{ErrorR}>Internal server error
+            <a href=@{NotFoundR}>Not found
+    |]
+
+getErrorR :: Handler ()
+getErrorR = error "This is an error"
+
+getNotFoundR :: Handler ()
+getNotFoundR = notFound
+
+main :: IO ()
+main = warp 3000 App
+

Here we specify a custom 404 error page. We can also use the +defaultErrorHandler when we don’t want to write a custom handler for each +error type. Due to type constraints, we need to start off our methods with +fmap toTypedContent, but otherwise you can write a typical handler function. +(We’ll learn more about TypedContent in the next chapter.)

+

In fact, you could even use special responses like redirects:

+
    errorHandler NotFound = redirect HomeR
+    errorHandler other = defaultErrorHandler other
+ +
+
+

External CSS and Javascript

+ +

One of the most powerful, and most intimidating, methods in the Yesod typeclass +is addStaticContent. Remember that a Widget consists of multiple components, +including CSS and Javascript. How exactly does that CSS/JS arrive in the user’s +browser? By default, they are served in the <head> of the page, inside +<style> and <script> tags, respectively.

+

That might be simple, but it’s far from efficient. Every page load will now +require loading up the CSS/JS from scratch, even if nothing changed! What we +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 +(text/css or text/javascript) and the content itself. It will then return +one of three possible results:

+
+
+Nothing +
+

+No static file saving occurred; embed this content directly in the +HTML. This is the default behavior. +

+
+
+Just (Left Text) +
+

+This content was saved in an external file, and use the +given textual link to refer to it. +

+
+
+Just (Right (Route a, Query)) +
+

+Same, but now use a type-safe URL along with +some query string parameters. +

+
+
+

The Left result is useful if you want to store your static files on an +external server, such as a CDN or memory-backed server. The Right result is +more commonly used, and ties in very well with the static subsite. This is the +recommended approach for most applications, and is provided by the scaffolded +site by default.

+ +

The scaffolded addStaticContent does a number of intelligent things to help +you out:

+
    +
  • +

    +It automatically minifies your Javascript using the hjsmin package. +

    +
  • +
  • +

    +It names the output files based on a hash of the file contents. This means + you can set your cache headers to far in the future without fears of stale + content. +

    +
  • +
  • +

    +Also, since filenames are based on hashes, you can be guaranteed that a file + doesn’t need to be written if a file with the same name already exists. The + scaffold code automatically checks for the existence of that file, and avoids + the costly disk I/O of a write if it’s not necessary. +

    +
  • +
+
+
+

Smarter Static Files

+

Google recommends an important optimization: +serve +static files from a separate domain. The advantage to this approach is that +cookies set on your main domain are not sent when retrieving static files, thus +saving on a bit of bandwidth.

+

To facilitate this, we have the urlParamRenderOverride method. +This method intercepts the normal URL rendering and query string parameters. +Then, it sets a special value for some routes. +For example, the scaffolding defines this method as:

+
urlParamRenderOverride :: site
+                       -> Route site
+                       -> [(T.Text, T.Text)] -- ^ query string
+                       -> Maybe Builder
+urlParamRenderOverride y (StaticR s) _ =
+    Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s
+urlParamRenderOverride _ _ _ = Nothing
+

This means that static routes are served from a special static root, which you +can configure to be a different domain. This is a great example of the power +and flexibility of type-safe URLs: with a single line of code you’re able to +change the rendering of static routes throughout all of your handlers.

+
+
+

Authentication/Authorization

+

For simple applications, checking permissions inside each handler function can +be a simple, convenient approach. However, it doesn’t scale well. Eventually, +you’re going to want to have a more declarative approach. Many systems out +there define ACLs, special config files, and a lot of other hocus-pocus. In +Yesod, it’s just plain old Haskell. There are three methods involved:

+
+
+isWriteRequest +
+

+Determine if the current request is a "read" or "write" operations. By default, Yesod follows RESTful principles, and assumes GET, HEAD, OPTIONS, and TRACE requests are read-only, while all others are writable. +

+
+
+isAuthorized +
+

+Takes a route (i.e., type-safe URL) and a boolean indicating whether or not the request is a write request. It returns an AuthResult, which can have one of three values: +

+
    +
  • +

    +Authorized +

    +
  • +
  • +

    +AuthenticationRequired +

    +
  • +
  • +

    +Unauthorized +

    +
  • +
+
+
+

By default, it returns Authorized for all requests.

+
+
+authRoute +
+

+If isAuthorized returns AuthenticationRequired, then redirect +to the given route. If no route is provided (the default), return a 401 +“authentication required” message. +

+
+
+

These methods tie in nicely with the yesod-auth package, which is used by the +scaffolded site to provide a number of authentication options, such as OpenID, +Mozilla Persona, email, username and Twitter. We’ll cover more concrete +examples in the auth chapter.

+
+
+

Some Simple Settings

+

Not everything in the Yesod typeclass is complicated. Some methods are simple +functions. Let’s just go through the list:

+
+
+maximumContentLength +
+

+To prevent Denial of Service (DoS) attacks, Yesod will +limit the size of request bodies. Some of the time, you’ll want to bump that +limit for some routes (e.g., a file upload page). This is where you’d do that. +

+
+
+fileUpload +
+

+Determines how uploaded files are treated, based on the size of +the request. The two most common approaches are saving the files in memory, or +streaming to temporary files. By default, small requests are kept in memory and +large ones are stored to disk. +

+
+
+shouldLogIO +
+

+Determines if a given log message (with associated source and +level) should be sent to the log. This allows you to put lots of debugging +information into your app, but only turn it on as necessary. +

+
+
+

For the most up-to-date information, please see the Haddock API documentation +for the Yesod typeclass.

+
+
+

Summary

+

The Yesod typeclass has a number of overrideable methods that allow you to +configure your application. They are all optional, and provide sensible +defaults. By using built-in Yesod constructs like defaultLayout and +getMessage, you’ll get a consistent look-and-feel throughout your site, +including pages automatically generated by Yesod such as error pages and +authentication.

+

We haven’t covered all the methods in the Yesod typeclass in this chapter. For +a full listing of methods available, you should consult the Haddock +documentation.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book-1.6/yesods-monads.html b/public/book-1.6/yesods-monads.html new file mode 100644 index 00000000..a5a4f1cf --- /dev/null +++ b/public/book-1.6/yesods-monads.html @@ -0,0 +1,645 @@ + Yesod’s Monads :: Yesod Web Framework Book- Version 1.6 +
+

Yesod’s Monads

+ + +

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 +Model-View-Controller (MVC) terms, we could consider YesodDB to be the model, +Widget to be the view, and Handler to be the controller.

+

So far, we’ve presented some very straight-forward ways to use these monads: +your main handler will run in Handler, using runDB to execute a YesodDB +query, and defaultLayout to return a Widget, which in turn was created by +calls to toWidget.

+

However, if we have a deeper understanding of these types, we can achieve some +fancier results.

+
+

Monad Transformers

+
+ +Shrek- more or less + +

Monads are like onions. Monads are not like cakes.

+
+

Before we get into the heart of Yesod’s monads, we need to understand a bit +about monad transformers. (If you already know all about monad transformers, +you can likely skip this section.) Different monads provide different +functionality: Reader allows read-only access to some piece of data +throughout a computation, Error allows you to short-circuit computations, and +so on.

+

Often times, however, you would like to be able to combine a few of these +features together. After all, why not have a computation with read-only access +to some settings variable, that could error out at any time? One approach to +this would be to write a new monad like ReaderError, but this has the obvious +downside of exponential complexity: you’ll need to write a new monad for every +single possible combination.

+

Instead, we have monad transformers. In addition to Reader, we have +ReaderT, which adds reader functionality to any other monad. So we could +represent our ReaderError as (conceptually):

+
type ReaderError = ReaderT Error
+

In order to access our settings variable, we can use the ask function. But +what about short-circuiting a computation? We’d like to use throwError, but +that won’t exactly work. Instead, we need to lift our call into the next +monad up. In other words:

+
throwError :: errValue -> Error
+lift . throwError :: errValue -> ReaderT Error
+

There are a few things you should pick up here:

+
    +
  • +

    +A transformer can be used to add functionality to an existing monad. +

    +
  • +
  • +

    +A transformer must always wrap around an existing monad. +

    +
  • +
  • +

    +The functionality available in a wrapped monad will be dependent not only on + the monad transformer, but also on the inner monad that is being wrapped. +

    +
  • +
+

A great example of that last point is the IO monad. No matter how many layers +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

+ +

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.

+

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 +takeaway here is that we can layer our Persistent transformer on top of +Handler or Widget.

+

In order to make it simpler to refer to the relevant Persistent transformer, +the yesod-persistent package defines the YesodPersistBackend associated type. +For example, if I have a site called MyApp and it uses SQL, I would define +something like type instance YesodPersistBackend MyApp = SqlBackend. And for +more convenience, we have a type synonym called YesodDB which is defined as:

+
type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerT site IO)
+

Our database actions will then have types that look like YesodDB MyApp +SomeResult. In order to run these, we can use the standard Persistent unwrap +functions (like runSqlPool) to run the action and get back a normal +Handler. To automate this, we provide the runDB function. Putting it all +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 +can do, by using the handlerToWidget function. So for example, you can run +database queries inside a Widget by using something like handlerToWidget . +runDB.

+
+
+

Example: Database-driven navbar

+

Let’s put some of this new knowledge into action. We want to create a Widget +that generates its output based on the contents of the database. Previously, +our approach would have been to load up the data in a Handler, and then pass +that data into a Widget. Now, we’ll do the loading of data in the Widget +itself. This is a boon for modularity, as this Widget can be used in any +Handler we want, without any need to pass in the database contents.

+
{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.Logger    (runNoLoggingT)
+import           Data.Text               (Text)
+import           Data.Time
+import           Database.Persist.Sqlite
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Link
+    title Text
+    url Text
+    added UTCTime
+|]
+
+data App = App ConnectionPool
+
+mkYesod "App" [parseRoutes|
+/         HomeR    GET
+/add-link AddLinkR POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB db = do
+        App pool <- getYesod
+        runSqlPool db pool
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{AddLinkR}>
+            <p>
+                Add a new link to
+                <input type=url name=url value=http://>
+                titled
+                <input type=text name=title>
+                <input type=submit value="Add link">
+        <h2>Existing links
+        ^{existingLinks}
+    |]
+
+existingLinks :: Widget
+existingLinks = do
+    links <- handlerToWidget $ runDB $ selectList [] [LimitTo 5, Desc LinkAdded]
+    [whamlet|
+        <ul>
+            $forall Entity _ link <- links
+                <li>
+                    <a href=#{linkUrl link}>#{linkTitle link}
+    |]
+
+postAddLinkR :: Handler ()
+postAddLinkR = do
+    url <- runInputPost $ ireq urlField "url"
+    title <- runInputPost $ ireq textField "title"
+    now <- liftIO getCurrentTime
+    runDB $ insert $ Link title url now
+    setMessage "Link added"
+    redirect HomeR
+
+main :: IO ()
+main = runNoLoggingT $ withSqlitePool "links.db3" 10 $ \pool -> liftIO $ do
+    runSqlPersistMPool (runMigration migrateAll) pool
+    warp 3000 $ App pool
+

Pay attention in particular to the existingLinks function. Notice how all we +needed to do was apply handlerToWidget . runDB to a normal database action. +And from within getHomeR, we treated existingLinks like any ordinary +Widget, no special parameters at all. See the figure for the output of this +app.

+ +
+
+

Example: Request information

+

Likewise, you can get request information inside a Widget. Here we can determine the sort order of a list based on a GET parameter.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.List (sortOn)
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Michael" 26
+    , Person "Gavriella" 1
+    ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href="?sort=name">Sort by name
+            |
+            <a href="?sort=age">Sort by age
+            |
+            <a href="?">No sort
+        ^{showPeople}
+    |]
+
+showPeople :: Widget
+showPeople = do
+    msort <- runInputGet $ iopt textField "sort"
+    let people' =
+            case msort of
+                Just "name" -> sortOn personName people
+                Just "age"  -> sortOn personAge  people
+                _           -> people
+    [whamlet|
+        <dl>
+            $forall person <- people'
+                <dt>#{personName person}
+                <dd>#{show $ personAge person}
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Notice that in this case, we didn’t even have to call handlerToWidget. The +reason is that a number of the functions included in Yesod automatically work +for both Handler and Widget, by means of the MonadHandler typeclass. In +fact, MonadHandler will allow these functions to be "autolifted" through +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

+ +

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:

+
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 +application. As a motivating example, let’s consider the +monadcryptorandom +package from Hackage, which defines both a MonadCRandom typeclass for monads +which allow generating cryptographically-secure random values, and CRandT as +a concrete instance of that typeclass. You would like to write some code that +generates a random bytestring, e.g.:

+
import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+getHomeR = do
+    randomBS <- getBytes 128
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+

However, this results in an error message along the lines of:

+
    No instance for (MonadCRandom e0 (HandlerT App IO))
+      arising from a use of ‘getBytes’
+    In a stmt of a 'do' block: randomBS <- getBytes 128
+

How do we get such an instance? One approach is to simply use the CRandT monad transformer when we call getBytes. A complete example of doing so would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import Yesod
+import Crypto.Random (SystemRandom, newGenIO)
+import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    gen <- liftIO newGenIO
+    eres <- evalCRandT (getBytes 16) (gen :: SystemRandom)
+    randomBS <-
+        case eres of
+            Left e -> error $ show (e :: GenError)
+            Right gen -> return gen
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+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.

+

But there are two downsides to this approach:

+
    +
  1. +

    +It requires you to jump into this alternate monad each time you want to work with random values. +

    +
  2. +
  3. +

    +It’s inefficient: you need to create a new random seed each time you enter this other monad. +

    +
  4. +
+

The second point could be worked around by storing the random seed in the +foundation datatype, in a mutable reference like an IORef, and then +atomically sampling it each time we enter the CRandT transformer. But we can +even go a step further, and use this trick to make our Handler monad itself +an instance of MonadCRandom! Let’s look at the code, which is in fact a bit +involved:

+
{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE TypeSynonymInstances  #-}
+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           Data.ByteString.Base16     (encode)
+import           Data.IORef
+import           Data.Text.Encoding         (decodeUtf8)
+import           UnliftIO.Exception         (catch)
+import           Yesod
+
+data App = App
+    { randGen :: IORef SystemRandom
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    randomBS <- getBytes 16
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+instance MonadError GenError Handler where
+    throwError = throwM
+    catchError = catch
+instance MonadCRandom GenError Handler where
+    getCRandom = wrap crandom
+    {-# INLINE getCRandom #-}
+    getBytes i = wrap (genBytes i)
+    {-# INLINE getBytes #-}
+    getBytesWithEntropy i e = wrap (genBytesWithEntropy i e)
+    {-# INLINE getBytesWithEntropy #-}
+    doReseed bs = do
+        genRef <- fmap randGen getYesod
+        join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+            case reseed bs gen of
+                Left e -> (gen, throwM e)
+                Right gen' -> (gen', return ())
+    {-# INLINE doReseed #-}
+
+wrap :: (SystemRandom -> Either GenError (a, SystemRandom)) -> Handler a
+wrap f = do
+    genRef <- fmap randGen getYesod
+    join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+        case f gen of
+            Left e -> (gen, throwM e)
+            Right (x, gen') -> (gen', return x)
+
+main :: IO ()
+main = do
+    gen <- newGenIO
+    genRef <- newIORef gen
+    warp 3000 App
+        { randGen = genRef
+        }
+

This really comes down to a few different concepts:

+
    +
  1. +

    +We modify the App datatype to have a field for an IORef SystemRandom. +

    +
  2. +
  3. +

    +Similarly, we modify the main function to generate an IORef SystemRandom. +

    +
  4. +
  5. +

    +Our getHomeR function became a lot simpler: we can now simply call getBytes without playing with transformers. +

    +
  6. +
  7. +

    +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. +

    +
  8. +
+

Hopefully, this helps get across an important point: the power of the +HandlerT transformer. 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 +ErrorT with this abstraction.

+
+
+

Summary

+

If you completely ignore this chapter, you’ll still be able to use Yesod to +great benefit. The advantage of understanding how Yesod’s monads interact is to +be able to produce cleaner, more modular code. Being able to perform arbitrary +actions in a Widget can be a powerful tool, and understanding how Persistent +and your Handler code interact can help you make more informed design +decisions in your app.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book.html b/public/book.html new file mode 100644 index 00000000..c140749e --- /dev/null +++ b/public/book.html @@ -0,0 +1,187 @@ + Yesod Web Framework Book- Version 1.6 +

Available from O'Reilly:

+
Developing Web Applications with Haskell and Yesod + +
+
+

The current version of the book +covers Yesod 1.6. We also maintain older copies for +version 1.4, version 1.2 +, and +version 1.1.

+ +
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/.html b/public/book/.html new file mode 100644 index 00000000..c140749e --- /dev/null +++ b/public/book/.html @@ -0,0 +1,187 @@ + Yesod Web Framework Book- Version 1.6 +

Available from O'Reilly:

+
Developing Web Applications with Haskell and Yesod + +
+
+

The current version of the book +covers Yesod 1.6. We also maintain older copies for +version 1.4, version 1.2 +, and +version 1.1.

+ +
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/authentication-and-authorization.html b/public/book/authentication-and-authorization.html new file mode 100644 index 00000000..57423f84 --- /dev/null +++ b/public/book/authentication-and-authorization.html @@ -0,0 +1,661 @@ + Authentication and Authorization :: Yesod Web Framework Book- Version 1.6 +
+

Authentication and Authorization

+ + +

Authentication and authorization are two very related, and yet separate, +concepts. While the former deals with identifying a user, the latter determines +what a user is allowed to do. Unfortunately, since both terms are often +abbreviated as "auth," the concepts are often conflated.

+

Yesod provides built-in support for a number of third-party authentication +systems, such as OpenID, BrowserID and OAuth. These are systems where your +application trusts some external system for validating a user’s credentials. +Additionally, there is support for more commonly used username/password and +email/password systems. The former route ensures simplicity for users (no new +passwords to remember) and implementors (no need to deal with an entire +security architecture), while the latter gives the developer more control.

+

On the authorization side, we are able to take advantage of REST and type-safe +URLs to create simple, declarative systems. Additionally, since all +authorization code is written in Haskell, you have the full flexibility of the +language at your disposal.

+

This chapter will cover how to set up an "auth" solution in Yesod and discuss +some trade-offs in the different authentication options.

+
+

Overview

+

The yesod-auth package provides a unified interface for a number of different +authentication plugins. The only real requirement for these backends is that +they identify a user based on some unique string. In OpenID, for instance, this +would be the actual OpenID value. In BrowserID, it’s the email address. For +HashDB (which uses a database of hashed passwords), it’s the username.

+

Each authentication plugin provides its own system for logging in, whether it +be via passing tokens with an external site or an email/password form. After a +successful login, the plugin sets a value in the user’s session to indicate +his/her AuthId. This AuthId is usually a Persistent ID from a table used +for keeping track of users.

+

There are a few functions available for querying a user’s AuthId, most +commonly maybeAuthId, requireAuthId, maybeAuth and requireAuth. The +“require” versions will redirect to a login page if the user is not logged in, +while the second set of functions (the ones not ending in Id) give both the +table ID and entity value.

+

Since all of the storage of AuthId is built on top of sessions, all of the +rules from there apply. In particular, the data is stored in an encrypted, +HMACed client cookie, which automatically times out after a certain configurable +period of inactivity. Additionally, since there is no server-side component to sessions, +logging out simply deletes the data from the session cookie; if a user reuses an +older cookie value, the session will still be valid.

+ +

On the flip side, authorization is handled by a few methods inside the Yesod +typeclass. For every request, these methods are run to determine if access +should be allowed, denied, or if the user needs to be authenticated. By +default, these methods allow access for every request. Alternatively, you can +implement authorization in a more ad-hoc way by adding calls to requireAuth +and the like within individual handler functions, though this undermines many +of the benefits of a declarative authorization system.

+
+
+

Authenticate Me

+

Let’s jump right in with an example of authentication. For the Google OAuth authentication to work you should follow these steps:

+
    +
  1. +

    +Read on Google Developers Help how to obtain OAuth 2.0 credentials such as a client ID and client secret that are known to both Google and your application. +

    +
  2. +
  3. +

    +Set Authorized redirect URIs to http://localhost:3000/auth/page/googleemail2/complete. +

    +
  4. +
  5. +

    +Enable Google+ API and Contacts API. +

    +
  6. +
  7. +

    +Once you have the clientId and secretId, replace them in the code below. +

    +
  8. +
+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default                (def)
+import           Data.Text                   (Text)
+import           Network.HTTP.Client.Conduit (Manager, newManager)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.GoogleEmail2
+
+
+-- Replace with Google client ID.
+clientId :: Text
+clientId = ""
+
+-- Replace with Google secret ID.
+clientSecret :: Text
+clientSecret = ""
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Note: In order to log in with BrowserID, you must correctly
+    -- set your hostname here.
+    approot = ApprootStatic "http://localhost:3000"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    authenticate = return . Authenticated . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ = [ authGoogleEmail clientId clientSecret ]
+
+    -- The default maybeAuthId assumes a Persistent database. We're going for a
+    -- simpler AuthId, so we'll just do a direct lookup in the session.
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = do
+    man <- newManager
+    warp 3000 $ App man
+

We’ll start with the route declarations. First, we declare our standard HomeR +route, and then we set up the authentication subsite. Remember that a subsite +needs four parameters: the path to the subsite, the route name, the subsite +name, and a function to get the subsite value. In other words, based on the +line:

+
/auth AuthR Auth getAuth
+

We need to have getAuth :: MyAuthSite → Auth. While we haven’t written +that function ourselves, yesod-auth provides it automatically. With other +subsites (like static files), we provide configuration settings in the subsite +value, and therefore need to specify the get function. In the auth subsite, we +specify these settings in a separate typeclass, YesodAuth.

+ +

So what exactly goes in this YesodAuth instance? There are five required declarations:

+
    +
  • +

    +AuthId is an associated type. This is the value yesod-auth will give you + when you ask if a user is logged in (via maybeAuthId or requireAuthId). + In our case, we’re simply using Text, to store the raw identifier- email + address in our case, as we’ll soon see. +

    +
  • +
  • +

    +authenticate gets the actual AuthId from the Creds (credentials) data + type. This type has three pieces of information: the authentication backend + used (googleemail in our case), the actual identifier, and an + associated list of arbitrary extra information. Each backend provides + different extra information; see their docs for more information. +

    +
  • +
  • +

    +loginDest gives the route to redirect to after a successful login. +

    +
  • +
  • +

    +Likewise, logoutDest gives the route to redirect to after a logout. +

    +
  • +
  • +

    +authPlugins is a list of individual authentication backends to use. In our example, we’re using Google OAuth, which authenticates a user using their Google account. +

    +
  • +
+

In addition to these five methods, there are other methods available to control +other behavior of the authentication system, such as what the login page looks +like. For more information, please +see the API documentation.

+

In our HomeR handler, we have some simple links to the login and logout +pages, depending on whether or not the user is logged in. Notice how we +construct these subsite links: first we give the subsite route name (AuthR), +followed by the route within the subsite (LoginR and LogoutR).

+
+
+

Email

+

For many use cases, third-party authentication of email will be sufficient. +Occasionally, you’ll want users to create passwords on your site. The +scaffolded site does not include this setup, because:

+
    +
  • +

    +In order to securely accept passwords, you need to be running over SSL. Many + users are not serving their sites over SSL. +

    +
  • +
  • +

    +While the email backend properly salts and hashes passwords, a compromised + database could still be problematic. Again, we make no assumptions that Yesod + users are following secure deployment practices. +

    +
  • +
  • +

    +You need to have a working system for sending email. Many web servers these + days are not equipped to deal with all of the spam protection measures used + by mail servers. +

    +
  • +
+ +

But assuming you are able to meet these demands, and you want to have a +separate password login specifically for your site, Yesod offers a built-in +backend. It requires quite a bit of code to set up, since it needs to store +passwords securely in the database and send a number of different emails to +users (verify account, password retrieval, etc.).

+

Let’s have a look at a site that provides email authentication, storing +passwords in a Persistent SQLite database.

+ +
{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad            (join)
+import           Control.Monad.Logger     (runNoLoggingT)
+import           Data.Maybe               (isJust)
+import           Data.Text                (Text, unpack)
+import qualified Data.Text.Lazy.Encoding
+import           Data.Typeable            (Typeable)
+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
+import           Yesod.Auth.Email
+
+share [mkPersist sqlSettings { mpsGeneric = False }, mkMigrate "migrateAll"] [persistLowerCase|
+User
+    email Text
+    password Text Maybe -- Password may not be set yet
+    verkey Text Maybe -- Used for resetting passwords
+    verified Bool
+    UniqueUser email
+    deriving Typeable
+|]
+
+data App = App SqlBackend
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    -- Emails will include links, so be sure to include an approot so that
+    -- the links are valid!
+    approot = ApprootStatic "http://localhost:3000"
+    yesodMiddleware = defaultCsrfMiddleware . defaultYesodMiddleware
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Set up Persistent
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB f = do
+        App conn <- getYesod
+        runSqlConn f conn
+
+instance YesodAuth App where
+    type AuthId App = UserId
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    authPlugins _ = [authEmail]
+
+    -- Need to find the UserId for the given email address.
+    authenticate creds = liftHandler $ runDB $ do
+        x <- insertBy $ User (credsIdent creds) Nothing Nothing False
+        return $ Authenticated $
+            case x of
+                Left (Entity userid _) -> userid -- existing user
+                Right userid -> userid -- newly added user
+
+instance YesodAuthPersist App
+
+-- Here's all of the email-specific code
+instance YesodAuthEmail App where
+    type AuthEmailId App = UserId
+
+    afterPasswordRoute _ = HomeR
+
+    addUnverified email verkey =
+        liftHandler $ runDB $ insert $ User email Nothing (Just verkey) False
+
+    sendVerifyEmail email _ verurl = do
+        -- Print out to the console the verification email, for easier
+        -- debugging.
+        liftIO $ putStrLn $ "Copy/ Paste this URL in your browser:" ++ unpack verurl
+
+        -- Send email.
+        liftIO $ renderSendMail (emptyMail $ Address Nothing "noreply")
+            { mailTo = [Address Nothing email]
+            , mailHeaders =
+                [ ("Subject", "Verify your email address")
+                ]
+            , mailParts = [[textPart, htmlPart]]
+            }
+      where
+        textPart = Part
+            { partType = "text/plain; charset=utf-8"
+            , partEncoding = None
+            , partDisposition = DefaultDisposition
+            , partContent = PartContent $ Data.Text.Lazy.Encoding.encodeUtf8
+                [stext|
+                    Please confirm your email address by clicking on the link below.
+
+                    #{verurl}
+
+                    Thank you
+                |]
+            , partHeaders = []
+            }
+        htmlPart = Part
+            { partType = "text/html; charset=utf-8"
+            , partEncoding = None
+            , partDisposition = DefaultDisposition
+            , partContent = PartContent $ renderHtml
+                [shamlet|
+                    <p>Please confirm your email address by clicking on the link below.
+                    <p>
+                        <a href=#{verurl}>#{verurl}
+                    <p>Thank you
+                |]
+            , partHeaders = []
+            }
+    getVerifyKey = liftHandler . runDB . fmap (join . fmap userVerkey) . get
+    setVerifyKey uid key = liftHandler $ runDB $ update uid [UserVerkey =. Just key]
+    verifyAccount uid = liftHandler $ runDB $ do
+        mu <- get uid
+        case mu of
+            Nothing -> return Nothing
+            Just u -> do
+                update uid [UserVerified =. True, UserVerkey =. Nothing]
+                return $ Just uid
+    getPassword = liftHandler . runDB . fmap (join . fmap userPassword) . get
+    setPassword uid pass = liftHandler . runDB $ update uid [UserPassword =. Just pass]
+    getEmailCreds email = liftHandler $ runDB $ do
+        mu <- getBy $ UniqueUser email
+        case mu of
+            Nothing -> return Nothing
+            Just (Entity uid u) -> return $ Just EmailCreds
+                { emailCredsId = uid
+                , emailCredsAuthId = Just uid
+                , emailCredsStatus = isJust $ userPassword u
+                , emailCredsVerkey = userVerkey u
+                , emailCredsEmail = email
+                }
+    getEmail = liftHandler . runDB . fmap (fmap userEmail) . get
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>Go to the login page
+        |]
+
+main :: IO ()
+main = runNoLoggingT $ withSqliteConn "email.db3" $ \conn -> liftIO $ do
+    runSqlConn (runMigration migrateAll) conn
+    warp 3000 $ App conn
+
+
+

Authorization

+

Once you can authenticate your users, you can use their credentials to +authorize requests. Authorization in Yesod is simple and declarative: most of +the time, you just need to add the authRoute and isAuthorized methods to +your Yesod typeclass instance. Let’s see an example.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Default         (def)
+import           Data.Text            (Text)
+import           Network.HTTP.Conduit (Manager, newManager, tlsManagerSettings)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy -- just for testing, don't use in real life!!!
+
+data App = App
+    { httpManager :: Manager
+    }
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET POST
+/admin AdminR GET
+/auth  AuthR  Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+
+    -- route name, then a boolean indicating if it's a write request
+    isAuthorized HomeR True = isAdmin
+    isAuthorized AdminR _ = isAdmin
+
+    -- anyone can access other pages
+    isAuthorized _ _ = return Authorized
+
+isAdmin = do
+    mu <- maybeAuthId
+    return $ case mu of
+        Nothing -> AuthenticationRequired
+        Just "admin" -> Authorized
+        Just _ -> Unauthorized "You must be an admin"
+
+instance YesodAuth App where
+    type AuthId App = Text
+    authenticate = return . Authenticated . credsIdent
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+
+    authPlugins _ = [authDummy]
+
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = do
+    maid <- maybeAuthId
+    defaultLayout
+        [whamlet|
+            <p>Note: Log in as "admin" to be an administrator.
+            <p>Your current auth ID: #{show maid}
+            $maybe _ <- maid
+                <p>
+                    <a href=@{AuthR LogoutR}>Logout
+            <p>
+                <a href=@{AdminR}>Go to admin page
+            <form method=post>
+                Make a change (admins only)
+                \ #
+                <input type=submit>
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    setMessage "You made some change to the page"
+    redirect HomeR
+
+getAdminR :: Handler Html
+getAdminR = defaultLayout
+    [whamlet|
+        <p>I guess you're an admin!
+        <p>
+            <a href=@{HomeR}>Return to homepage
+    |]
+
+main :: IO ()
+main = do
+    manager <- newManager tlsManagerSettings
+    warp 3000 $ App manager
+

authRoute should be your login page, almost always AuthR LoginR. +isAuthorized is a function that takes two parameters: the requested route, +and whether or not the request was a "write" request. You can actually change +the meaning of what a write request is using the isWriteRequest method, but +the out-of-the-box version follows RESTful principles: anything but a GET, +HEAD, OPTIONS or TRACE request is a write request.

+

What’s convenient about the body of isAuthorized is that you can run any +Handler code you want. This means you can:

+
    +
  • +

    +Access the filesystem (normal IO) +

    +
  • +
  • +

    +Lookup values in the database +

    +
  • +
  • +

    +Pull any session or request values you want +

    +
  • +
+

Using these techniques, you can develop as sophisticated an authorization +system as you like, or even tie into existing systems used by your +organization.

+
+
+

Conclusion

+

This chapter covered the basics of setting up user authentication, as well as +how the built-in authorization functions provide a simple, declarative approach +for users. While these are complicated concepts, with many approaches, Yesod +should provide you with the building blocks you need to create your own +customized auth solution.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/basics.html b/public/book/basics.html new file mode 100644 index 00000000..d7c89dd7 --- /dev/null +++ b/public/book/basics.html @@ -0,0 +1,466 @@ + Basics :: Yesod Web Framework Book- Version 1.6 +
+

Basics

+ + +

The first step with any new technology is getting it running. The goal of +this chapter is to get you started with a simple Yesod application, and cover +some of the basic concepts and terminology.

+
+

Hello World

+

Let’s get this book started properly: a simple web page that says Hello +World:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data HelloWorld = HelloWorld
+
+mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod HelloWorld
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|Hello World!|]
+
+main :: IO ()
+main = warp 3000 HelloWorld
+

If you save that code in helloworld.hs and run it with runhaskell +helloworld.hs, you’ll get a web server running on port 3000. Note, +if you followed the quick start guide and installed yesod with stack +then you will not have runhaskell and will need to run stack runghc +helloworld.hs instead. If you point your browser to http://localhost:3000, +you’ll get the following HTML:

+
<!DOCTYPE html>
+<html><head><title></title></head><body>Hello World!</body></html>
+

We’ll refer back to this example through the rest of the chapter.

+
+
+

Routing

+

Like most modern web frameworks, Yesod follows a +front controller +pattern. This means that every request to a Yesod application enters at the +same point and is routed from there. As a contrast, in systems like PHP and ASP +you usually create a number of different files, and the web server +automatically directs requests to the relevant file.

+

In addition, Yesod uses a declarative style for specifying routes. In our +example above, this looked like:

+
mkYesod "HelloWorld" [parseRoutes|
+/ HomeR GET
+|]
+ +

In English, all this means is: In the HelloWorld application, create one route. +I’d like to call it HomeR, it should listen for requests to / (the root of +the application), and should answer GET requests. We call HomeR a resource, +which is where the "R" suffix comes from.

+ +

The mkYesod TH function generates quite a bit of code here: a route data +type, parser/render functions, a dispatch function, and some helper types. +We’ll look at this in more detail in the routing chapter. But by using the +-ddump-splices GHC option, we can get an immediate look at the generated +code. A much cleaned up version of it is:

+
instance RenderRoute HelloWorld where
+    data Route HelloWorld = HomeR
+        deriving (Show, Eq, Read)
+    renderRoute HomeR = ([], [])
+
+instance ParseRoute HelloWorld where
+    parseRoute ([], _) = Just HomeR
+    parseRoute _       = Nothing
+
+instance YesodDispatch HelloWorld where
+    yesodDispatch env req =
+        yesodRunner handler env mroute req
+      where
+        mroute = parseRoute (pathInfo req, textQueryString req)
+        handler =
+            case mroute of
+                Nothing -> notFound
+                Just HomeR ->
+                    case requestMethod req of
+                        "GET" -> getHomeR
+                        _     -> badMethod
+
+type Handler = HandlerT HelloWorld IO
+ +

We can see that the RenderRoute class defines an associated data type +providing the routes for our application. In this simple example, we have just +one route: HomeR. In real life applications, we’ll have many more, and they +will be more complicated than our HomeR.

+

renderRoute takes a route and turns it into path segments and query string +parameters. Again, our example is simple, so the code is likewise simple: both +values are empty lists.

+

ParseRoute provides the inverse function, parseRoute. Here we see the first +strong motivation for our reliance on Template Haskell: it ensures that the +parsing and rendering of routes correspond correctly with each other. This kind +of code can easily become difficult to keep in sync when written by hand. By +relying on code generation, we’re letting the compiler (and Yesod) handle those +details for us.

+

YesodDispatch provides a means of taking an input request and passing it to +the appropriate handler function. The process is essentially:

+
    +
  1. +

    +Parse the request. +

    +
  2. +
  3. +

    +Choose a handler function. +

    +
  4. +
  5. +

    +Run the handler function. +

    +
  6. +
+

The code generation follows a simple format for matching routes to handler +function names, which we’ll describe in the next section.

+

Finally, we have a simple type synonym defining Handler to make our code a +little easier to write.

+

There’s a lot more going on here than we’ve described. The generated dispatch +code actually uses the view patterns language extension for efficiency, more +type class instances are created, and there are other cases to handle such as +subsites. We’ll get into the details as we go through the book, especially in +the “Understanding a Request” chapter.

+
+
+

Handler function

+

So we have a route named HomeR, and it responds to GET requests. How do you +define your response? You write a handler function. Yesod follows a standard +naming scheme for these functions: it’s the lower case method name (e.g., GET +becomes get) followed by the route name. In this case, the function name +would be getHomeR.

+

Most of the code you write in Yesod lives in handler functions. This is where +you process user input, perform database queries and create responses. In our +simple example, we create a response using the defaultLayout function. This +function wraps up the content it’s given in your site’s template. By default, +it produces an HTML file with a doctype and html, head and body tags. As +we’ll see in the Yesod typeclass chapter, this function can be overridden to do +much more.

+

In our example, we pass [whamlet|Hello World!|] to defaultLayout. +whamlet is another quasi-quoter. In this case, it converts Hamlet syntax into +a Widget. Hamlet is the default HTML templating engine in Yesod. Together with +its siblings Cassius, Lucius and Julius, you can create HTML, CSS and +Javascript in a fully type-safe and compile-time-checked manner. We’ll see much +more about this in the Shakespeare chapter.

+

Widgets are another cornerstone of Yesod. They allow you to create modular +components of a site consisting of HTML, CSS and Javascript and reuse them +throughout your site. We’ll get into more detail on them in the widgets +chapter.

+
+
+

The Foundation

+

The word ‘HelloWorld’ shows up a number of times in our example. Every Yesod +application has a foundation datatype. This datatype must be an instance of the +Yesod typeclass, which provides a central place for declaring a number of +different settings controlling the execution of our application.

+

In our case, this datatype is pretty boring: it doesn’t contain any +information. Nonetheless, the foundation is central to how our example runs: it +ties together the routes with the instance declaration and lets it all be run. +We’ll see throughout this book that the foundation pops up in a whole bunch of +places.

+

But foundations don’t have to be boring: they can be used to store lots of +useful information, usually stuff that needs to be initialized at program +launch and used throughout. Some very common examples are:

+
    +
  • +

    +A database connection pool. +

    +
  • +
  • +

    +Settings loaded from a config file. +

    +
  • +
  • +

    +An HTTP connection manager. +

    +
  • +
  • +

    +A random number generator. +

    +
  • +
+ +
+
+

Running

+

Once again we mention HelloWorld in our main function. Our foundation +contains all the information we need to route and respond to requests in our +application; now we just need to convert it into something that can run. A +useful function for this in Yesod is warp, which runs the Warp webserver with +a number of default settings enabled on the specified port (here, it’s 3000).

+

One of the features of Yesod is that you aren’t tied down to a single +deployment strategy. Yesod is built on top of the Web Application Interface +(WAI), allowing it to run on FastCGI, SCGI, Warp, or even as a desktop +application using the Webkit library. We’ll discuss some of these options in +the deployment chapter. And at the end of this chapter, we will explain the +development server.

+

Warp is the premiere deployment option for Yesod. It is a lightweight, highly +efficient web server developed specifically for hosting Yesod. It is also used +outside of Yesod for other Haskell development (both framework and +non-framework applications), as well as a standard file server in a number of +production environments.

+
+
+

Resources and type-safe URLs

+

In our hello world, we defined just a single resource (HomeR). A web +application is usually much more exciting with more than one page on it. Let’s +take a look:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data Links = Links
+
+mkYesod "Links" [parseRoutes|
+/ HomeR GET
+/page1 Page1R GET
+/page2 Page2R GET
+|]
+
+instance Yesod Links
+
+getHomeR  = defaultLayout [whamlet|<a href=@{Page1R}>Go to page 1!|]
+getPage1R = defaultLayout [whamlet|<a href=@{Page2R}>Go to page 2!|]
+getPage2R = defaultLayout [whamlet|<a href=@{HomeR}>Go home!|]
+
+main = warp 3000 Links
+

Overall, this is very similar to Hello World. Our foundation is now Links +instead of HelloWorld, and in addition to the HomeR resource, we’ve added +Page1R and Page2R. As such, we’ve also added two more handler functions: +getPage1R and getPage2R.

+

The only truly new feature is inside the whamlet quasi-quotation. We’ll delve +into syntax in the “Shakespeare” chapter, but we can see that:

+
<a href=@{Page1R}>Go to page 1!
+

creates a link to the Page1R resource. The important thing to note here is +that Page1R is a data constructor. By making each resource a data +constructor, we have a feature called type-safe URLs. Instead of splicing +together strings to create URLs, we simply create a plain old Haskell value. By +using at-sign interpolation (@{…}), Yesod automatically renders those +values to textual URLs before sending things off to the user. We can see how +this is implemented by looking again at the -ddump-splices output:

+
instance RenderRoute Links where
+    data Route Links = HomeR | Page1R | Page2R
+      deriving (Show, Eq, Read)
+
+    renderRoute HomeR  = ([], [])
+    renderRoute Page1R = (["page1"], [])
+    renderRoute Page2R = (["page2"], [])
+

In the Route associated type for Links, we have additional constructors for +Page1R and Page2R. We also now have a better glimpse of the return values +for renderRoute. The first part of the tuple gives the path pieces for the +given route. The second part gives the query string parameters; for almost all +use cases, this will be an empty list.

+

It’s hard to over-estimate the value of type-safe URLs. They give you a huge +amount of flexibility and robustness when developing your application. You can +move URLs around at will without ever breaking links. In the routing chapter, +we’ll see that routes can take parameters, such as a blog entry URL taking the +blog post ID.

+

Let’s say you want to switch from routing on the numerical post ID to a +year/month/slug setup. In a traditional web framework, you would need to go +through every single reference to your blog post route and update +appropriately. If you miss one, you’ll have 404s at runtime. In Yesod, all you +do is update your route and compile: GHC will pinpoint every single line of +code that needs to be corrected.

+
+
+

Non-HTML responses

+

Yesod can serve up any kind of content you want, and has first-class support +for many commonly used response formats. You’ve seen HTML so far, but JSON data +is just as easy, via the aeson package:

+
{-# LANGUAGE ExtendedDefaultRules #-}
+{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE QuasiQuotes          #-}
+{-# LANGUAGE TemplateHaskell      #-}
+{-# LANGUAGE TypeFamilies         #-}
+import Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR  = return $ object ["msg" .= "Hello World"]
+
+main = warp 3000 App
+

We’ll cover JSON responses in more detail in later chapters, including how to +automatically switch between HTML and JSON representations depending on the +Accept request header.

+
+
+

The scaffolded site

+

We’ll use stack to install the Yesod libraries and the yesod helper +executable. Then we’ll use stack new to create a scaffolded site. It +will generate a folder containing the default scaffolded site. Inside that +folder, you can run stack install --only-dependencies to build any extra +dependencies (such as your database backends), and then yesod devel to run +your site.

+ +

The scaffolded site gives you a lot of best practices out of the box, setting +up files and dependencies in a time-tested approach used by most production +Yesod sites. However, all this convenience can get in the way of actually +learning Yesod. Therefore, most of this book will avoid the scaffolding tool, +and instead deal directly with Yesod as a library. But if you’re going to build +a real site, I strongly recommend using the scaffolding.

+

We will cover the structure of the scaffolded site in the scaffolding chapter.

+
+
+

Development server

+

One of the advantages interpreted languages have over compiled languages is +fast prototyping: you save changes to a file and hit refresh. If we want to +make any changes to our Yesod apps above, we’ll need to call runhaskell from +scratch, which can be a bit tedious.

+

Fortunately, there’s a solution to this: yesod devel automatically rebuilds +and reloads your code for you. This can be a great way to develop your Yesod +projects, and when you’re ready to move to production, you still get to compile +down to incredibly efficient code. The Yesod scaffolding automatically sets +things up for you. This gives you the best of both worlds: rapid prototyping +and fast production code.

+

It’s a little bit more involved to set up your code to be used by yesod +devel, so our examples will just use warp. Fortunately, the scaffolded site +is fully configured to use the development server, so when you’re ready to move +over to the real world, it will be waiting for you.

+
+
+

Summary

+

Every Yesod application is built around a foundation datatype. We associate +some resources with that datatype and define some handler functions, and Yesod +handles all of the routing. These resources are also data constructors, which +lets us have type-safe URLs.

+

By being built on top of WAI, Yesod applications can run with a number of +different backends. For simple apps, the warp function provides a convenient +way to use the Warp web server. For rapid development, using yesod devel is a +good choice. And when you’re ready to move to production, you have the full +power and flexibility to configure Warp (or any other WAI handler) to suit your +needs.

+

When developing in Yesod, we get a number of choices for coding style: +quasi-quotation or external files, warp or yesod devel, and so on. The +examples in this book will tend towards using the choices that are easiest to +copy-and-paste, but the more powerful options will be available when you start +building real Yesod applications.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/blog-example-advanced.html b/public/book/blog-example-advanced.html new file mode 100644 index 00000000..fc7d2bdd --- /dev/null +++ b/public/book/blog-example-advanced.html @@ -0,0 +1,511 @@ + Blog: i18n, authentication, authorization, and database :: Yesod Web Framework Book- Version 1.6 +
+

Blog: i18n, authentication, authorization, and database

+ + +

This is a simple blog app. It allows an admin to add blog posts via a rich text +editor (nicedit), allows logged-in users to comment, and has full i18n support. +It is also a good example of using a Persistent database, leveraging Yesod’s +authorization system, and templates.

+

While in general we recommend placing templates, Persist entity definitions, +and routing in separate files, we’ll keep it all in one file here for +convenience. The one exception you’ll see below will be i18n messages.

+

We’ll start off with our language extensions. In scaffolded code, the language +extensions are specified in the cabal file, so you won’t need to put this in +your individual Haskell files.

+
{-# LANGUAGE OverloadedStrings, TypeFamilies, QuasiQuotes,
+             TemplateHaskell, GADTs, FlexibleContexts,
+             MultiParamTypeClasses, DeriveDataTypeable,
+             GeneralizedNewtypeDeriving, ViewPatterns #-}
+

Now our imports.

+
import Yesod
+import Yesod.Auth
+import Yesod.Form.Nic (YesodNic, nicHtmlField)
+import Yesod.Auth.OpenId (IdentifierType(..), authOpenId)
+import Data.Text (Text)
+import Network.HTTP.Client.TLS (tlsManagerSettings)
+import Network.HTTP.Conduit (Manager, newManager)
+import Database.Persist.Sqlite
+    ( ConnectionPool, SqlBackend, runSqlPool, runMigration
+    , createSqlitePool, runSqlPersistMPool
+    )
+import Data.Time (UTCTime, getCurrentTime)
+import Control.Applicative ((<$>), (<*>), pure)
+import Data.Typeable (Typeable)
+import Control.Monad.Logger (runStdoutLoggingT)
+

First we’ll set up our Persistent entities. We’re going to both create our data +types (via mkPersist) and create a migration function, which will automatically +create and update our SQL schema. If you were using the MongoDB backend, +migration would not be needed.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+

Keeps track of users. In a more robust application, we would also keep account +creation date, display name, etc.

+
User
+   email Text
+   UniqueUser email
+

In order to work with yesod-auth’s caching, our User type must be an instance +of Typeable.

+
   deriving Typeable
+

An individual blog entry (I’ve avoided using the word "post" due to the +confusion with the request method POST).

+
Entry
+   title Text
+   posted UTCTime
+   content Html
+

And a comment on the blog post.

+
Comment
+   entry EntryId
+   posted UTCTime
+   user UserId
+   name Text
+   text Textarea
+|]
+

Every site has a foundation datatype. This value is initialized before +launching your application, and is available throughout. We’ll store a database +connection pool and HTTP connection manager in ours. See the very end of this +file for how those are initialized.

+
data Blog = Blog
+   { connPool    :: ConnectionPool
+   , httpManager :: Manager
+   }
+

To make i18n easy and translator friendly, we have a special file format for +translated messages. There is a single file for each language, and each file is +named based on the language code (e.g., en, es, de-DE) and placed in that +folder. We also specify the main language file (here, "en") as a default +language.

+
mkMessage "Blog" "blog-messages" "en"
+

Our blog-messages/en.msg file contains the following content:

+
-- @blog-messages/en.msg
+NotAnAdmin: You must be an administrator to access this page.
+
+WelcomeHomepage: Welcome to the homepage
+SeeArchive: See the archive
+
+NoEntries: There are no entries in the blog
+LoginToPost: Admins can login to post
+NewEntry: Post to blog
+NewEntryTitle: Title
+NewEntryContent: Content
+
+PleaseCorrectEntry: Your submitted entry had some errors, please correct and try again.
+EntryCreated title@Text: Your new blog post, #{title}, has been created
+
+EntryTitle title@Text: Blog post: #{title}
+CommentsHeading: Comments
+NoComments: There are no comments
+AddCommentHeading: Add a Comment
+LoginToComment: You must be logged in to comment
+AddCommentButton: Add comment
+
+CommentName: Your display name
+CommentText: Comment
+CommentAdded: Your comment has been added
+PleaseCorrectComment: Your submitted comment had some errors, please correct and try again.
+
+HomepageTitle: Yesod Blog Demo
+BlogArchiveTitle: Blog Archive
+

Now we’re going to set up our routing table. We have four entries: a homepage, +an entry list page (BlogR), an individual entry page (EntryR) and our +authentication subsite. Note that BlogR and EntryR both accept GET and POST +methods. The POST methods are for adding a new blog post and adding a new +comment, respectively.

+
mkYesod "Blog" [parseRoutes|
+/              HomeR  GET
+/blog          BlogR  GET POST
+/blog/#EntryId EntryR GET POST
+/auth          AuthR  Auth getAuth
+|]
+

Every foundation needs to be an instance of the Yesod typeclass. This is where +we configure various settings.

+
instance Yesod Blog where
+

The base of our application. Note that in order to make BrowserID work +properly, this must be a valid URL.

+
    approot = ApprootStatic "http://localhost:3000"
+

Our authorization scheme. We want to have the following rules:

+
    +
  • +

    +Only admins can add a new entry. +

    +
  • +
  • +

    +Only logged in users can add a new comment. +

    +
  • +
  • +

    +All other pages can be accessed by anyone. +

    +
  • +
+

We set up our routes in a RESTful way, where the actions that could make +changes are always using a POST method. As a result, we can simply check for +whether or not a request is a write request, given by the True in the second +field.

+

First, we’ll authorize requests to add a new entry.

+
    isAuthorized BlogR True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just (Entity _ user)
+                | isAdmin user -> return Authorized
+                | otherwise    -> unauthorizedI MsgNotAnAdmin
+

Now we’ll authorize requests to add a new comment.

+
    isAuthorized (EntryR _) True = do
+        mauth <- maybeAuth
+        case mauth of
+            Nothing -> return AuthenticationRequired
+            Just _  -> return Authorized
+

And for all other requests, the result is always authorized.

+
    isAuthorized _ _ = return Authorized
+

Where a user should be redirected to if they get an AuthenticationRequired.

+
    authRoute _ = Just (AuthR LoginR)
+

This is where we define our site look-and-feel. The function is given the +content for the individual page, and wraps it up with a standard template.

+
    defaultLayout inside = do
+

Yesod encourages the get-following-post pattern, where after a POST, the user +is redirected to another page. In order to allow the POST page to give the user +some kind of feedback, we have the getMessage and setMessage functions. It’s a +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 +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 +CSS for all pages. We’ll use the Lucius template language to create the latter.

+
        pc <- widgetToPageContent $ do
+            toWidget [lucius|
+body {
+    width: 760px;
+    margin: 1em auto;
+    font-family: sans-serif;
+}
+textarea {
+    width: 400px;
+    height: 200px;
+}
+#message {
+  color: #900;
+}
+|]
+            inside
+

And finally we’ll use a new Hamlet template to wrap up the individual +components (title, head data and body data) into the final output.

+
        withUrlRenderer [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle pc}
+        ^{pageHead pc}
+    <body>
+        $maybe msg <- mmsg
+            <div #message>#{msg}
+        ^{pageBody pc}
+|]
+

This is a simple function to check if a user is the admin. In a real +application, we would likely store the admin bit in the database itself, or +check with some external system. For now, I’ve just hard-coded my own email +address.

+
isAdmin :: User -> Bool
+isAdmin user = userEmail user == "michael@snoyman.com"
+

In order to access the database, we need to create a YesodPersist instance, +which says which backend we’re using and how to run an action.

+
instance YesodPersist Blog where
+   type YesodPersistBackend Blog = SqlBackend
+   runDB f = do
+       master <- getYesod
+       let pool = connPool master
+       runSqlPool f pool
+

This is a convenience synonym. It is defined automatically for you in the +scaffolding.

+
type Form x = Html -> MForm Handler (FormResult x, Widget)
+

In order to use yesod-form and yesod-auth, we need an instance of RenderMessage +for FormMessage. This allows us to control the i18n of individual form +messages.

+
instance RenderMessage Blog FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

In order to use the built-in nic HTML editor, we need this instance. We just +take the default values, which use a CDN-hosted version of Nic.

+
instance YesodNic Blog
+

In order to use yesod-auth, we need a YesodAuth instance.

+
instance YesodAuth Blog where
+    type AuthId Blog = UserId
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+

We’ll use external OpenId providers to authenticate our users and request +email addresses to use as user id. This makes it easy to switch to other +systems in the future, locally authenticated email addresses (also included +with yesod-auth).

+
    authPlugins _ = [authOpenId Claimed
+                        [ ("openid.ns.ax", "http://openid.net/srv/ax/1.0")
+                        , ("openid.ax.mode", "fetch_request")
+                        , ("openid.ax.type.email",
+                           "http://axschema.org/contact/email")
+                        , ("openid.ax.required", "email")
+                        ]
+                    ]
+

This function takes someone’s login credentials (including his/her email address) +and gives back a UserId.

+
    getAuthId creds =
+      -- Key name for email value may vary between providers
+      let emailKey = "openid.ax.value.email" in
+      case lookup emailKey (credsExtra creds) of
+          Just email -> do
+              res <- liftHandler $ runDB $ insertBy (User email)
+              return $ Just $ either entityKey id res
+          Nothing -> return Nothing
+

We also need to provide a YesodAuthPersist instance to work with Persistent.

+
instance YesodAuthPersist Blog
+

Homepage handler. The one important detail here is our usage of setTitleI, +which allows us to use i18n messages for the title. We also use this message +with a _{Msg…} interpolation in Hamlet.

+
getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitleI MsgHomepageTitle
+    [whamlet|
+<p>_{MsgWelcomeHomepage}
+<p>
+   <a href=@{BlogR}>_{MsgSeeArchive}
+|]
+

Define a form for adding new entries. We want the user to provide the title and +content, and then fill in the post date automatically via getCurrentTime.

+

Note that slightly strange lift (liftIO getCurrentTime) manner of running an +IO action. The reason is that applicative forms are not monads, and therefore +cannot be instances of MonadIO. Instead, we use lift to run the action in +the underlying Handler monad, and liftIO to convert the IO action into a +Handler action.

+
entryForm :: Form Entry
+entryForm = renderDivs $ Entry
+    <$> areq textField (fieldSettingsLabel MsgNewEntryTitle) Nothing
+    <*> lift (liftIO getCurrentTime)
+    <*> areq nicHtmlField (fieldSettingsLabel MsgNewEntryContent) Nothing
+

Get the list of all blog entries, and present an admin with a form to create a +new entry.

+
getBlogR :: Handler Html
+getBlogR = do
+    muser <- maybeAuth
+    entries <- runDB $ selectList [] [Desc EntryPosted]
+    (entryWidget, enctype) <- generateFormPost entryForm
+    defaultLayout $ do
+        setTitleI MsgBlogArchiveTitle
+        [whamlet|
+$if null entries
+    <p>_{MsgNoEntries}
+$else
+    <ul>
+        $forall Entity entryId entry <- entries
+            <li>
+                <a href=@{EntryR entryId}>#{entryTitle entry}
+

We have three possibilities: the user is logged in as an admin, the user is +logged in and is not an admin, and the user is not logged in. In the first +case, we should display the entry form. In the second, we’ll do nothing. In the +third, we’ll provide a login link.

+
$maybe Entity _ user <- muser
+    $if isAdmin user
+        <form method=post enctype=#{enctype}>
+            ^{entryWidget}
+            <div>
+                <input type=submit value=_{MsgNewEntry}>
+$nothing
+    <p>
+        <a href=@{AuthR LoginR}>_{MsgLoginToPost}
+|]
+

Process an incoming entry addition. We don’t do any permissions checking, since +isAuthorized handles it for us. If the form submission was valid, we add the +entry to the database and redirect to the new entry. Otherwise, we ask the user +to try again.

+
postBlogR :: Handler Html
+postBlogR = do
+    ((res, entryWidget), enctype) <- runFormPost entryForm
+    case res of
+        FormSuccess entry -> do
+            entryId <- runDB $ insert entry
+            setMessageI $ MsgEntryCreated $ entryTitle entry
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectEntry
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{entryWidget}
+    <div>
+        <input type=submit value=_{MsgNewEntry}>
+|]
+

A form for comments, very similar to our entryForm above. It takes the +EntryId of the entry the comment is attached to. By using pure, we embed +this value in the resulting Comment output, without having it appear in the +generated HTML.

+
commentForm :: EntryId -> Form Comment
+commentForm entryId = renderDivs $ Comment
+    <$> pure entryId
+    <*> lift (liftIO getCurrentTime)
+    <*> lift requireAuthId
+    <*> areq textField (fieldSettingsLabel MsgCommentName) Nothing
+    <*> areq textareaField (fieldSettingsLabel MsgCommentText) Nothing
+

Show an individual entry, comments, and an add comment form if the user is +logged in.

+
getEntryR :: EntryId -> Handler Html
+getEntryR entryId = do
+    (entry, comments) <- runDB $ do
+        entry <- get404 entryId
+        comments <- selectList [CommentEntry ==. entryId] [Asc CommentPosted]
+        return (entry, map entityVal comments)
+    muser <- maybeAuth
+    (commentWidget, enctype) <-
+        generateFormPost (commentForm entryId)
+    defaultLayout $ do
+        setTitleI $ MsgEntryTitle $ entryTitle entry
+        [whamlet|
+<h1>#{entryTitle entry}
+<article>#{entryContent entry}
+    <section .comments>
+        <h1>_{MsgCommentsHeading}
+        $if null comments
+            <p>_{MsgNoComments}
+        $else
+            $forall Comment _entry posted _user name text <- comments
+                <div .comment>
+                    <span .by>#{name}
+                    <span .at>#{show posted}
+                    <div .content>#{text}
+        <section>
+            <h1>_{MsgAddCommentHeading}
+            $maybe _ <- muser
+                <form method=post enctype=#{enctype}>
+                    ^{commentWidget}
+                    <div>
+                        <input type=submit value=_{MsgAddCommentButton}>
+            $nothing
+                <p>
+                    <a href=@{AuthR LoginR}>_{MsgLoginToComment}
+|]
+

Receive an incoming comment submission.

+
postEntryR :: EntryId -> Handler Html
+postEntryR entryId = do
+    ((res, commentWidget), enctype) <-
+        runFormPost (commentForm entryId)
+    case res of
+        FormSuccess comment -> do
+            _ <- runDB $ insert comment
+            setMessageI MsgCommentAdded
+            redirect $ EntryR entryId
+        _ -> defaultLayout $ do
+            setTitleI MsgPleaseCorrectComment
+            [whamlet|
+<form method=post enctype=#{enctype}>
+    ^{commentWidget}
+    <div>
+        <input type=submit value=_{MsgAddCommentButton}>
+|]
+

Finally our main function.

+
main :: IO ()
+main = do
+    pool <- runStdoutLoggingT $ createSqlitePool "blog.db3" 10 -- create a new pool
+    -- perform any necessary migration
+    runSqlPersistMPool (runMigration migrateAll) pool
+    manager <- newManager tlsManagerSettings -- create a new HTTP manager
+    warp 3000 $ Blog pool manager -- start our server
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/case-study-sphinx.html b/public/book/case-study-sphinx.html new file mode 100644 index 00000000..20f2eb12 --- /dev/null +++ b/public/book/case-study-sphinx.html @@ -0,0 +1,790 @@ + Case Study: Sphinx-based Search :: Yesod Web Framework Book- Version 1.6 +
+

Case Study: Sphinx-based Search

+ + +

Sphinx is a search server, and powers the +search feature on many sites. While the actual code necessary to integrate +Yesod with Sphinx is relatively short, it touches on a number of complicated +topics, and is therefore a great case study in how to play with some of the +under-the-surface details of Yesod.

+

There are essentially three different pieces at play here:

+
    +
  • +

    +Storing the content we wish to search. This is fairly straight-forward + Persistent code, and we won’t dwell on it much in this chapter. +

    +
  • +
  • +

    +Accessing Sphinx search results from inside Yesod. Thanks to the sphinx + package, this is actually very easy. +

    +
  • +
  • +

    +Providing the document content to Sphinx. This is where the interesting stuff + happens, and will show how to deal with streaming content from a database + directly to XML, which gets sent directly over the wire to the client. +

    +
  • +
+

The full code for this example can be +found +on FP Haskell Center.

+
+

Sphinx Setup

+

Unlike many of our other examples, to start with here we’ll need to actually +configure and run our external Sphinx server. I’m not going to go into all the +details of Sphinx, partly because it’s not relevant to our point here, and +mostly because I’m not an expert on Sphinx.

+

Sphinx provides three main command line utilities: searchd is the actual +search daemon that receives requests from the client (in this case, our web +app) and returns the search results. indexer parses the set of documents and +creates the search index. search is a debugging utility that will run simple +queries against Sphinx.

+

There are two important settings: the source and the index. The source tells +Sphinx where to read document information from. It has direct support for MySQL +and PostgreSQL, as well as a more general XML format known as xmlpipe2. We’re +going to use the last one. This not only will give us more flexibility with +choosing Persistent backends, but will also demonstrate some more powerful +Yesod concepts.

+

The second setting is the index. Sphinx can handle multiple indices +simultaneously, which allows it to provide search for multiple services at +once. Each index will have a source it pulls from.

+

In our case, we’re going to provide a URL from our application +(/search/xmlpipe) that provides the XML file required by Sphinx, and then pipe +that through to the indexer. So we’ll add the following to our Sphinx config +file:

+
source searcher_src
+{
+        type = xmlpipe2
+        xmlpipe_command = curl http://localhost:3000/search/xmlpipe
+}
+
+index searcher
+{
+        source = searcher_src
+        path = /var/data/searcher
+        docinfo = extern
+        charset_type = utf-8
+}
+
+searchd
+{
+        listen                  = 9312
+        pid_file                = /var/run/sphinxsearch/searchd.pid
+}
+

In order to build your search index, you would run indexer searcher. +Obviously this won’t work until you have your web app running. For a production +site, it would make sense to run this command via a cron job so the index +is regularly updated.

+
+
+

Basic Yesod Setup

+

Let’s get our basic Yesod setup going. We’re going to have a single table in +the database for holding documents, which consist of a title and content. We’ll +store this in a SQLite database, and provide routes for searching, adding +documents, viewing documents and providing the xmlpipe file to Sphinx.

+
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlBackend
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where -- see below
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

Hopefully all of this looks pretty familiar by now. The one new thing we’ve +defined here is an instance of YesodPersistRunner. This is a typeclass +necessary for creating streaming database responses. The default implementation +(defaultGetDBRunner) is almost always appropriate.

+

Next we’ll define some forms: one for creating documents, and one for searching:

+
addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+

The True parameter to searchField makes the field auto-focus on page load. +Finally, we have some standard handlers for the homepage (shows the add +document form and the search form), the document display, and adding a +document.

+
getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+
+

Searching

+

Now that we’ve got the boring stuff out of the way, let’s jump into the actual +searching. We’re going to need three pieces of information for displaying a +result: the document ID it comes from, the title of that document, and the +excerpts. Excerpts are the highlighted portions of the document which contain +the search term.

+

Search Result

+ + + + + + +
+

So let’s start off by defining a Result datatype:

+
data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+

Next we’ll look at the search handler:

+
getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+

Nothing magical here, we’re just relying on the searchForm defined above, and +the getResults function which hasn’t been defined yet. This function just +takes a search string, and returns a list of results. This is where we first +interact with the Sphinx API. We’ll be using two functions: query will return +a list of matches, and buildExcerpts will return the highlighted excerpts. +Let’s first look at getResults:

+
getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (toSqlKey . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+

query takes three parameters: the configuration options, the index to search +against (searcher in this case) and the search string. It returns a list of +document IDs that contain the search string. The tricky bit here is that those +documents are returned as Int64 values, whereas we need DocIds. +Fortunately, for the SQL Persist backends, we can just use the toSqlKey +function to perform the conversion.

+ +

We then loop over the resulting IDs to get a [Maybe Result] value, and use +catMaybes to turn it into a [Result]. In the where clause, we define our +local settings, which override the default port and set up the search to work +when any term matches the document.

+

Let’s finally look at the getResult function:

+
getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [escape $ docContent doc]
+        "searcher"
+        qstring
+    let excerpt =
+            case excerpt' of
+                ST.Ok texts -> preEscapedToHtml $ mconcat texts
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+

buildExcerpts takes four parameters: the configuration options, the textual +contents of the document, the search index and the search term. The interesting +bit is that we entity escape the text content. Sphinx won’t automatically +escape these for us, so we must do it explicitly.

+

Similarly, the result from Sphinx is a list of Texts. But of course, we’d +rather have Html. So we concat that list into a single Text and use +preEscapedToHtml to make sure that the tags inserted for matches are not +escaped. A sample of this HTML is:

+
&#8230; Departments.  The President shall have <span class='match'>Power</span> to fill up all Vacancies
+&#8230;  people. Amendment 11 The Judicial <span class='match'>power</span> of the United States shall
+&#8230; jurisdiction. 2. Congress shall have <span class='match'>power</span> to enforce this article by
+&#8230; 5. The Congress shall have <span class='match'>power</span> to enforce, by appropriate legislation
+&#8230;
+
+
+

Streaming xmlpipe output

+

We’ve saved the best for last. For the majority of Yesod handlers, the +recommended approach is to load up the database results into memory and then +produce the output document based on that. It’s simpler to work with, but more +importantly it’s more resilient to exceptions. If there’s a problem loading the +data from the database, the user will get a proper 500 response code.

+ +

However, generating the xmlpipe output is a perfect example of the alternative. +There are potentially a huge number of documents, and documents could easily be +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: +a content type, and a conduit Source providing a stream of blaze-builder +Builders. 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.

+

Now we know we want to create a stream of Builders from some XML content. +Fortunately, the xml-conduit package provides this interface directly. +xml-conduit provides some high-level interfaces for dealing with documents as +a whole, but in our case, we’re going to need to use the low-level Event +interface to ensure minimal memory impact. So the function we’re interested in +is:

+
renderBuilder :: Monad m => RenderSettings -> Conduit Event m Builder
+

In plain English, that means renderBuilder takes some settings (we’ll just use +the defaults), and will then convert a stream of Events to a stream of +Builders. This is looking pretty good, all we need now is a stream of +Events.

+

Speaking of which, what should our XML document actually look like? It’s pretty +simple, we have a sphinx:docset root element, a sphinx:schema element +containing a single sphinx:field (which defines the content field), and then +a sphinx:document for each document in our database. That last element will +have an id attribute and a child content element. Below is an example of +such a document:

+
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">
+    <sphinx:schema>
+        <sphinx:field name="content"/>
+    </sphinx:schema>
+    <sphinx:document id="1">
+        <content>bar</content>
+    </sphinx:document>
+    <sphinx:document id="2">
+        <content>foo bar baz</content>
+    </sphinx:document>
+</sphinx:docset>
+ +

Every document is going to start off with the same events (start the docset, +start the schema, etc) and end with the same event (end the docset). We’ll +start off by defining those:

+
toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+

Now that we have the shell of our document, we need to get the Events for +each individual document. This is actually a fairly simple function:

+
entityToEvents :: Entity Doc -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+

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.

+

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 +also provides the (more powerful) selectSource function. So we end up with +the function:

+
docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+

The $= operator joins together a source and a conduit into a new source. 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 +piece of cake:

+
fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+

Now we need to tie it together in getXmlpipeR. To do so, we’ll use the respondSourceDB function mentioned earlier. The last trick we need to do is convert our stream of Events into a stream of Chunk Builders. Converting to a stream of Builders is achieved with renderBuilder, and finally we’ll just wrap each Builder in its own Chunk:

+
getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+
+

Full code

+
{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# 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)
+import qualified Text.Search.Sphinx                      as S
+import qualified Text.Search.Sphinx.ExcerptConfiguration as E
+import qualified Text.Search.Sphinx.Types                as ST
+import           Text.XML.Stream.Render                  (def, renderBuilder)
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Doc
+    title Text
+    content Textarea
+|]
+
+data Searcher = Searcher
+    { connPool :: ConnectionPool
+    }
+
+mkYesod "Searcher" [parseRoutes|
+/ HomeR GET
+/doc/#DocId DocR GET
+/add-doc AddDocR POST
+/search SearchR GET
+/search/xmlpipe XmlpipeR GET
+|]
+
+instance Yesod Searcher
+
+instance YesodPersist Searcher where
+    type YesodPersistBackend Searcher = SqlBackend
+
+    runDB action = do
+        Searcher pool <- getYesod
+        runSqlPool action pool
+
+instance YesodPersistRunner Searcher where
+    getDBRunner = defaultGetDBRunner connPool
+
+instance RenderMessage Searcher FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+addDocForm :: Html -> MForm Handler (FormResult Doc, Widget)
+addDocForm = renderTable $ Doc
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+
+searchForm :: Html -> MForm Handler (FormResult Text, Widget)
+searchForm = renderDivs $ areq (searchField True) "Query" Nothing
+
+getHomeR :: Handler Html
+getHomeR = do
+    docCount <- runDB $ count ([] :: [Filter Doc])
+    ((_, docWidget), _) <- runFormPost addDocForm
+    ((_, searchWidget), _) <- runFormGet searchForm
+    let docs = if docCount == 1
+                then "There is currently 1 document."
+                else "There are currently " ++ show docCount ++ " documents."
+    defaultLayout
+        [whamlet|
+            <p>Welcome to the search application. #{docs}
+            <form method=post action=@{AddDocR}>
+                <table>
+                    ^{docWidget}
+                    <tr>
+                        <td colspan=3>
+                            <input type=submit value="Add document">
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+        |]
+
+postAddDocR :: Handler Html
+postAddDocR = do
+    ((res, docWidget), _) <- runFormPost addDocForm
+    case res of
+        FormSuccess doc -> do
+            docid <- runDB $ insert doc
+            setMessage "Document added"
+            redirect $ DocR docid
+        _ -> defaultLayout
+            [whamlet|
+                <form method=post action=@{AddDocR}>
+                    <table>
+                        ^{docWidget}
+                        <tr>
+                            <td colspan=3>
+                                <input type=submit value="Add document">
+            |]
+
+getDocR :: DocId -> Handler Html
+getDocR docid = do
+    doc <- runDB $ get404 docid
+    defaultLayout
+        [whamlet|
+            <h1>#{docTitle doc}
+            <div .content>#{docContent doc}
+        |]
+
+data Result = Result
+    { resultId      :: DocId
+    , resultTitle   :: Text
+    , resultExcerpt :: Html
+    }
+
+getResult :: DocId -> Doc -> Text -> IO Result
+getResult docid doc qstring = do
+    excerpt' <- S.buildExcerpts
+        excerptConfig
+        [escape $ docContent doc]
+        "searcher"
+        qstring
+    let excerpt =
+            case excerpt' of
+                ST.Ok texts -> preEscapedToHtml $ mconcat texts
+                _ -> ""
+    return Result
+        { resultId = docid
+        , resultTitle = docTitle doc
+        , resultExcerpt = excerpt
+        }
+  where
+    excerptConfig = E.altConfig { E.port = 9312 }
+
+escape :: Textarea -> Text
+escape =
+    T.concatMap escapeChar . unTextarea
+  where
+    escapeChar '<' = "&lt;"
+    escapeChar '>' = "&gt;"
+    escapeChar '&' = "&amp;"
+    escapeChar c   = T.singleton c
+
+getResults :: Text -> Handler [Result]
+getResults qstring = do
+    sphinxRes' <- liftIO $ S.query config "searcher" qstring
+    case sphinxRes' of
+        ST.Ok sphinxRes -> do
+            let docids = map (toSqlKey . ST.documentId) $ ST.matches sphinxRes
+            fmap catMaybes $ runDB $ forM docids $ \docid -> do
+                mdoc <- get docid
+                case mdoc of
+                    Nothing -> return Nothing
+                    Just doc -> liftIO $ Just <$> getResult docid doc qstring
+        _ -> error $ show sphinxRes'
+  where
+    config = S.defaultConfig
+        { S.port = 9312
+        , S.mode = ST.Any
+        }
+
+getSearchR :: Handler Html
+getSearchR = do
+    ((formRes, searchWidget), _) <- runFormGet searchForm
+    searchResults <-
+        case formRes of
+            FormSuccess qstring -> getResults qstring
+            _ -> return []
+    defaultLayout $ do
+        toWidget
+            [lucius|
+                .excerpt {
+                    color: green; font-style: italic
+                }
+                .match {
+                    background-color: yellow;
+                }
+            |]
+        [whamlet|
+            <form method=get action=@{SearchR}>
+                ^{searchWidget}
+                <input type=submit value=Search>
+            $if not $ null searchResults
+                <h1>Results
+                $forall result <- searchResults
+                    <div .result>
+                        <a href=@{DocR $ resultId result}>#{resultTitle result}
+                        <div .excerpt>#{resultExcerpt result}
+        |]
+
+getXmlpipeR :: Handler TypedContent
+getXmlpipeR =
+    respondSourceDB "text/xml"
+ $  fullDocSource
+ $= renderBuilder def
+ $= CL.map Chunk
+
+entityToEvents :: (Entity Doc) -> [X.Event]
+entityToEvents (Entity docid doc) =
+    [ X.EventBeginElement document [("id", [X.ContentText $ toPathPiece docid])]
+    , X.EventBeginElement content []
+    , X.EventContent $ X.ContentText $ unTextarea $ docContent doc
+    , X.EventEndElement content
+    , X.EventEndElement document
+    ]
+
+fullDocSource :: Source (YesodDB Searcher) X.Event
+fullDocSource = do
+    mapM_ yield startEvents
+    docSource
+    mapM_ yield endEvents
+
+docSource :: Source (YesodDB Searcher) X.Event
+docSource = selectSource [] [] $= CL.concatMap entityToEvents
+
+toName :: Text -> X.Name
+toName x = X.Name x (Just "http://sphinxsearch.com/") (Just "sphinx")
+
+docset, schema, field, document, content :: X.Name
+docset = toName "docset"
+schema = toName "schema"
+field = toName "field"
+document = toName "document"
+content = "content" -- no prefix
+
+startEvents, endEvents :: [X.Event]
+startEvents =
+    [ X.EventBeginDocument
+    , X.EventBeginElement docset []
+    , X.EventBeginElement schema []
+    , X.EventBeginElement field [("name", [X.ContentText "content"])]
+    , X.EventEndElement field
+    , X.EventEndElement schema
+    ]
+
+endEvents =
+    [ X.EventEndElement docset
+    ]
+
+main :: IO ()
+main = runStdoutLoggingT $ withSqlitePool "searcher.db3" 10 $ \pool -> liftIO $ do
+    runSqlPool (runMigration migrateAll) pool
+    warp 3000 $ Searcher pool
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/creating-a-subsite.html b/public/book/creating-a-subsite.html new file mode 100644 index 00000000..8c2feadb --- /dev/null +++ b/public/book/creating-a-subsite.html @@ -0,0 +1,225 @@ + Creating a Subsite :: Yesod Web Framework Book- Version 1.6 +
+

Creating a Subsite

+ + +

How many sites provide authentication systems? Or need to provide +create-read-update-delete (CRUD) management of some objects? Or a blog? Or a +wiki?

+

The theme here is that many websites include common components that can be +reused throughout multiple sites. However, it is often quite difficult to get +code to be modular enough to be truly plug-and-play: a component will require +hooks into the routing system, usually for multiple routes, and will need some +way of sharing styling information with the master site.

+

In Yesod, the solution is subsites. A subsite is a collection of routes and +their handlers that can be easily inserted into a master site. By using type +classes, it is easy to ensure that the master site provides certain +capabilities, and to access the default site layout. And with type-safe URLs, +it’s easy to link from the master site to subsites.

+
+

Hello World

+

Perhaps the trickiest part of writing subsites is getting started. Let’s dive +in with a simple Hello World subsite. We need to create one module to contain +our subsite’s data types, another for the subsite’s dispatch code, and then a +final module for an application that uses the subsite.

+ +
-- @HelloSub/Data.hs
+{-# LANGUAGE QuasiQuotes     #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies    #-}
+module HelloSub.Data where
+
+import           Yesod
+
+-- Subsites have foundations just like master sites.
+data HelloSub = HelloSub
+
+-- We have a familiar analogue from mkYesod, with just one extra parameter.
+-- We'll discuss that later.
+mkYesodSubData "HelloSub" [parseRoutes|
+/ SubHomeR GET
+|]
+
-- @HelloSub.hs
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+module HelloSub
+    ( module HelloSub.Data
+    , module HelloSub
+    ) where
+
+import           HelloSub.Data
+import           Yesod
+
+-- And we'll spell out the handler type signature.
+getSubHomeR :: Yesod master => SubHandlerFor HelloSub master Html
+getSubHomeR = liftHandler $ defaultLayout [whamlet|Welcome to the subsite!|]
+
+instance Yesod master => YesodSubDispatch HelloSub master where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesHelloSub)
+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           HelloSub
+import           Yesod
+
+-- And let's create a master site that calls it.
+data Master = Master
+    { getHelloSub :: HelloSub
+    }
+
+mkYesod "Master" [parseRoutes|
+/ HomeR GET
+/subsite SubsiteR HelloSub getHelloSub
+|]
+
+instance Yesod Master
+
+-- Spelling out type signature again.
+getHomeR :: HandlerFor Master Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>Welcome to the homepage
+        <p>
+            Feel free to visit the #
+            <a href=@{SubsiteR SubHomeR}>subsite
+            \ as well.
+    |]
+
+main = warp 3000 $ Master HelloSub
+

This simple example actually shows most of the complications involved in +creating a subsite. Like a normal Yesod application, everything in a subsite is +centered around a foundation datatype, HelloSub in our case. We then use +mkYesodSubData to generate our subsite route data type and associated parse +and render functions.

+

On the dispatch side, we start off by defining our handler function for the SubHomeR route. You should pay special attention to the type signature on this function:

+
getSubHomeR :: Yesod master
+            => SubHandlerFor HelloSub master Html
+

This is the heart and soul of what a subsite is all about. All of our actions +live in this layered monad, where we have our subsite wrapping around our main +site. Given this monadic layering, it should come as no surprise that we end up +calling liftHandler. In this case, our subsite is using the master site’s +defaultLayout function to render a widget.

+

The defaultLayout function is part of the Yesod typeclass. Therefore, in +order to call it, the master type argument must be an instance of Yesod. +The advantage of this approach is that any modifications to the master site’s +defaultLayout method will automatically be reflected in subsites.

+

When we embed a subsite in our master site route definition, we need to specify +four pieces of information: the route to use as the base of the subsite (in +this case, /subsite), the constructor for the subsite routes (SubsiteR), +the subsite foundation data type (HelloSub) and a function that takes a +master foundation value and returns a subsite foundation value (getHelloSub).

+

In the definition of getHomeR, we can see how the route constructor gets used. +In a sense, SubsiteR promotes any subsite route to a master site route, +making it possible to safely link to it from any master site template.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/deploying-your-webapp.html b/public/book/deploying-your-webapp.html new file mode 100644 index 00000000..e117cadb --- /dev/null +++ b/public/book/deploying-your-webapp.html @@ -0,0 +1,581 @@ + Deploying your Webapp :: Yesod Web Framework Book- Version 1.6 +
+

Deploying your Webapp

+ + +

I can’t speak for others, but I personally prefer programming to system +administration. But the fact is that, eventually, you need to serve your app +somehow, and odds are that you’ll need to be the one to set it up.

+

There are some promising initiatives in the Haskell web community towards +making deployment easier. In the future, we may even have a service that allows +you to deploy your app with a single command.

+

But we’re not there yet. And even if we were, such a solution will never work +for everyone. This chapter covers the different options you have for +deployment, and gives some general recommendations on what you should choose in +different situations.

+

The Yesod team strongly recommends using the stack build tool as discussed in +the quick start guide for Yesod +development, check out the quick start if you haven’t already.

+
+

Keter

+

The Yesod scaffolding comes with some built-in support for the Keter deployment +engine, which is also written in Haskell and uses many of the same underlying +technologies like WAI and http-client. Keter works as a reverse proxy to your +applications, as well as a system for starting, monitoring, and redeploying +running apps. If you’d like to deploy with Keter, follow these steps:

+
    +
  1. +

    +Edit the config/keter.yaml file in your scaffolded application as necessary. +

    +
  2. +
  3. +

    +Set up some kind of server for hosting your apps. I recommend trying Ubuntu on Amazon EC2. +

    +
  4. +
  5. +

    +Install Keter on that machine. Please follow the instructions on the Keter website, which will be the most up to date. +

    +
  6. +
  7. +

    +Run yesod keter to generate a Keter bundle, e.g., myapp.keter. +

    +
  8. +
  9. +

    +Copy myapp.keter to the /opt/keter/incoming directory on your server. +

    +
  10. +
+

If you’ve got things configured correctly, you should now be able to view +your website, running in a production environment! In the future, upgrades can +be handled by simply rerunning yesod keter and recopying the myapp.keter +bundle to the server. Note that Keter will automatically detect the presence of +the new file and reload your application.

+

The rest of this chapter will go into some more details about various steps, and +provide some alternatives for people looking to either not use the scaffolding +or not use Keter.

+
+
+

Compiling

+

The biggest advice I can give is: don’t compile on your server. It’s tempting to do so, as you have to just transfer source code around, and you avoid confusing dependency issues. However, compiling a Yesod application takes significant memory and CPU, which means:

+
    +
  • +

    +While you’re recompiling your app, your existing application will suffer performance-wise. +

    +
  • +
  • +

    +You will need to get a much larger machine to handle compilation, and that capacity will likely sit idle most of the time, since Yesod applications tend to require far less CPU and memory than GHC itself. +

    +
  • +
+

Once you’re ready to compile, you should always make sure to stack clean +before a new production build, to make sure no old files are lying around. +Then, you can run stack build to get an executable, which +will be located at dist/build/myapp/myapp.

+
+
+

Files to deploy

+

With a Yesod scaffolded application, there are essentially three sets of files that need +to be deployed:

+
    +
  1. +

    +Your executable. +

    +
  2. +
  3. +

    +The config folder. +

    +
  4. +
  5. +

    +The static folder. +

    +
  6. +
+

Everything else, such as Shakespearean templates, gets compiled into the +executable itself.

+

There is one caveat, however: the config/client_session_key.aes file. This +file controls the server side encryption used for securing client side session +cookies. Yesod will automatically generate a new one of these keys if none is +present. In practice, this means that, if you do not include this file in +deployment, all of your users will have to log in again when you redeploy. If +you follow the advice above and include the config folder, this issue will be +partially resolved. Another approach is to +put +your session key in an environment variable.

+

The other half of the resolution is to ensure that once you generate a +config/client_session_key.aes file, you keep the same one for all future +deployments. The simplest way to ensure this is to keep that file in your +version control. However, if your version control is open source, this will be +dangerous: anyone with access to your repository will be able to spoof login +credentials!

+

The problem described here is essentially one of system administration, not +programming. Yesod does not provide any built-in approach for securely storing +client session keys. If you have an open source repository, or do not trust +everyone who has access to your source code repository, it’s vital to figure +out a safe storage solution for the client session key.

+
+
+

SSL and static files

+

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 +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:

+
    +
  • +

    +Serve your static files over HTTPS as well. +

    +
  • +
  • +

    +Serve your static files from the same domain name as your main site. +

    +
  • +
+

Note that if you go for the first option (which is the better one), you’ll +either need two separate SSL certificates, or a wildcard certificate.

+
+
+

Warp

+

As we have mentioned before, Yesod is built on the Web Application Interface +(WAI), allowing it to run on any WAI backend. At the time of writing, the +following backends are available:

+
    +
  • +

    +Warp +

    +
  • +
  • +

    +FastCGI +

    +
  • +
  • +

    +SCGI +

    +
  • +
  • +

    +CGI +

    +
  • +
  • +

    +Webkit +

    +
  • +
  • +

    +Development server +

    +
  • +
+

The last two are not intended for production deployments. Of the remaining +four, all can be used for production deployment in theory. In practice, a CGI +backend will likely be horribly inefficient, since a new process must be +spawned for each connection. And SCGI is not nearly as well supported by +frontend web servers as Warp (via reverse proxying) or FastCGI.

+

So between the two remaining choices, Warp gets a very strong recommendation +because:

+
    +
  • +

    +It is significantly faster. +

    +
  • +
  • +

    +Like FastCGI, it can run behind a frontend server like Nginx, using reverse + HTTP proxy. +

    +
  • +
  • +

    +In addition, it is a fully capable server of its own accord, and can + therefore be used without any frontend server. +

    +
  • +
+

So that leaves one last question: should Warp run on its own, or via reverse +proxy behind a frontend server? For most use cases, I recommend the latter, +because:

+
    +
  • +

    +Having a reverse proxy in front of your app makes it easier to deploy new versions. +

    +
  • +
  • +

    +Also, if you have a bug in your application, a reverse proxy can give slightly nicer error messages to users. +

    +
  • +
  • +

    +You can host multiple applications from a single host via virtual hosting. +

    +
  • +
  • +

    +Your reverse proxy can function as both a load balancer or SSL proxy as well, simplifying your application. +

    +
  • +
+

As discussed above, Keter is a great way to get started. If you have an +existing web server running like Nginx, Yesod will work just fine sitting +behind it instead.

+
+

Nginx Configuration

+

Keter configuration is trivial, since it is designed to work with Yesod +applications. But if you want to instead use Nginx, how do you set it up?

+

In general, Nginx will listen on port 80 and your Yesod/Warp app will listen on +some unprivileged port (let’s say 4321). You will then need to provide a +nginx.conf file, such as:

+
daemon off; # Don't run nginx in the background, good for monitoring apps
+events {
+    worker_connections 4096;
+}
+
+http {
+    server {
+        listen 80; # Incoming port for Nginx
+        server_name www.myserver.com;
+        location / {
+            proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+        }
+    }
+}
+

You can add as many server blocks as you like. A common addition is to ensure +users always access your pages with the www prefix on the domain name, ensuring +the RESTful principle of canonical URLs. (You could just as easily do the +opposite and always strip the www, just make sure that your choice is reflected +in both the nginx config and the approot of your site.) In this case, we would +add the block:

+
server {
+    listen 80;
+    server_name myserver.com;
+    rewrite ^/(.*) http://www.myserver.com/$1 permanent;
+}
+

A highly recommended optimization is to serve static files from a separate +domain name, therefore bypassing the cookie transfer overhead. Assuming that +our static files are stored in the static folder within our site folder, and +the site folder is located at /home/michael/sites/mysite, this would look +like:

+
server {
+    listen 80;
+    server_name static.myserver.com;
+    root /home/michael/sites/mysite/static;
+    # Since yesod-static appends a content hash in the query string,
+    # we are free to set expiration dates far in the future without
+    # concerns of stale content.
+    expires max;
+}
+

In order for this to work, your site must properly rewrite static URLs to this +alternate domain name. The scaffolded site is set up to make this fairly simple +via the Settings.staticRoot function and the definition of +urlRenderOverride. However, if you just want to get the benefit of nginx’s +faster static file serving without dealing with separate domain names, you can +instead modify your original server block like so:

+
server {
+    listen 80; # Incoming port for Nginx
+    server_name www.myserver.com;
+    location / {
+        proxy_pass http://127.0.0.1:4321; # Reverse proxy to your Yesod app
+    }
+    location /static {
+        root /home/michael/sites/mysite; # Notice that we do *not* include /static
+        expires max;
+    }
+}
+
+
+

Server Process

+

Many people are familiar with an Apache/mod_php or Lighttpd/FastCGI kind of +setup, where the web server automatically spawns the web application. With +nginx, either for reverse proxying or FastCGI, this is not the case: you are +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 +/etc/systemd/system/yesod-sample.service:

+
[Service]
+ExecStart=/home/sibi/.local/bin/my-yesod-executable
+Restart=always
+StandardOutput=syslog
+StandardError=syslog
+SyslogIdentifier=yesod-sample
+
+[Install]
+WantedBy=multi-user.target
+

Now you can start your service with:

+
systemctl enable yesod-sample
+systemctl start yesod-sample
+

You can also see the status of your process using systemctl status +yesod-sample.

+
+
+
+

Nginx + FastCGI

+

Some people may prefer using FastCGI for deployment. In this case, you’ll need +to add an extra tool to the mix. FastCGI works by receiving new connection from +a file descriptor. The C library assumes that this file descriptor will be 0 +(standard input), so you need to use the spawn-fcgi program to bind your +application’s standard input to the correct socket.

+

It can be very convenient to use Unix named sockets for this instead of binding +to a port, especially when hosting multiple applications on a single host. A +possible script to load up your app could be:

+
spawn-fcgi \
+    -d /home/michael/sites/mysite \
+    -s /tmp/mysite.socket \
+    -n \
+    -M 511 \
+    -u michael \
+    -- /home/michael/sites/mysite/dist/build/mysite-fastcgi/mysite-fastcgi
+

You will also need to configure your frontend server to speak to your app over +FastCGI. This is relatively painless in Nginx:

+
server {
+    listen 80;
+    server_name www.myserver.com;
+    location / {
+        fastcgi_pass unix:/tmp/mysite.socket;
+    }
+}
+

That should look pretty familiar from above. The only last trick is that, with +Nginx, you need to manually specify all of the FastCGI variables. It is +recommended to store these in a separate file (say, fastcgi.conf) and then add +include fastcgi.conf; to the end of your http block. The contents of the +file, to work with WAI, should be:

+
fastcgi_param  QUERY_STRING       $query_string;
+fastcgi_param  REQUEST_METHOD     $request_method;
+fastcgi_param  CONTENT_TYPE       $content_type;
+fastcgi_param  CONTENT_LENGTH     $content_length;
+fastcgi_param  PATH_INFO          $fastcgi_script_name;
+fastcgi_param  SERVER_PROTOCOL    $server_protocol;
+fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
+fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
+fastcgi_param  REMOTE_ADDR        $remote_addr;
+fastcgi_param  SERVER_ADDR        $server_addr;
+fastcgi_param  SERVER_PORT        $server_port;
+fastcgi_param  SERVER_NAME        $server_name;
+
+
+

Desktop

+

Another nifty backend is wai-handler-webkit. This backend combines Warp and +QtWebkit to create an executable that a user simply double-clicks. This can be +a convenient way to provide an offline version of your application.

+

One of the very nice conveniences of Yesod for this is that your templates are +all compiled into the executable, and thus do not need to be distributed with +your application. Static files do, however.

+ +

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 +doesn’t receive a ping for two minutes, it shuts down.

+
+
+

CGI on Apache

+

CGI and FastCGI work almost identically on Apache, so it should be fairly +straight-forward to port this configuration. You essentially need to accomplish +two goals:

+
    +
  1. +

    +Get the server to serve your file as (Fast)CGI. +

    +
  2. +
  3. +

    +Rewrite all requests to your site to go through the (Fast)CGI executable. +

    +
  4. +
+

Here is a configuration file for serving a blog application, with an executable +named "bloggy.cgi", living in a subfolder named "blog" of the document root. +This example was taken from an application living in the path +/f5/snoyman/public/blog.

+
Options +ExecCGI
+AddHandler cgi-script .cgi
+Options +FollowSymlinks
+
+RewriteEngine On
+RewriteRule ^/f5/snoyman/public/blog$ /blog/ [R=301,S=1]
+RewriteCond $1 !^bloggy.cgi
+RewriteCond $1 !^static/
+RewriteRule ^(.*) bloggy.cgi/$1 [L]
+

The first RewriteRule is to deal with subfolders. In particular, it redirects a +request for /blog to /blog/. The first RewriteCond prevents directly +requesting the executable, the second allows Apache to serve the static files, +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." +when absent and serves static files from a separate domain. However, this +should serve to show the basics.

+

Here, "/home/michael/fastcgi" is the fastcgi application. The idea is to +rewrite all requests to start with "/app", and then serve everything beginning +with "/app" via the FastCGI executable.

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_fastcgi", "mod_rewrite")
+
+url.rewrite-once = (
+  "(.*)" => "/app/$1"
+)
+
+fastcgi.server = (
+    "/app" => ((
+        "socket" => "/tmp/test.fastcgi.socket",
+        "check-local" => "disable",
+        "bin-path" => "/home/michael/fastcgi", # full path to executable
+        "min-procs" => 1,
+        "max-procs" => 30,
+        "idle-timeout" => 30
+    ))
+)
+
+
+

CGI on lighttpd

+

This is basically the same as the FastCGI version, but tells lighttpd to run a +file ending in ".cgi" as a CGI executable. In this case, the file lives at +"/home/michael/myapp.cgi".

+
server.port = 3000
+server.document-root = "/home/michael"
+server.modules = ("mod_cgi", "mod_rewrite")
+
+url.rewrite-once = (
+    "(.*)" => "/myapp.cgi/$1"
+)
+
+cgi.assign = (".cgi" => "")
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/environment-variables.html b/public/book/environment-variables.html new file mode 100644 index 00000000..fbf45af5 --- /dev/null +++ b/public/book/environment-variables.html @@ -0,0 +1,171 @@ + Environment variables for configuration :: Yesod Web Framework Book- Version 1.6 +
+

Environment variables for configuration

+ + +

There’s a recent move, perhaps most prominently advocated by +the twelve-factor app, to store all app +configuration in environment variables, instead of using config files or +hard-coding them into the application source code (you don’t do that, right?).

+

Yesod’s scaffolding comes built in with some support for this, most +specifically for respecting the YESOD_APPROOT environment variable to indicate how +URLs should be generated, the YESOD_PORT environment variable for which port to +listen for requests on, and database connection settings. (Incidentally, this +ties in nicely with the Keter deployment +manager.)

+

The technique for doing this is quite easy: just do the environment variable +lookup in your main function. This example demonstrates this technique, along +with the slightly special handling necessary for setting the application root.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text          (Text, pack)
+import           System.Environment
+import           Yesod
+
+data App = App
+    { myApproot      :: Text
+    , welcomeMessage :: Text
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    approot = ApprootMaster myApproot
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    App {..} <- getYesod
+    setTitle "Environment variables"
+    [whamlet|
+        <p>Here's the welcome message: #{welcomeMessage}
+        <p>
+            <a href=@{HomeR}>And a link to: @{HomeR}
+    |]
+
+main :: IO ()
+main = do
+    myApproot <- fmap pack $ getEnv "YESOD_APPROOT"
+    welcomeMessage <- fmap pack $ getEnv "WELCOME_MESSAGE"
+    warp 3000 App {..}
+

The only tricky things here are:

+
    +
  1. +

    +You need to convert the String value returned by getEnv into a Text by using pack. +

    +
  2. +
  3. +

    +We use the ApprootMaster constructor for approot, which says "apply this function to the foundation datatype to get the actual application root." +

    +
  4. +
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/forms.html b/public/book/forms.html new file mode 100644 index 00000000..cc59642c --- /dev/null +++ b/public/book/forms.html @@ -0,0 +1,1127 @@ + Forms :: Yesod Web Framework Book- Version 1.6 +
+

Forms

+ + +

I’ve mentioned the boundary issue already: whenever data enters or leaves an +application, we need to validate it. Probably the most difficult place this +occurs is forms. Coding forms is complex; in an ideal world, we’d like a +solution that addresses the following problems:

+
    +
  • +

    +Ensure data is valid. +

    +
  • +
  • +

    +Marshal string data in the form submission to Haskell datatypes. +

    +
  • +
  • +

    +Generate HTML code for displaying the form. +

    +
  • +
  • +

    +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. +

    +
  • +
  • +

    +Automatically assign names to our fields that are guaranteed to be unique. +

    +
  • +
+

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 +Haskell’s type system to make sure everything is working correctly.

+
+

Synopsis

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative ((<$>), (<*>))
+import           Data.Text           (Text)
+import           Data.Time           (Day)
+import           Yesod
+import           Yesod.Form.Jquery
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/person PersonR POST
+|]
+
+instance Yesod App
+
+-- Tells our application to use the standard English messages.
+-- If you want i18n, then you can supply a translating function instead.
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- And tell us where to find the jQuery libraries. We'll just use the defaults,
+-- which point to the Google CDN.
+instance YesodJquery App
+
+-- The datatype we wish to receive from the form
+data Person = Person
+    { personName          :: Text
+    , personBirthday      :: Day
+    , personFavoriteColor :: Maybe Text
+    , personEmail         :: Text
+    , personWebsite       :: Maybe Text
+    }
+  deriving Show
+
+-- Declare the form. The type signature is a bit intimidating, but here's the
+-- overview:
+--
+-- * The Html parameter is used for encoding some extra information. See the
+-- discussion regarding runFormGet and runFormPost below for further
+-- explanation.
+--
+-- * We have our Handler as the inner monad, which indicates which site this is
+-- running in.
+--
+-- * FormResult can be in three states: FormMissing (no data available),
+-- FormFailure (invalid data) and FormSuccess
+--
+-- * The Widget is the viewable form to place into the web page.
+--
+-- Note that the scaffolded site provides a convenient Form type synonym,
+-- so that our signature could be written as:
+--
+-- > personForm :: Form Person
+--
+-- For our purposes, it's good to see the long version.
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm = renderDivs $ Person
+    <$> areq textField "Name" Nothing
+    <*> areq (jqueryDayField def
+        { jdsChangeYear = True -- give a year dropdown
+        , jdsYearRange = "1900:-5" -- 1900 till five years ago
+        }) "Birthday" Nothing
+    <*> aopt textField "Favorite color" Nothing
+    <*> areq emailField "Email address" Nothing
+    <*> aopt urlField "Website" Nothing
+
+-- The GET handler displays the form
+getHomeR :: Handler Html
+getHomeR = do
+    -- Generate the form to be displayed
+    (widget, enctype) <- generateFormPost personForm
+    defaultLayout
+        [whamlet|
+            <p>
+                The widget generated contains only the contents
+                of the form, not the form tag itself. So...
+            <form method=post action=@{PersonR} enctype=#{enctype}>
+                ^{widget}
+                <p>It also doesn't include the submit button.
+                <button>Submit
+        |]
+
+-- The POST handler processes the form. If it is successful, it displays the
+-- parsed person. Otherwise, it displays the form again with error messages.
+postPersonR :: Handler Html
+postPersonR = do
+    ((result, widget), enctype) <- runFormPost personForm
+    case result of
+        FormSuccess person -> defaultLayout [whamlet|<p>#{show person}|]
+        _ -> defaultLayout
+            [whamlet|
+                <p>Invalid input, let's try again.
+                <form method=post action=@{PersonR} enctype=#{enctype}>
+                    ^{widget}
+                    <button>Submit
+            |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Kinds of Forms

+

Before jumping into the types themselves, we should begin with an overview of +the different kinds of forms. There are three categories:

+
+
+Applicative +
+

+These are the most commonly used (it’s what appeared in the +synopsis). Applicative gives us some nice properties of letting error messages +coalesce together and keep a very high-level, declarative approach. (For more +information on applicative code, see +the Haskell +wiki.) +

+
+
+Monadic +
+

+A more powerful alternative to applicative. While this allows you +more flexibility, it does so at the cost of being more verbose. Useful if you +want to create forms that don’t fit into the standard two-column look. +

+
+
+Input +
+

+Used only for receiving input. Does not generate any HTML for receiving +the user input. Useful for interacting with existing forms. +

+
+
+

In addition, there are a number of different variables that come into play for +each form and field you will want to set up:

+
    +
  • +

    +Is the field required or optional? +

    +
  • +
  • +

    +Should it be submitted with GET or POST? +

    +
  • +
  • +

    +Does it have a default value, or not? +

    +
  • +
+

An overriding goal is to minimize the number of field definitions and let them +work in as many contexts as possible. One result of this is that we end up with +a few extra words for each field. In the synopsis, you may have noticed things +like areq and that extra Nothing parameter. We’ll cover why all of those +exist in the course of this chapter, but for now realize that by making these +parameters explicit, we are able to reuse the individuals fields (like +intField) in many different ways.

+

A quick note on naming conventions. Each form type has a one-letter prefix (A, +M and I) which is used in a few places, such as saying MForm. We also use req +and opt to mean required and optional. Combining these, we create a required +applicative field with areq, or an optional input field with iopt.

+
+
+

Types

+

The Yesod.Form.Types module declares a few types. We won’t cover all the types +available, but will instead focus on the most crucial. Let’s start with some of +the simple ones:

+
+
+Enctype +
+

+The encoding type, either UrlEncoded or Multipart. This datatype +declares an instance of ToHtml, so you can use the enctype directly in +Hamlet. +

+
+
+FormResult +
+

+Has one of three possible states: FormMissing if no data was +submitted, FormFailure if there was an error parsing the form (e.g., missing +a required field, invalid content), or FormSuccess if everything went +smoothly. +

+
+
+FormMessage +
+

+Represents all of the different messages that can be generated as +a data type. For example, MsgInvalidInteger is used by the library to +indicate that the textual value provided is not an integer. By keeping this +data highly structured, you are able to provide any kind of rendering function +you want, which allows for internationalization (i18n) of your application. +

+
+
+

Next we have some datatypes used for defining individual fields. We define a +field as a single piece of information, such as a number, a string, or an email +address. Fields are combined together to build forms.

+
+
+Field +
+

+Defines two pieces of functionality: how to parse the text input from a +user into a Haskell value, and how to create the widget to be displayed to the +user. yesod-form defines a number of individual Fields in Yesod.Form.Fields. +

+
+
+FieldSettings +
+

+Basic information on how a field should be displayed, such as +the display name, an optional tooltip, and possibly hardcoded id and name +attributes. (If none are provided, they are automatically generated.) Note that +FieldSettings provides an IsString instance, so when you need to provide a +FieldSettings value, you can actually type in a literal string. That’s how we +interacted with it in the synopsis. +

+
+
+

And finally, we get to the important stuff: the forms themselves. There are +three types for this: MForm is for monadic forms, AForm for applicative and +FormInput for input. MForm is actually a type synonym for a +monad stack that provides the following features:

+
    +
  • +

    +A Reader monad giving us the parameters submitted by the user, the + foundation datatype and the list of languages the user supports. The last two + are used for rendering of the FormMessages to support i18n (more on this + later). +

    +
  • +
  • +

    +A Writer monad keeping track of the Enctype. A form will always be + UrlEncoded, unless there is a file input field, which will force us to use + multipart instead. +

    +
  • +
  • +

    +A State monad keeping track of generated names and identifiers for fields. +

    +
  • +
+

An AForm is pretty similar. However, there are a few major differences:

+
    +
  • +

    +It produces a list of FieldViews, which are used for tracking what we + will display to the user. This allows us to keep an abstract idea of the form + display, and then at the end of the day choose an appropriate function for + laying it out on the page. In the synopsis, we used renderDivs, which + creates a bunch of div tags. Two other options are renderBootstrap and + renderTable. +

    +
  • +
  • +

    +It does not provide a Monad instance. The goal of Applicative is to allow + the entire form to run, grab as much information on each field as possible, + and then create the final result. This cannot work in the context of Monad. +

    +
  • +
+

A FormInput is even simpler: it returns either a list of error messages or a +result.

+
+
+

Converting

+

“But wait a minute,” you say. “You said the synopsis uses applicative forms, +but I’m sure the type signature said MForm. Shouldn’t it be Monadic?” That’s +true, the final form we produced was monadic. But what really happened is that +we converted an applicative form to a monadic one.

+

Again, our goal is to reuse code as much as possible, and minimize the number +of functions in the API. And Monadic forms are more powerful than Applicative, +if a bit clumsy, so anything that can be expressed in an Applicative form could +also be expressed in a Monadic form. There are two core functions that help out +with this: aformToForm converts any applicative form to a monadic one, and +formToAForm converts certain kinds of monadic forms to applicative forms.

+

“But wait another minute,” you insist. “I didn’t see any aformToForm!” +Also true. The renderDivs function takes care of that for us.

+
+
+

Create AForms

+

Now that I’ve (hopefully) convinced you that in our synopsis we were really +dealing with applicative forms, let’s have a look and try to understand how +these things get created. Let’s take a simple example:

+
data Car = Car
+    { carModel :: Text
+    , carYear  :: Int
+    }
+  deriving Show
+
+carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable carAForm
+

Here, we’ve explicitly split up applicative and monadic forms. In carAForm, +we use the <$> and <*> operators. This should not be surprising; these are +almost always used in applicative-style code. And we have one line for each +record in our Car datatype. Perhaps also unsurprisingly, we have a +textField for the Text record, and an intField for the Int record.

+

Let’s look a bit more closely at the areq function. Its (simplified) type +signature is Field a → FieldSettings → Maybe a → AForm a. That +first argument specifies the datatype of this field, how to parse +it, and how to render it. The next argument, FieldSettings, tells us the +label, tooltip, name and ID of the field. In this case, we’re using the +previously-mentioned IsString instance of FieldSettings.

+

And what’s up with that Maybe a? It provides the optional default value. For +example, if we want our form to fill in "2007" as the default car year, we +would use areq intField "Year" (Just 2007). We can even take this to the next +level, and have a form that takes an optional parameter giving the default +values.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+
+

Optional fields

+

Suppose we wanted to have an optional field (like the car color). All we do +instead is use the aopt function.

+
carAForm :: AForm Handler Car
+carAForm = Car
+    <$> areq textField "Model" Nothing
+    <*> areq intField "Year" Nothing
+    <*> aopt textField "Color" Nothing
+

And like required fields, the last argument is the optional default value. +However, this has two layers of Maybe wrapping. This is actually a bit +redundant, but it makes it much easier to write code that takes an optional +default form parameter, such as in the next example.

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq intField  "Year"  (carYear  <$> mcar)
+    <*> aopt textField "Color" (carColor <$> mcar)
+
+carForm :: Html -> MForm Handler (FormResult Car, Widget)
+carForm = renderTable $ carAForm $ Just $ Car "Forte" 2010 $ Just "gray"
+
+
+
+

Validation

+

How would we make our form only accept cars created after 1990? If you +remember, we said above that the Field itself contained the information on +what is a valid entry. So all we need to do is write a new Field, right? +Well, that would be a bit tedious. Instead, let’s just modify an existing one:

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField    "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year"  (carYear  <$> mcar)
+    <*> aopt textField    "Color" (carColor <$> mcar)
+  where
+    errorMessage :: Text
+    errorMessage = "Your car is too old, get a new one!"
+
+    carYearField = check validateYear intField
+
+    validateYear y
+        | y < 1990 = Left errorMessage
+        | otherwise = Right y
+

The trick here is the check function. It takes a function (validateYear) +that returns either an error message or a modified field value. In this +example, we haven’t modified the value at all. That is usually going to be the +case. This kind of checking is very common, so we have a shortcut:

+
carYearField = checkBool (>= 1990) errorMessage intField
+

checkBool takes two parameters: a condition that must be fulfilled, and an +error message to be displayed if it was not.

+ +

It’s great to make sure the car isn’t too old. But what if we want to make sure +that the year specified is not from the future? In order to look up the current +year, we’ll need to run some IO. For such circumstances, we’ll need checkM, +which allows our validation code to perform arbitrary actions:

+
    carYearField = checkM inPast $ checkBool (>= 1990) errorMessage intField
+
+    inPast y = do
+        thisYear <- liftIO getCurrentYear
+        return $ if y <= thisYear
+            then Right y
+            else Left ("You have a time machine!" :: Text)
+
+getCurrentYear :: IO Int
+getCurrentYear = do
+    now <- getCurrentTime
+    let today = utctDay now
+    let (year, _, _) = toGregorian today
+    return $ fromInteger year
+

inPast is a function that will return an Either result in the Handler +monad. We use liftIO getCurrentYear to get the current year and then compare +it against the user-supplied year. Also, notice how we can chain together +multiple validators.

+ +
+
+

More sophisticated fields

+

Our color entry field is nice, but it’s not exactly user-friendly. What we +really want is a drop-down list.

+
data Car = Car
+    { carModel :: Text
+    , carYear :: Int
+    , carColor :: Maybe Color
+    }
+  deriving Show
+
+data Color = Red | Blue | Gray | Black
+    deriving (Show, Eq, Enum, Bounded)
+
+carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectFieldList colors) "Color" (carColor <$> mcar)
+  where
+    colors :: [(Text, Color)]
+    colors = [("Red", Red), ("Blue", Blue), ("Gray", Gray), ("Black", Black)]
+

selectFieldList takes a list of pairs. The first item in the pair is the text displayed to the user in the drop-down list, and the second item is the actual Haskell value. Of course, the code above looks really repetitive; we can get the same result using the Enum and Bounded instance GHC automatically derives for us.

+
colors = map (pack . show &&& id) [minBound..maxBound]
+

[minBound..maxBound] gives us a list of all the different Color values. We +then apply a map and &&& (a.k.a, the fan-out operator) to turn that into a +list of pairs. And even this can be simplified by using the optionsEnum +function provided by yesod-form, which would turn our original code into:

+
carAForm :: Maybe Car -> AForm Handler Car
+carAForm mcar = Car
+    <$> areq textField "Model" (carModel <$> mcar)
+    <*> areq carYearField "Year" (carYear <$> mcar)
+    <*> aopt (selectField optionsEnum) "Color" (carColor <$> mcar)
+

Some people prefer radio buttons to drop-down lists. Fortunately, this is just a one-word change.

+
carAForm = Car
+    <$> areq textField                    "Model" Nothing
+    <*> areq intField                     "Year"  Nothing
+    <*> aopt (radioField optionsEnum) "Color" Nothing
+
+
+

Running forms

+

At some point, we’re going to need to take our beautiful forms and produce some +results. There are a number of different functions available for this, each +with its own purpose. I’ll go through them, starting with the most common.

+
+
+runFormPost +
+

+This will run your form against any submitted POST parameters. +If this is not a POST submission, it will return a FormMissing. This +automatically inserts a security token as a hidden form field to avoid +cross-site request +forgery (CSRF) attacks. +

+
+
+runFormGet +
+

+The equivalent of runFormPost for GET parameters. In order to +distinguish a normal GET page load from a GET submission, it includes an +extra _hasdata hidden field in the form. Unlike runFormPost, it does +not include CSRF protection. +

+
+
+runFormPostNoToken +
+

+Same as runFormPost, but does not include (or require) +the CSRF security token. +

+
+
+generateFormPost +
+

+Instead of binding to existing POST parameters, acts as if +there are none. This can be useful when you want to generate a new form after a +previous form was submitted, such as in a wizard. +

+
+
+generateFormGet +
+

+Same as generateFormPost, but for GET. +

+
+
+

The return type from the first three is ((FormResult a, Widget), Enctype). +The Widget will already have any validation errors and previously submitted +values.

+ +
+
+

i18n

+

There have been a few references to i18n in this chapter. The topic will get +more thorough coverage in its own chapter, but since it has such a profound +effect on yesod-form, I wanted to give a brief overview. The idea behind i18n +in Yesod is to have data types represent messages. Each site can have an +instance of RenderMessage for a given datatype which will translate that +message based on a list of languages the user accepts. As a result of all this, +there are a few things you should be aware of:

+
    +
  • +

    +There is an automatic instance of RenderMessage for Text in every site, + so you can just use plain strings if you don’t care about i18n support. + However, you may need to use explicit type signatures occasionally. +

    +
  • +
  • +

    +yesod-form expresses all of its messages in terms of the FormMessage datatype. Therefore, to use yesod-form, you’ll need to have an appropriate RenderMessage instance. A simple one that uses the default English translations would be: +

    +
  • +
+
instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+

This is provided automatically by the scaffolded site.

+
+
+

Monadic Forms

+

Often times, a simple form layout is adequate, and applicative forms excel at +this approach. Sometimes, however, you’ll want to have a more customized look +to your form.

+

A non-standard form layout

+ + + + + + +
+

For these use cases, monadic forms fit the bill. They are a bit more verbose +than their applicative cousins, but this verbosity allows you to have complete +control over what the form will look like. In order to generate the form above, +we could code something like this.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+personForm :: Html -> MForm Handler (FormResult Person, Widget)
+personForm extra = do
+    (nameRes, nameView) <- mreq textField "this is not used" Nothing
+    (ageRes, ageView) <- mreq intField "neither is this" Nothing
+    let personRes = Person <$> nameRes <*> ageRes
+    let widget = do
+            toWidget
+                [lucius|
+                    ##{fvId ageView} {
+                        width: 3em;
+                    }
+                |]
+            [whamlet|
+                #{extra}
+                <p>
+                    Hello, my name is #
+                    ^{fvInput nameView}
+                    \ and I am #
+                    ^{fvInput ageView}
+                    \ years old. #
+                    <input type=submit value="Introduce myself">
+            |]
+    return (personRes, widget)
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet personForm
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+        |]
+
+main :: IO ()
+main = warp 3000 App
+

Similar to the applicative areq, we use mreq for monadic forms. (And yes, +there’s also mopt for optional fields.) But there’s a big difference: mreq +gives us back a pair of values. Instead of hiding away the FieldView value and +automatically inserting it into a widget, we have the ability to insert it as +we see fit.

+

FieldView has a number of pieces of information. The most important is +fvInput, which is the actual form field. In this example, we also use fvId, +which gives us back the HTML id attribute of the input tag. In our example, +we use that to specify the width of the field.

+

You might be wondering what the story is with the “this is not used” and +“neither is this” values. mreq takes a FieldSettings as its second +argument. Since FieldSettings provides an IsString instance, the strings +are essentially expanded by the compiler to:

+
fromString "this is not used" == FieldSettings
+    { fsLabel = "this is not used"
+    , fsTooltip = Nothing
+    , fsId = Nothing
+    , fsName = Nothing
+    , fsAttrs = []
+    }
+

In the case of applicative forms, the fsLabel and fsTooltip values are used +when constructing your HTML. In the case of monadic forms, Yesod does not +generate any of the “wrapper” HTML for you, and therefore these values are +ignored. However, we still keep the FieldSettings parameter to allow you to +override the id and name attributes of your fields if desired.

+

The other interesting bit is the extra value. GET forms include an extra +field to indicate that they have been submitted, and POST forms include a +security token to prevent CSRF attacks. If you don’t include this extra hidden +field in your form, the form submission will fail.

+

Other than that, things are pretty straight-forward. We create our personRes +value by combining together the nameRes and ageRes values, and then return +a tuple of the person and the widget. And in the getHomeR function, +everything looks just like an applicative form. In fact, you could swap out our +monadic form with an applicative one and the code would still work.

+
+
+

Input forms

+

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 +forms.

+

These work mostly the same as applicative and monadic forms, with some differences:

+
    +
  • +

    +You use runInputPost and runInputGet. +

    +
  • +
  • +

    +You use ireq and iopt. These functions now only take two arguments: the + field type and the name (i.e., HTML name attribute) of the field in + question. +

    +
  • +
  • +

    +After running a form, it returns the value. It doesn’t return a widget or an + encoding type. +

    +
  • +
  • +

    +If there are any validation errors, the page returns an "invalid arguments" + error page. +

    +
  • +
+

You can use input forms to recreate the previous example. Note, however, that +the input version is less user friendly. If you make a mistake in an +applicative or monadic form, you will be brought back to the same page, with +your previously entered values in the form, and an error message explaining what +you need to correct. With input forms, the user simply gets an error message.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/input InputR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+    deriving Show
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form action=@{InputR}>
+            <p>
+                My name is
+                <input type=text name=name>
+                and I am
+                <input type=text name=age>
+                years old.
+                <input type=submit value="Introduce myself">
+    |]
+
+getInputR :: Handler Html
+getInputR = do
+    person <- runInputGet $ Person
+                <$> ireq textField "name"
+                <*> ireq intField "age"
+    defaultLayout [whamlet|<p>#{show person}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Custom fields

+

The fields that come built-in with Yesod will likely cover the vast majority of +your form needs. But occasionally, you’ll need something more specialized. +Fortunately, you can create new fields in Yesod yourself. The Field constructor +has three values: fieldParse takes a list of values submitted by the user and +returns one of three results:

+
    +
  • +

    +An error message saying validation failed. +

    +
  • +
  • +

    +The parsed value. +

    +
  • +
  • +

    +Nothing, indicating that no data was supplied. +

    +
  • +
+

That last case might sound surprising. It would seem that Yesod can +automatically know that no information is supplied when the input list is +empty. But in reality, for some field types, the lack of any input is actually +valid input. Checkboxes, for instance, indicate an unchecked state by sending +in an empty list.

+

Also, what’s up with the list? Shouldn’t it be a Maybe? That’s also not the +case. With grouped checkboxes and multi-select lists, you’ll have multiple +widgets with the same name. We also use this trick in our example below.

+

The second value in the constructor is fieldView, and it renders a widget to display to the +user. This function has the following arguments:

+
    +
  1. +

    +The id attribute. +

    +
  2. +
  3. +

    +The name attribute. +

    +
  4. +
  5. +

    +Any other arbitrary attributes. +

    +
  6. +
  7. +

    +The result, given as an Either value. This will provide either the unparsed +input (when parsing failed) or the successfully parsed value. intField is a +great example of how this works. If you type in 42, the value of result +will be Right 42. But if you type in turtle, the result will be Left +"turtle". This lets you put in a value attribute on your input tag that will +give the user a consistent experience. +

    +
  8. +
  9. +

    +A Bool indicating if the field is required. +

    +
  10. +
+

The final value in the constructor is fieldEnctype. If you’re dealing with +file uploads, this should be Multipart; otherwise, it should be UrlEncoded.

+

As a small example, let’s create a new field type that is a password confirm +field. This field has two text inputs- both with the same name attribute- and +returns an error message if the values don’t match. Note that, unlike most +fields, it does not provide a value attribute on the input tags, as you don’t +want to send back a user-entered password in your HTML ever.

+
passwordConfirmField :: Field Handler Text
+passwordConfirmField = Field
+    { fieldParse = \rawVals _fileVals ->
+        case rawVals of
+            [a, b]
+                | a == b -> return $ Right $ Just a
+                | otherwise -> return $ Left "Passwords don't match"
+            [] -> return $ Right Nothing
+            _ -> return $ Left "You must enter two values"
+    , fieldView = \idAttr nameAttr otherAttrs eResult isReq ->
+        [whamlet|
+            <input id=#{idAttr} name=#{nameAttr} *{otherAttrs} type=password>
+            <div>Confirm:
+            <input id=#{idAttr}-confirm name=#{nameAttr} *{otherAttrs} type=password>
+        |]
+    , fieldEnctype = UrlEncoded
+    }
+
+getHomeR :: Handler Html
+getHomeR = do
+    ((res, widget), enctype) <- runFormGet $ renderDivs
+        $ areq passwordConfirmField "Password" Nothing
+    defaultLayout
+        [whamlet|
+            <p>Result: #{show res}
+            <form enctype=#{enctype}>
+                ^{widget}
+                <input type=submit value="Change password">
+        |]
+
+
+

Values that don’t come from the user

+

Imagine you’re writing a blog hosting web app, and you want to have a form for +users to enter a blog post. A blog post will consist of four pieces of +information:

+
    +
  • +

    +Title +

    +
  • +
  • +

    +HTML contents +

    +
  • +
  • +

    +User ID of the author +

    +
  • +
  • +

    +Publication date +

    +
  • +
+

We want the user to enter the first two values, but not the second two. User ID +should be determined automatically by authenticating the user (a topic we +haven’t covered yet), and the publication date should just be the current time. +The question is, how do we keep our simple applicative form syntax, and yet +pull in values that don’t come from the user?

+

The answer is two separate helper functions:

+
    +
  • +

    +pure allows us to wrap up a plain value as an applicative form value. +

    +
  • +
  • +

    +lift allows us to run arbitrary Handler actions inside an applicative form. +

    +
  • +
+

Let’s see an example of using these two functions:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Control.Applicative
+import           Data.Text           (Text)
+import           Data.Time
+import           Yesod
+
+-- In the authentication chapter, we'll address this properly
+newtype UserId = UserId Int
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+type Form a = Html -> MForm Handler (FormResult a, Widget)
+
+data Blog = Blog
+    { blogTitle    :: Text
+    , blogContents :: Textarea
+    , blogUser     :: UserId
+    , blogPosted   :: UTCTime
+    }
+    deriving Show
+
+form :: UserId -> Form Blog
+form userId = renderDivs $ Blog
+    <$> areq textField "Title" Nothing
+    <*> areq textareaField "Contents" Nothing
+    <*> pure userId
+    <*> lift (liftIO getCurrentTime)
+
+getHomeR :: Handler Html
+getHomeR = do
+    let userId = UserId 5 -- again, see the authentication chapter
+    ((res, widget), enctype) <- runFormPost $ form userId
+    defaultLayout
+        [whamlet|
+            <p>Previous result: #{show res}
+            <form method=post action=@{HomeR} enctype=#{enctype}>
+                ^{widget}
+                <input type=submit>
+        |]
+
+postHomeR :: Handler Html
+postHomeR = getHomeR
+
+main :: IO ()
+main = warp 3000 App
+

One trick we’ve introduced here is using the same handler code for both the +GET and POST request methods. This is enabled by the implementation of +runFormPost, which will behave exactly like generateFormPost in the case of +a GET request. Using the same handler for both request methods cuts down on +some boilerplate.

+
+
+

Summary

+

Forms in Yesod are broken up into three groups. Applicative is the most common, +as it provides a nice user interface with an easy-to-use API. Monadic forms +give you more power, but are harder to use. Input forms are intended when you +just want to read data from the user, not generate the input widgets.

+

There are a number of different Fields provided by Yesod out-of-the-box. In +order to use these in your forms, you need to indicate the kind of form and +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 +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.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/haskell.html b/public/book/haskell.html new file mode 100644 index 00000000..427a2f23 --- /dev/null +++ b/public/book/haskell.html @@ -0,0 +1,450 @@ + Haskell :: Yesod Web Framework Book- Version 1.6 +
+

Haskell

+ + +

Haskell is a powerful, fast, type-safe, functional programming language. This +book takes as an assumption that you are already familiar with most of the +basics of Haskell. There are two wonderful books for learning Haskell, both of +which are available for reading online:

+ +

Additionally, there are a number of great articles on +School of Haskell.

+

In order to use Yesod, you’re going to have to know at least the basics of +Haskell. Additionally, Yesod uses some features of Haskell that aren’t covered +in most introductory texts. While this book assumes the reader has a basic +familiarity with Haskell, this chapter is intended to fill in the gaps.

+

If you are already fluent in Haskell, feel free to completely skip this +chapter. Also, if you would prefer to start off by getting your feet wet with +Yesod, you can always come back to this chapter later as a reference.

+
+

Terminology

+

Even for those familiar with Haskell as a language, there can sometimes be some +confusion about terminology. Let’s establish some base terms that we can use +throughout this book.

+
+
+Data type +
+

+This is one of the core building blocks for a strongly typed +language like Haskell. Some data types, like Int, can be treated as primitive +values, while other data types will build on top of these to create more +complicated values. For example, you might represent a person with: +

+
data Person = Person Text Int
+

Here, the Text would give the person’s name, and the Int would give the +person’s age. Due to its simplicity, this specific example type will recur +throughout the book. There are essentially three ways you can create a new data +type:

+
    +
  • +

    +A type declaration such as type GearCount = Int merely creates a + synonym for an existing type. The type system will do nothing to prevent + you from using an Int where you asked for a GearCount. Using this can + make your code more self-documenting. +

    +
  • +
  • +

    +A newtype declaration such as newtype Make = Make Text. In this case, + you cannot accidentally use a Text in place of a Make; the compiler + will stop you. The newtype wrapper always disappears during compilation, + and will introduce no overhead. +

    +
  • +
  • +

    +A data declaration, such as Person above. You can also create + Algebraic Data Types (ADTs), such as data Vehicle = Bicycle GearCount | + Car Make Model. +

    +
  • +
+
+
+Data constructor +
+

+In our examples above, Person, Make, Bicycle, and +Car are all data constructors. +

+
+
+Type constructor +
+

+In our examples above, Person, Make, and Vehicle are +all type constructors. +

+
+
+Type variables +
+

+Consider the data type data Maybe a = Just a | Nothing. In +this case, a is a type variable. +

+
+
+ +
+
+

Tools

+

Since July 2015, the tooling recommendation for Yesod has become very simple: +use stack. stack is a +complete build tool for Haskell which deals with your compiler (Glasgow Haskell +Compiler, aka GHC), libraries (including Yesod), additional build tools (like +alex and happy), and much more. There are other build tools available in +Haskell, and most of them support Yesod quite well. But for the easiest +experience, it’s strongly recommended to stick with stack. The Yesod website +keeps an up-to-date quick start +guide, which provides instructions on installing stack and getting started +with a new scaffolded site.

+

Once you have your toolchain set up correctly, you’ll need to install a number +of Haskell libraries. For the vast majority of the book, the following command +will install all the libraries you need:

+
stack build classy-prelude-yesod persistent-sqlite
+

In order to run an example from the book, save it in a file, e.g., +yesod-example.hs, and then run it with:

+
stack runghc yesod-example.hs
+
+
+

Language Pragmas

+

GHC will run by default in something very close to Haskell98 mode. It also +ships with a large number of language extensions, allowing more powerful type +classes, syntax changes, and more. There are multiple ways to tell GHC to turn +on these extensions. For most of the code snippets in this book, you’ll see +language pragmas, which look like this:

+
{-# LANGUAGE MyLanguageExtension #-}
+

These should always appear at the top of your source file. Additionally, there +are two other common approaches:

+
    +
  • +

    +On the GHC command line, pass an extra argument -XMyLanguageExtension. +

    +
  • +
  • +

    +In your cabal file, add an default-extensions block. +

    +
  • +
+

I personally never use the GHC command line argument approach. It’s a personal +preference, but I like to have my settings clearly stated in a file. In general +it’s recommended to avoid putting extensions in your cabal file; however, +this rule mostly applies when writing publicly available libraries. When you’re +writing an application that you and your team will be working on, having all of +your language extensions defined in a single location makes a lot of sense. +The Yesod scaffolded site specifically uses this approach to avoid the +boilerplate of specifying the same language pragmas in every source file.

+

We’ll end up using quite a few language extensions in this book (at the time of +writing, the scaffolding uses 13). We will not cover the meaning of all of +them. Instead, please see the +GHC +documentation.

+
+
+

Overloaded Strings

+

What’s the type of "hello"? Traditionally, it’s String, which is defined as +type String = [Char]. Unfortunately, there are a number of limitations with +this:

+
    +
  • +

    +It’s a very inefficient implementation of textual data. We need to allocate + extra memory for each cons cell, plus the characters themselves each take up + a full machine word. +

    +
  • +
  • +

    +Sometimes we have string-like data that’s not actually text, such as + ByteStrings and HTML. +

    +
  • +
+

To work around these limitations, GHC has a language extension called +OverloadedStrings. When enabled, literal strings no longer have the +monomorphic type String; instead, they have the type IsString a ⇒ a, +where IsString is defined as:

+
class IsString a where
+    fromString :: String -> a
+

There are IsString instances available for a number of types in Haskell, such +as Text (a much more efficient packed String type), ByteString, and +Html. Virtually every example in this book will assume that this language +extension is turned on.

+

Unfortunately, there is one drawback to this extension: it can sometimes +confuse GHC’s type checker. Imagine we have:

+
{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-}
+import Data.Text (Text)
+
+class DoSomething a where
+    something :: a -> IO ()
+
+instance DoSomething String where
+    something _ = putStrLn "String"
+
+instance DoSomething Text where
+    something _ = putStrLn "Text"
+
+myFunc :: IO ()
+myFunc = something "hello"
+

Will the program print out String or Text? It’s not clear. So instead, +you’ll need to give an explicit type annotation to specify whether "hello" +should be treated as a String or Text.

+ +
+
+

Type Families

+

The basic idea of a type family is to state some association between two +different types. Suppose we want to write a function that will safely take the +first element of a list. But we don’t want it to work just on lists; we’d like +it to treat a ByteString like a list of Word8s. To do so, we need to +introduce some associated type to specify what the contents of a certain type +are.

+
{-# LANGUAGE TypeFamilies, OverloadedStrings #-}
+import Data.Word (Word8)
+import qualified Data.ByteString as S
+import Data.ByteString.Char8 () -- get an orphan IsString instance
+
+class SafeHead a where
+    type Content a
+    safeHead :: a -> Maybe (Content a)
+
+instance SafeHead [a] where
+    type Content [a] = a
+    safeHead [] = Nothing
+    safeHead (x:_) = Just x
+
+instance SafeHead S.ByteString where
+    type Content S.ByteString = Word8
+    safeHead bs
+        | S.null bs = Nothing
+        | otherwise = Just $ S.head bs
+
+main :: IO ()
+main = do
+    print $ safeHead ("" :: String)
+    print $ safeHead ("hello" :: String)
+
+    print $ safeHead ("" :: S.ByteString)
+    print $ safeHead ("hello" :: S.ByteString)
+

The new syntax is the ability to place a type inside of a class and +instance. We can also use data instead, which will create a new datatype +instead of reference an existing one.

+ +
+
+

Template Haskell

+

Template Haskell (TH) is an approach to code generation. We use it in Yesod +in a number of places to reduce boilerplate, and to ensure that the generated +code is correct. Template Haskell is essentially Haskell which generates a +Haskell Abstract Syntax Tree (AST).

+ +

Writing TH code can be tricky, and unfortunately there isn’t very much type +safety involved. You can easily write TH that will generate code that won’t +compile. This is only an issue for the developers of Yesod, not for its users. +During development, we use a large collection of unit tests to ensure that the +generated code is correct. As a user, all you need to do is call these already +existing functions. For example, to include an externally defined Hamlet +template, you can write:

+
$(hamletFile "myfile.hamlet")
+

(Hamlet is discussed in the Shakespeare chapter.) The dollar sign immediately +followed by parentheses tell GHC that what follows is a Template Haskell +function. The code inside is then run by the compiler and generates a Haskell +AST, which is then compiled. And yes, it’s even possible to +go meta +with this.

+

A nice trick is that TH code is allowed to perform arbitrary IO actions, and +therefore we can place some input in external files and have it parsed at +compile time. One example usage is to have compile-time checked HTML, CSS, and +Javascript templates.

+

If your Template Haskell code is being used to generate declarations, and is +being placed at the top level of our file, we can leave off the dollar sign and +parentheses. In other words:

+
{-# LANGUAGE TemplateHaskell #-}
+
+-- Normal function declaration, nothing special
+myFunction = ...
+
+-- Include some TH code
+$(myThCode)
+
+-- Or equivalently
+myThCode
+

It can be useful to see what code is being generated by Template Haskell for +you. To do so, you should use the -ddump-splices GHC option.

+ +

Template Haskell introduces something called the stage +restriction, which essentially means that code before a Template Haskell splice +cannot refer to code in the Template Haskell, or what follows. This will +sometimes require you to rearrange your code a bit. The same restriction +applies to QuasiQuotes.

+

While out of the box, Yesod is really geared for using code generation to avoid +boilerplate, it’s perfectly acceptable to use Yesod in a Template Haskell-free +way. There’s more information on that in the "Yesod for Haskellers" chapter.

+
+
+

QuasiQuotes

+

QuasiQuotes (QQ) are a minor extension of Template Haskell that let us embed +arbitrary content within our Haskell source files. For example, we mentioned +previously the hamletFile TH function, which reads the template contents from +an external file. We also have a quasi-quoter named hamlet that takes the +content inline:

+
{-# LANGUAGE QuasiQuotes #-}
+
+[hamlet|<p>This is quasi-quoted Hamlet.|]
+

The syntax is set off using square brackets and pipes. The name of the +quasi-quoter is given between the opening bracket and the first pipe, and the +content is given between the pipes.

+

Throughout the book, we will often times use the QQ-approach over a TH-powered +external file since the former is simpler to copy-and-paste. However, in +production, external files are recommended for all but the shortest of inputs +as it gives a nice separation of the non-Haskell syntax from your Haskell code.

+
+
+

API Documentation

+

The standard API documentation program in Haskell is called Haddock. The +standard Haddock search tool is called Hoogle. My recommendation is to use +Stackage’s Hoogle search and its +accompanying Haddocks for searching and browsing documentation. The reason for +this is that the Stackage Hoogle database covers a very large number of open +source Haskell packages, and the documentation provided is always fully +generated and known to link to other working Haddocks.

+

If when reading this book you run into types or functions that you do not +understand, try doing a Hoogle search with Hoogle to get more +information.

+
+
+

Summary

+

You don’t need to be an expert in Haskell to use Yesod, a basic familiarity +will suffice. This chapter hopefully gave you just enough extra information to +feel more comfortable following the rest of the book.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/http-conduit.html b/public/book/http-conduit.html new file mode 100644 index 00000000..7c8790e5 --- /dev/null +++ b/public/book/http-conduit.html @@ -0,0 +1,127 @@ + http-conduit :: Yesod Web Framework Book- Version 1.6 +
+ +
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/initializing-foundation-data.html b/public/book/initializing-foundation-data.html new file mode 100644 index 00000000..491771a0 --- /dev/null +++ b/public/book/initializing-foundation-data.html @@ -0,0 +1,270 @@ + Initializing data in the foundation datatype :: Yesod Web Framework Book- Version 1.6 +
+

Initializing data in the foundation datatype

+ + +

This example is meant to demonstrate a relatively simple concept: performing +some initialization of data to be kept in the foundation datatype. There are +various reasons to do this, though the two most important are:

+
    +
  • +

    +Efficiency: by initializing data once, at process startup, you can avoid + having to recompute the same value in each request. +

    +
  • +
  • +

    +Persistence: we want to store some information in a mutable location which + will be persisted between individual requests. Often times, this is done via + an external database, but it can also be done via an in-memory mutable + variable. +

    +
  • +
+ +

To demonstrate, we’ll implement a very simple website. It will contain a single +route, and will serve content stored in a Markdown file. In addition to serving +that content, we’ll also display an old-school visitor counter indicating how +many visitors have been to the site.

+
+

Step 1: define your foundation

+

We’ve identified two pieces of information to be initialized: the Markdown +content to be display on the homepage, and a mutable variable holding the +visitor count. Remember that our goal is to perform as much of the work in the +initialization phase as possible and thereby avoid performing the same work in +the handlers themselves. Therefore, we want to preprocess the Markdown content +into HTML. As for the visitor count, a simple IORef should be sufficient. So +our foundation data type is:

+
data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+
+

Step 2: use the foundation

+

For this trivial example, we only have one route: the homepage. All we need to do is:

+
    +
  1. +

    +Increment the visitor count. +

    +
  2. +
  3. +

    +Get the new visitor count. +

    +
  4. +
  5. +

    +Display the Markdown content together with the visitor count. +

    +
  6. +
+

One trick we’ll use to make the code a bit shorter is to utilize record +wildcard syntax: App {..}. This is convenient when we want to deal with a +number of different fields in a datatype.

+
getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+
+

Step 3: create the foundation value

+

When we initialize our application, we’ll now need to provide values for the +two fields we described above. This is normal IO code, and can perform any +arbitrary actions needed. In our case, we need to:

+
    +
  1. +

    +Read the Markdown from the file. +

    +
  2. +
  3. +

    +Convert that Markdown to HTML. +

    +
  4. +
  5. +

    +Create the visitor counter variable. +

    +
  6. +
+

The code ends up being just as simple as those steps imply:

+
go :: IO ()
+go = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+

Conclusion

+

There’s no rocket science involved in this example, just very straightforward +programming. The purpose of this chapter is to demonstrate the commonly used +best practice for achieving these often needed objectives. In your own +applications, the initialization steps will likely be much more complicated: +setting up database connection pools, starting background jobs to batch process +large data, or anything else. After reading this chapter, you should now have a +good idea of where to place your application-specific initialization code.

+

Below is the full source code for the example described above:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import qualified Data.Text.Lazy.IO as TLIO
+import           Text.Markdown
+import           Yesod
+
+data App = App
+    { homepageContent :: Html
+    , visitorCount    :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    App {..} <- getYesod
+    currentCount <- liftIO $ atomicModifyIORef visitorCount
+        $ \i -> (i + 1, i + 1)
+    defaultLayout $ do
+        setTitle "Homepage"
+        [whamlet|
+            <article>#{homepageContent}
+            <p>You are visitor number: #{currentCount}.
+        |]
+
+main :: IO ()
+main = do
+    rawMarkdown <- TLIO.readFile "homepage.md"
+    countRef <- newIORef 0
+    warp 3000 App
+        { homepageContent = markdown def rawMarkdown
+        , visitorCount    = countRef
+        }
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/internationalization.html b/public/book/internationalization.html new file mode 100644 index 00000000..5875e77b --- /dev/null +++ b/public/book/internationalization.html @@ -0,0 +1,439 @@ + Internationalization :: Yesod Web Framework Book- Version 1.6 +
+

Internationalization

+ + +

Users expect our software to speak their language. Unfortunately for us, there +will likely be more than one language involved. While doing simple string +replacement isn’t too involved, correctly dealing with all the grammar issues +can be tricky. After all, who wants to see "List 1 file(s)" from a program +output?

+

But a real i18n solution needs to do more than just provide a means of +achieving the correct output. It needs to make this process easy for both the +programmer and the translator and relatively error-proof. Yesod’s answer to the +problem gives you:

+
    +
  • +

    +Intelligent guessing of the user’s desired language based on request headers, + with the ability to override. +

    +
  • +
  • +

    +A simple syntax for giving translations which requires no Haskell knowledge. + (After all, most translators aren’t programmers.) +

    +
  • +
  • +

    +The ability to bring in the full power of Haskell for tricky grammar issues + as necessary, along with a default selection of helper functions to cover + most needs. +

    +
  • +
  • +

    +Absolutely no issues at all with word order. +

    +
  • +
+
+

Synopsis

+
-- @messages/en.msg
+Hello: Hello
+EnterItemCount: I would like to buy:
+Purchase: Purchase
+ItemCount count@Int: You have purchased #{showInt count} #{plural count "item" "items"}.
+SwitchLanguage: Switch language to:
+Switch: Switch
+
-- @messages/he.msg
+Hello: שלום
+EnterItemCount: אני רוצה לקנות:
+Purchase: קנה
+ItemCount count: קנית #{showInt count} #{plural count "דבר" "דברים"}.
+SwitchLanguage: החלף שפה ל:
+Switch: החלף
+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkMessage "App" "messages" "en"
+
+plural :: Int -> String -> String -> String
+plural 1 x _ = x
+plural _ _ y = y
+
+showInt :: Int -> String
+showInt = show
+
+mkYesod "App" [parseRoutes|
+/     HomeR GET
+/buy  BuyR  GET
+/lang LangR POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <h1>_{MsgHello}
+        <form action=@{BuyR}>
+            _{MsgEnterItemCount}
+            <input type=text name=count>
+            <input type=submit value=_{MsgPurchase}>
+        <form action=@{LangR} method=post>
+            _{MsgSwitchLanguage}
+            <select name=lang>
+                <option value=en>English
+                <option value=he>Hebrew
+            <input type=submit value=_{MsgSwitch}>
+    |]
+
+getBuyR :: Handler Html
+getBuyR = do
+    count <- runInputGet $ ireq intField "count"
+    defaultLayout [whamlet|<p>_{MsgItemCount count}|]
+
+postLangR :: Handler ()
+postLangR = do
+    lang <- runInputPost $ ireq textField "lang"
+    setLanguage lang
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Overview

+

Most existing i18n solutions out there, like gettext or Java message bundles, +work on the principle of string lookups. Usually some form of +printf-interpolation is used to interpolate variables into the strings. In +Yesod, as you might guess, we instead rely on types. This gives us all of our +normal advantages, such as the compiler automatically catching mistakes.

+

Let’s take a concrete example. Suppose our application has two things it wants +to say to a user: say hello, and state how many users are logged into the +system. This can be modeled with a sum type:

+
data MyMessage = MsgHello | MsgUsersLoggedIn Int
+

I can also write a function to turn this datatype into an English representation:

+
toEnglish :: MyMessage -> String
+toEnglish MsgHello = "Hello there!"
+toEnglish (MsgUsersLoggedIn 1) = "There is 1 user logged in."
+toEnglish (MsgUsersLoggedIn i) = "There are " ++ show i ++ " users logged in."
+

We can also write similar functions for other languages. The advantage to this +inside-Haskell approach is that we have the full power of Haskell for +addressing tricky grammar issues, especially pluralization.

+ +

The downside, however, is that you have to write all of this inside of Haskell, +which won’t be very translator-friendly. To solve this, Yesod introduces the +concept of message files. We’ll cover that in a little bit.

+

Assuming we have this full set of translation functions, how do we go about +using them? What we need is a new function to wrap them all up together, and +then choose the appropriate translation function based on the user’s selected +language. Once we have that, Yesod can automatically choose the most relevant +render function and call it on the values you provide.

+

In order to simplify things a bit, Hamlet has a special interpolation syntax, +_{…}, which handles all the calls to the render functions. And in order to +associate a render function with your application, you use the YesodMessage +typeclass.

+
+
+

Message files

+

The simplest approach to creating translations is via message files. The setup +is simple: there is a single folder containing all of your translation files, +with a single file for each language. Each file is named based on its language +code, e.g. en.msg. And each line in a file handles one phrase, which +correlates to a single constructor in your message data type.

+

So firstly, a word about language codes. There are really two choices +available: using a two-letter language code, or a language-LOCALE code. For +example, when I load up a page in my web browser, it sends two language codes: +en-US and en. What my browser is saying is "if you have American English, I +like that the most. If you have English, I’ll take that instead."

+

So which format should you use in your application? Most likely two-letter +codes, unless you are actually creating separate translations by locale. This +ensures that someone asking for Canadian English will still see your English. +Behind the scenes, Yesod will add the two-letter codes where relevant. For +example, suppose a user has the following language list:

+
pt-BR, es, he
+

What this means is "I like Brazilian Portuguese, then Spanish, and then +Hebrew." Suppose your application provides the languages pt (general +Portuguese) and English, with English as the default. Strictly following the +user’s language list would result in the user being served English. Instead, +Yesod translates that list into:

+
pt-BR, es, he, pt
+

In other words: unless you’re giving different translations based on locale, +just stick to the two-letter language codes.

+

Now what about these message files? The syntax should be very familiar after +your work with Hamlet and Persistent. The line starts off with the name of the +message. Since this is a data constructor, it must start with a capital letter. +Next, you can have individual parameters, which must be given as lower case. +These will be arguments to the data constructor.

+

The argument list is terminated by a colon, and then followed by the translated +string, which allows usage of our typical variable interpolation syntax +translation helper functions to deal with issues like pluralization, you can +create all the translated messages you need.

+
+

Scaffolding

+

The scaffolding used to include a messages folder for i18n messages. Since it is +used rarely it was removed to save some performance. +To add back i18n to your application you need to:

+
    +
  • +

    +Add the line mkMessage "App" "messages" "en" to Foundation.hs. +

    +
  • +
  • +

    +Create a directory "messages" in the main folder of your scaffolding project. +

    +
  • +
  • +

    +Create a file "messages/en.msg" with the following dummy content: Hello: Hello +

    +
  • +
+

After that you can use {..} anywhere in all your Hamlet files. Just make sure +to insert mkMessage "App" "messages" "en" before instance Yesod App where. +Otherwise you can’t use i18n in your defaultLayout. If your default language is +not "en", you can decide it here. Just make sure to also name your message file accordingly.

+
+
+

Specifying types

+

Since we will be creating a datatype out of our message specifications, each +parameter to a data constructor must be given a data type. We use a @-syntax +for this. For example, to create the datatype data MyMessage = MsgHello | +MsgSayAge Int, we would write:

+
Hello: Hi there!
+SayAge age@Int: Your age is: #{show age}
+

But there are two problems with this:

+
    +
  1. +

    +It’s not very DRY (don’t repeat yourself) to have to specify this datatype in every file. +

    +
  2. +
  3. +

    +Translators will be confused having to specify these datatypes. +

    +
  4. +
+

So instead, the type specification is only required in the main language file. +This is specified as the third argument in the mkMessage function. This also +specifies what the backup language will be, to be used when none of the +languages provided by your application match the user’s language list.

+
+
+
+

RenderMessage typeclass

+

Your call to mkMessage creates an instance of the RenderMessage typeclass, +which is the core of Yesod’s i18n. It is defined as:

+
class RenderMessage master message where
+    renderMessage :: master  -- ^ type that specifies which set of translations to use
+                  -> [Lang]  -- ^ acceptable languages in descending order of preference
+                  -> message -- ^ message to translate
+                  -> Text
+
+-- | an RFC1766 / ISO 639-1 language code (eg, @fr@, @en-GB@, etc).
+type Lang = Text
+

Notice that there are two parameters to the RenderMessage class: the master +site and the message type. In theory, we could skip the master type here, but +that would mean that every site would need to have the same set of translations +for each message type. When it comes to shared libraries like forms, that would +not be a workable solution.

+

The renderMessage function takes a parameter for each of the class’s type +parameters: master and message. The extra parameter is a list of languages the +user will accept, in descending order of priority. The method then returns a +user-ready Text that can be displayed.

+

A simple instance of RenderMessage may involve no actual translation of +strings; instead, it will just display the same value for every language. For +example:

+
data MyMessage = Hello | Greet Text
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ _ Hello = "Hello"
+    renderMessage _ _ (Greet name) = "Welcome, " <> name <> "!"
+

Notice how we ignore the first two parameters to renderMessage. We can now +extend this to support multiple languages:

+
renderEn Hello = "Hello"
+renderEn (Greet name) = "Welcome, " <> name <> "!"
+renderHe Hello = "שלום"
+renderHe (Greet name) = "ברוכים הבאים, " <> name <> "!"
+instance RenderMessage MyApp MyMessage where
+    renderMessage _ ("en":_) = renderEn
+    renderMessage _ ("he":_) = renderHe
+    renderMessage master (_:langs) = renderMessage master langs
+    renderMessage _ [] = renderEn
+

The idea here is fairly straight-forward: we define helper functions to support +each language. We then add a clause to catch each of those languages in the +renderMessage definition. We then have two final cases: if no languages +matched, continue checking with the next language in the user’s priority list. +If we’ve exhausted all languages the user specified, then use the default +language (in our case, English).

+

But odds are that you will never need to worry about writing this stuff +manually, as the message file interface does all this for you. But it’s always +a good idea to have an understanding of what’s going on under the surface.

+
+
+

Interpolation

+

One way to use your new RenderMessage instance would be to directly call the +renderMessage function. This would work, but it’s a bit tedious: you need to +pass in the foundation value and the language list manually. Instead, Hamlet +provides a specialized i18n interpolation, which looks like _{…}.

+ +

Hamlet will then automatically translate that to a call to renderMessage. +Once Hamlet gets the output Text value, it uses the toHtml function to +produce an Html value, meaning that any special characters (<, &, +>) will be automatically escaped.

+
+
+

Phrases, not words

+

As a final note, I’d just like to give some general i18n advice. Let’s say you +have an application for selling turtles. You’re going to use the word "turtle" +in multiple places, like "You have added 4 turtles to your cart." and "You have +purchased 4 turtles, congratulations!" As a programmer, you’ll immediately +notice the code reuse potential: we have the phrase "4 turtles" twice. So you +might structure your message file as:

+
AddStart: You have added
+AddEnd: to your cart.
+PurchaseStart: You have purchased
+PurchaseEnd: , congratulations!
+Turtles count@Int: #{show count} #{plural count "turtle" "turtles"}
+

STOP RIGHT THERE! This is all well and good from a programming perspective, but translations are not programming. There are a many things that could go wrong with this, such as:

+
    +
  • +

    +Some languages might put "to your cart" before "You have added." +

    +
  • +
  • +

    +Maybe "added" will be constructed differently depending on whether you added 1 or more turtles. +

    +
  • +
  • +

    +There are a bunch of whitespace issues as well. +

    +
  • +
+

So the general rule is: translate entire phrases, not just words.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/introduction.html b/public/book/introduction.html new file mode 100644 index 00000000..e00d8c1e --- /dev/null +++ b/public/book/introduction.html @@ -0,0 +1,220 @@ + Introduction :: Yesod Web Framework Book- Version 1.6 +
+

Introduction

+ + +

Since web programming began, people have been trying to make the development +process a more pleasant one. As a community, we have continually pushed new +techniques to try and solve some of the lingering difficulties of security +threats, the stateless nature of HTTP, the multiple languages (HTML, CSS, +Javascript) necessary to create a powerful web application, and more.

+

Yesod attempts to ease the web development process by playing to the strengths +of the Haskell programming language. Haskell’s strong compile-time guarantees +of correctness not only encompass types; referential transparency ensures that +we don’t have any unintended side effects. Pattern matching on algebraic data +types can help guarantee we’ve accounted for every possible case. By building +upon Haskell, entire classes of bugs disappear.

+

Unfortunately, using Haskell isn’t enough. The web, by its very nature, is +not type safe. Even the simplest case of distinguishing between an integer +and string is impossible: all data on the web is transferred as raw bytes, +evading our best efforts at type safety. Every app writer is left with the task +of validating all input. I call this problem the boundary issue: as much as +your application is type safe on the inside, every boundary with the outside +world still needs to be sanitized.

+
+

Type Safety

+

This is where Yesod comes in. By using high-level declarative techniques, you +can specify the exact input types you are expecting. And the process works the +other way as well: using a process of type-safe URLs, you can make sure that +the data you send out is also guaranteed to be well formed.

+

The boundary issue is not just a problem when dealing with the client: the same +problem exists when persisting and loading data. Once again, Yesod saves you on +the boundary by performing the marshaling of data for you. You can specify your +entities in a high-level definition and remain blissfully ignorant of the +details.

+
+
+

Concise

+

We all know that there is a lot of boilerplate coding involved in web +applications. Wherever possible, Yesod tries to use Haskell’s features to save +your fingers the work:

+
    +
  • +

    +The forms library reduces the amount of code used for common cases by + leveraging the Applicative type class. +

    +
  • +
  • +

    +Routes are declared in a very terse format, without sacrificing type safety. +

    +
  • +
  • +

    +Serializing your data to and from a database is handled automatically via + code generation. +

    +
  • +
+

In Yesod, we have two kinds of code generation. To get your project started, we +provide a scaffolding tool to set up your file and folder structure. However, +most code generation is done at compile time via meta-programming. This means +your generated code will never get stale, as a simple library upgrade will +bring all your generated code up-to-date.

+

But for those who like to stay in control, and know exactly what their code is +doing, you can always run closer to the compiler and write all your code +yourself.

+
+
+

Performance

+

Haskell’s main compiler, the GHC, has amazing performance characteristics, and +is improving all the time. This choice of language by itself gives Yesod a +large performance advantage over other offerings. But that’s not enough: we +need an architecture designed for performance.

+

Our approach to templates is one example: by allowing HTML, CSS and JavaScript +to be analyzed at compile time, Yesod both avoids costly disk I/O at runtime +and can optimize the rendering of this code. But the architectural decisions go +deeper: we use advanced techniques such as conduits and builders in the +underlying libraries to make sure our code runs in constant memory, without +exhausting precious file handles and other resources. By offering high-level +abstractions, you can get highly compressed and properly cached CSS and +JavaScript.

+

Yesod’s flagship web server, Warp, is the fastest Haskell web server around. +When these two pieces of technology are combined, it produces one of the +fastest web application deployment solutions available.

+
+
+

Modular

+

Yesod has spawned the creation of dozens of packages, most of which are usable +in a context outside of Yesod itself. One of the goals of the project is to +contribute back to the community as much as possible; as such, even if you are +not planning on using Yesod in your next project, a large portion of this book +may still be relevant for your needs.

+

Of course, these libraries have all been designed to integrate well together. +Using the Yesod Framework should give you a strong feeling of consistency +throughout the various APIs.

+
+
+

A solid foundation

+

I remember once seeing a PHP framework advertising support for UTF-8. This +struck me as surprising: you mean having UTF-8 support isn’t automatic? In the +Haskell world, issues like character encoding are already well addressed and +fully supported. In fact, we usually have the opposite problem: there are a +number of packages providing powerful and well-designed support for the +problem. The Haskell community is constantly pushing the boundaries finding the +cleanest, most efficient solutions for each challenge.

+

The downside of such a powerful ecosystem is the complexity of choice. By using +Yesod, you will already have most of the tools chosen for you, and you can be +guaranteed they work together. Of course, you always have the option of pulling +in your own solution.

+

As a real-life example, Yesod and Hamlet (the default templating language) use +blaze-builder for textual content generation. This choice was made because +blaze provides the fastest interface for generating UTF-8 data. Anyone who +wants to use one of the other great libraries out there, such as text, should +have no problem dropping it in.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/json-web-service.html b/public/book/json-web-service.html new file mode 100644 index 00000000..f7f8bdab --- /dev/null +++ b/public/book/json-web-service.html @@ -0,0 +1,221 @@ + JSON Web Service :: Yesod Web Framework Book- Version 1.6 +
+

JSON Web Service

+ + +

Let’s create a very simple web service: it takes a JSON request and returns a +JSON response. We’re going to write the server in WAI/Warp, and the client in +http-conduit. We’ll be using aeson for JSON parsing and rendering. We could +also write the server in Yesod itself, but for such a simple example, the extra +features of Yesod don’t add much.

+
+

Server

+

WAI uses the conduit package to handle streaming request bodies, and +efficiently generates responses using blaze-builder. aeson uses attoparsec for +parsing; by using attoparsec-conduit we get easy interoperability with WAI. +This plays out as:

+
{-# LANGUAGE OverloadedStrings #-}
+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.ByteString          (ByteString)
+import           Data.Conduit             (($$))
+import           Data.Conduit.Attoparsec  (sinkParser)
+import           Network.HTTP.Types       (status200, status400)
+import           Network.Wai              (Application, Response, responseLBS)
+import           Network.Wai.Conduit      (sourceRequestBody)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req sendResponse = handle (sendResponse . invalidJson) $ do
+    value <- sourceRequestBody req $$ sinkParser json
+    newValue <- liftIO $ modValue value
+    sendResponse $ responseLBS
+        status200
+        [("Content-Type", "application/json")]
+        $ encode newValue
+
+invalidJson :: SomeException -> Response
+invalidJson ex = responseLBS
+    status400
+    [("Content-Type", "application/json")]
+    $ encode $ object
+        [ ("message" .= show ex)
+        ]
+
+-- Application-specific logic would go here.
+modValue :: Value -> IO Value
+modValue = return
+
+
+

Client

+

http-conduit was written as a companion to WAI. It too uses conduit and +blaze-builder pervasively, meaning we once again get easy interop with +aeson. A few extra comments for those not familiar with http-conduit:

+
    +
  • +

    +A Manager is present to keep track of open connections, so that multiple + requests to the same server use the same connection. You usually want to use + the getGlobalManager function to get the global connection manager. +

    +
  • +
  • +

    +We need to know the size of our request body, which can’t be determined + directly from a Builder. Instead, we convert the Builder into a lazy + ByteString and take the size from there. +

    +
  • +
  • +

    +There are a number of different functions for initiating a request. We use + http, which allows us to directly access the data stream. There are other + higher level functions (such as httpLbs) that let you ignore the issues of + sources and get the entire body directly. +

    +
  • +
+
{-# LANGUAGE OverloadedStrings #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Data.Aeson              (Value (Object, String))
+import           Data.Aeson              (encode, object, (.=))
+import           Data.Aeson.Parser       (json)
+import           Data.Conduit            (($$+-))
+import           Data.Conduit.Attoparsec (sinkParser)
+import           Network.HTTP.Conduit    (RequestBody (RequestBodyLBS),
+                                          Response (..), http, method, parseUrl,
+                                          requestBody, getGlobalManager)
+
+main :: IO ()
+main = do
+    manager <- getGlobalManager
+    value <- liftIO makeValue
+    -- We need to know the size of the request body, so we convert to a
+    -- ByteString
+    let valueBS = encode value
+    req' <- liftIO $ parseUrl "http://localhost:3000/"
+    let req = req' { method = "POST", requestBody = RequestBodyLBS valueBS }
+    res <- http req manager
+    resValue <- responseBody res $$+- sinkParser json
+    liftIO $ handleResponse resValue
+
+-- Application-specific function to make the request value
+makeValue :: IO Value
+makeValue = return $ object
+    [ ("foo" .= ("bar" :: String))
+    ]
+
+-- Application-specific function to handle the response from the server
+handleResponse :: Value -> IO ()
+handleResponse = print
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/monad-control.html b/public/book/monad-control.html new file mode 100644 index 00000000..cd270c2e --- /dev/null +++ b/public/book/monad-control.html @@ -0,0 +1,450 @@ + monad-control :: Yesod Web Framework Book- Version 1.6 +
+

monad-control

+ + +

monad-control is used in a few places within Yesod, most notably to ensure +proper exception handling within Persistent. It is a general purpose package to +extend standard functionality in monad transformers.

+
+

Overview

+

One of the powerful, and sometimes confusing, features in Haskell is monad +transformers. They allow you to take different pieces of functionality- such as +mutable state, error handling, or logging- and compose them together easily. +Though I swore I’d never write a monad tutorial, I’m going to employ a painful +analogy here: monads are like onions. (Monads are not like cakes.) By that, I +mean layers.

+

We have the core monad- also known as the innermost or bottom monad. On top of +this core, we add layers, each adding a new feature and spreading +outward/upward. As a motivating example, let’s consider an ErrorT transformer +stacked on top of the IO monad:

+
newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
+type MyStack = ErrorT MyError IO
+

Now pay close attention here: ErrorT is just a simple newtype around an Either +wrapped in a monad. Getting rid of the newtype, we have:

+
type ErrorTUnwrapped e m a = m (Either e a)
+

At some point, we’ll need to actually perform some IO inside our MyStack. If we +went with the unwrapped approach, it would be trivial, since there would be no +ErrorT constructor in the way. However, we need that newtype wrapper for a +whole bunch of type reasons I won’t go into here (this isn’t a monad +transformer tutorial after all). So the solution is the MonadTrans typeclass:

+
class MonadTrans t where
+    lift :: Monad m => m a -> t m a
+

I’ll admit, the first time I saw that type signature, my response was stunned +confusion, and incredulity that it actually meant anything. But looking at an +instance helps a bit:

+
instance (Error e) => MonadTrans (ErrorT e) where
+    lift m = ErrorT $ do
+        a <- m
+        return (Right a)
+

All we’re doing is wrapping the inside of the IO with a Right value, and then +applying our newtype wrapper. This allows us to take an action that lives in +IO, and "lift" it to the outer/upper monad.

+

But now to the point at hand. This works very well for simple functions. For +example:

+
sayHi :: IO ()
+sayHi = putStrLn "Hello"
+
+sayHiError :: ErrorT MyError IO ()
+sayHiError = lift $ putStrLn "Hello"
+

But let’s take something slightly more complicated, like a callback:

+
withMyFile :: (Handle -> IO a) -> IO a
+withMyFile = withFile "test.txt" WriteMode
+
+sayHi :: Handle -> IO ()
+sayHi handle = hPutStrLn handle "Hi there"
+
+useMyFile :: IO ()
+useMyFile = withMyFile sayHi
+

So far so good, right? Now let’s say that we need a version of sayHi that has +access to the Error monad:

+
sayHiError :: Handle -> ErrorT MyError IO ()
+sayHiError handle = do
+    lift $ hPutStrLn handle "Hi there, error!"
+    throwError MyError
+

We would like to write a function that combines withMyFile and sayHiError. +Unfortunately, GHC doesn’t like this very much:

+
useMyFileErrorBad :: ErrorT MyError IO ()
+useMyFileErrorBad = withMyFile sayHiError
+
+    Couldn't match expected type `ErrorT MyError IO ()'
+                with actual type `IO ()'
+

Why does this happen, and how can we work around it?

+
+
+

Intuition

+

Let’s try and develop an external intuition of what’s happening here. The +ErrorT monad transformer adds extra functionality to the IO monad. We’ve +defined a way to "tack on" that extra functionality to normal IO actions: we +add that Right constructor and wrap it all in ErrorT. Wrapping in Right is our +way of saying "it went OK," there wasn’t anything wrong with this action.

+

Now this intuitively makes sense: since the IO monad doesn’t have the concept +of returning a MyError when something goes wrong, it will always succeed in the +lifting phase. (Note: This has nothing to do with runtime exceptions, don’t +even think about them.) What we have is a guaranteed one-directional +translation up the monad stack.

+

Let’s take another example: the Reader monad. A Reader has access to some extra +piece of data floating around. Whatever is running in the inner monad doesn’t +know about that extra piece of information. So how would you do a lift? You +just ignore that extra information. The Writer monad? Don’t write anything. +State? Don’t change anything. I’m seeing a pattern here.

+

But now let’s try and go in the opposite direction: I have something in a +Reader, and I’d like to run it in the base monad (e.g., IO). Well… that’s not +going to work, is it? I need that extra piece of information, I’m relying on +it, and it’s not there. There’s simply no way to go in the opposite direction +without providing that extra value.

+

Or is there? If you remember, we’d pointed out earlier that ErrorT is just a +simple wrapper around the inner monad. In other words, if I have errorValue +:: ErrorT MyError IO MyValue, I can apply runErrorT and get a value of +type IO (Either MyError MyValue). The looks quite a bit like bi-directional +translation, doesn’t it?

+

Well, not quite. We originally had an ErrorT MyError IO monad, with a value +of type MyValue. Now we have a monad of type IO with a value of type +Either MyError MyValue. So this process has in fact changed the value, while +the lifting process leaves it the same.

+

But still, with a little fancy footwork we can unwrap the ErrorT, do some +processing, and then wrap it back up again.

+
useMyFileError1 :: ErrorT MyError IO ()
+useMyFileError1 =
+    let unwrapped :: Handle -> IO (Either MyError ())
+        unwrapped handle = runErrorT $ sayHiError handle
+        applied :: IO (Either MyError ())
+        applied = withMyFile unwrapped
+        rewrapped :: ErrorT MyError IO ()
+        rewrapped = ErrorT applied
+     in rewrapped
+

This is the crucial point of this whole article, so look closely. We first +unwrap our monad. This means that, to the outside world, it’s now just a plain +old IO value. Internally, we’ve stored all the information from our ErrorT +transformer. Now that we have a plain old IO, we can easily pass it off to +withMyFile. withMyFile takes in the internal state and passes it back out +unchanged. Finally, we wrap everything back up into our original ErrorT.

+

This is the entire pattern of monad-control: we embed the extra features of our +monad transformer inside the value. Once in the value, the type system ignores +it and focuses on the inner monad. When we’re done playing around with that +inner monad, we can pull our state back out and reconstruct our original monad +stack.

+
+
+

Types

+

I purposely started with the ErrorT transformer, as it is one of the simplest +for this inversion mechanism. Unfortunately, others are a bit more complicated. +Take for instance ReaderT. It is defined as newtype ReaderT r m a = ReaderT { +runReaderT :: r -> m a }. If we apply runReaderT to it, we get a +function that returns a monadic value. So we’re going to need some extra +machinery to deal with all that stuff. And this is when we leave Kansas behind.

+

There are a few approaches to solving these problems. In the past, I +implemented a solution using type families in the neither package. Anders +Kaseorg implemented a much more straight-forward solution in monad-peel. And +for efficiency, in monad-control, Bas van Dijk uses CPS (continuation passing +style) and existential types.

+ +

The first type we’re going to look at is:

+
type Run t = forall n o b. (Monad n, Monad o, Monad (t o)) => t n b -> n (t o b)
+

That’s incredibly dense, let’s talk it out. The only "input" datatype to this +thing is t, a monad transformer. A Run is a function that will then work with +any combination of types n, o and b (that’s what the forall means). n and o +are both monads, while b is a simple value contained by them.

+

The left hand side of the Run function, t n b, is our monad transformer +wrapped around the n monad and holding a b value. So for example, that could be +a MyTrans FirstMonad MyValue. It then returns a value with the transformer +"popped" inside, with a brand new monad at its core. In other words, +FirstMonad (MyTrans NewMonad MyValue).

+

That might sound pretty scary at first, but it actually isn’t as foreign as +you’d think: this is essentially what we did with ErrorT. We started with +ErrorT on the outside, wrapping around IO, and ended up with an IO by itself +containing an Either. Well guess what: another way to represent an Either is +ErrorT MyError Identity. So essentially, we pulled the IO to the outside and +plunked an Identity in its place. We’re doing the same thing in a Run: pulling +the FirstMonad outside and replacing it with a NewMonad.

+ +

Alright, now we’re getting somewhere. If we had access to one of those Run +functions, we could use it to peel off the ErrorT on our sayHiError function +and pass it to withMyFile. With the magic of undefined, we can play such a +game:

+
errorRun :: Run (ErrorT MyError)
+errorRun = undefined
+
+useMyFileError2 :: IO (ErrorT MyError Identity ())
+useMyFileError2 =
+    let afterRun :: Handle -> IO (ErrorT MyError Identity ())
+        afterRun handle = errorRun $ sayHiError handle
+        applied :: IO (ErrorT MyError Identity ())
+        applied = withMyFile afterRun
+     in applied
+

This looks eerily similar to our previous example. In fact, errorRun is acting +almost identically to runErrorT. However, we’re still left with two problems: +we don’t know where to get that errorRun value from, and we still need to +restructure the original ErrorT after we’re done.

+
+

MonadTransControl

+

Obviously in the specific case we have before us, we could use our knowledge of +the ErrorT transformer to beat the types into submission and create our Run +function manually. But what we really want is a general solution for many +transformers. At this point, you know we need a typeclass.

+

So let’s review what we need: access to a Run function, and some way to +restructure our original transformer after the fact. And thus was born +MonadTransControl, with its single method liftControl:

+
class MonadTrans t => MonadTransControl t where
+    liftControl :: Monad m => (Run t -> m a) -> t m a
+

Let’s look at this closely. liftControl takes a function (the one we’ll be +writing). That function is provided with a Run function, and must return a +value in some monad (m). liftControl will then take the result of that function +and reinstate the original transformer on top of everything.

+
useMyFileError3 :: Monad m => ErrorT MyError IO (ErrorT MyError m ())
+useMyFileError3 =
+    liftControl inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

Close, but not exactly what I had in mind. What’s up with the double monads? +Well, let’s start at the end: sayHiError handle returns a value of type ErrorT +MyError IO (). This we knew already, no surprises. What might be a little +surprising (it got me, at least) is the next two steps.

+

First we apply run to that value. Like we’d discussed before, the result is +that the IO inner monad is popped to the outside, to be replaced by some +arbitrary monad (represented by m here). So we end up with an IO (ErrorT +MyError m ()). Ok… We then get the same result after applying withMyFile. Not +surprising.

+

The last step took me a long time to understand correctly. Remember how we said +that we reconstruct the original transformer? Well, so we do: by plopping it +right on top of everything else we have. So our end result is the previous +type- IO (ErrorT MyError m ())- with a ErrorT MyError stuck on the front.

+

Well, that seems just about utterly worthless, right? Well, almost. But don’t +forget, that "m" can be any monad, including IO. If we treat it that way, we +get ErrorT MyError IO (ErrorT MyError IO ()). That looks a lot like m (m +a), and we want just plain old m a. Fortunately, now we’re in luck:

+
useMyFileError4 :: ErrorT MyError IO ()
+useMyFileError4 = join useMyFileError3
+

And it turns out that this usage is so common, that Bas had mercy on us and +defined a helper function:

+
control :: (Monad m, Monad (t m), MonadTransControl t)
+        => (Run t -> m (t m a)) -> t m a
+control = join . liftControl
+

So all we need to write is:

+
useMyFileError5 :: ErrorT MyError IO ()
+useMyFileError5 =
+    control inside
+  where
+    inside :: Monad m => Run (ErrorT MyError) -> IO (ErrorT MyError m ())
+    inside run = withMyFile $ helper run
+    helper :: Monad m
+           => Run (ErrorT MyError) -> Handle -> IO (ErrorT MyError m ())
+    helper run handle = run (sayHiError handle :: ErrorT MyError IO ())
+

And just to make it a little shorter:

+
useMyFileError6 :: ErrorT MyError IO ()
+useMyFileError6 = control $ \run -> withMyFile $ run . sayHiError
+
+
+

MonadControlIO

+

The MonadTrans class provides the lift method, which allows you to lift an +action one level in the stack. There is also the MonadIO class that provides +liftIO, which lifts an IO action as far in the stack as desired. We have the +same breakdown in monad-control. But first, we need a corrolary to Run:

+
type RunInBase m base = forall b. m b -> base (m b)
+

Instead of dealing with a transformer, we’re dealing with two monads. base is +the underlying monad, and m is a stack built on top of it. RunInBase is a +function that takes a value of the entire stack, pops out that base, and puts +in on the outside. Unlike in the Run type, we don’t replace it with an +arbitrary monad, but with the original one. To use some more concrete types:

+
RunInBase (ErrorT MyError IO) IO = forall b. ErrorT MyError IO b -> IO (ErrorT MyError IO b)
+

This should look fairly similar to what we’ve been looking at so far, the only +difference is that we want to deal with a specific inner monad. Our +MonadControlIO class is really just an extension of MonadControlTrans using +this RunInBase.

+
class MonadIO m => MonadControlIO m where
+    liftControlIO :: (RunInBase m IO -> IO a) -> m a
+

Simply put, liftControlIO takes a function which receives a RunInBase. That +RunInBase can be used to strip down our monad to just an IO, and then +liftControlIO builds everything back up again. And like MonadControlTrans, it +comes with a helper function

+
controlIO :: MonadControlIO m => (RunInBase m IO -> IO (m a)) -> m a
+controlIO = join . liftControlIO
+

We can easily rewrite our previous example with it:

+
useMyFileError7 :: ErrorT MyError IO ()
+useMyFileError7 = controlIO $ \run -> withMyFile $ run . sayHiError
+

And as an advantage, it easily scales to multiple transformers:

+
sayHiCrazy :: Handle -> ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+sayHiCrazy handle = liftIO $ hPutStrLn handle "Madness!"
+
+useMyFileCrazy :: ReaderT Int (StateT Double (ErrorT MyError IO)) ()
+useMyFileCrazy = controlIO $ \run -> withMyFile $ run . sayHiCrazy
+
+
+
+

Real Life Examples

+

Let’s solve some real-life problems with this code. Probably the biggest +motivating use case is exception handling in a transformer stack. For example, +let’s say that we want to automatically run some cleanup code when an exception +is thrown. If this were normal IO code, we’d use:

+
onException :: IO a -> IO b -> IO a
+

But if we’re in the ErrorT monad, we can’t pass in either the action or the +cleanup. In comes controlIO to the rescue:

+
onExceptionError :: ErrorT MyError IO a
+                 -> ErrorT MyError IO b
+                 -> ErrorT MyError IO a
+onExceptionError action after = controlIO $ \run ->
+    run action `onException` run after
+

Let’s say we need to allocate some memory to store a Double in. In the IO +monad, we could just use the alloca function. Once again, our solution is +simple:

+
allocaError :: (Ptr Double -> ErrorT MyError IO b)
+            -> ErrorT MyError IO b
+allocaError f = controlIO $ \run -> alloca $ run . f
+
+
+

Lost State

+

Let’s rewind a bit to our onExceptionError. It uses onException under the +surface, which has a type signature: IO a -> IO b -> IO a. Let me ask +you something: what happened to the b in the output? Well, it was thoroughly +ignored. But that seems to cause us a bit of a problem. After all, we store our +transformer state information in the value of the inner monad. If we ignore it, +we’re essentially ignoring the monadic side effects as well!

+

And the answer is that, yes, this does happen with monad-control. Certain functions will drop some of the monadic side effects. This is put best by Bas, in the comments on the relevant functions:[quote]

+
+

Note, any monadic side effects in m of the "release" computation will be discarded; it is run only for its side effects in IO.

+
+

In practice, monad-control will usually be doing the right thing for you, but +you need to be aware that some side effects may disappear.

+
+
+

More Complicated Cases

+

In order to make our tricks work so far, we’ve needed to have functions that +give us full access to play around with their values. Sometimes, this isn’t the +case. Take, for instance:

+
addMVarFinalizer :: MVar a -> IO () -> IO ()
+

In this case, we are required to have no value inside our finalizer function. +Intuitively, the first thing we should notice is that there will be no way to +capture our monadic side effects. So how do we get something like this to +compile? Well, we need to explicitly tell it to drop all of its state-holding +information:

+
addMVarFinalizerError :: MVar a -> ErrorT MyError IO () -> ErrorT MyError IO ()
+addMVarFinalizerError mvar f = controlIO $ \run ->
+    return $ liftIO $ addMVarFinalizer mvar (run f >> return ())
+

Another case from the same module is:

+
modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b
+

Here, we have a restriction on the return type in the second argument: it must +be a tuple of the value passed to that function and the final return value. +Unfortunately, I can’t see a way of writing a little wrapper around modifyMVar +to make it work for ErrorT. Instead, in this case, I copied the definition of +modifyMVar and modified it:

+
modifyMVar :: MVar a
+           -> (a -> ErrorT MyError IO (a, b))
+           -> ErrorT MyError IO b
+modifyMVar m io =
+  Control.Exception.Control.mask $ \restore -> do
+    a      <- liftIO $ takeMVar m
+    (a',b) <- restore (io a) `onExceptionError` liftIO (putMVar m a)
+    liftIO $ putMVar m a'
+    return b
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/persistent.html b/public/book/persistent.html new file mode 100644 index 00000000..6df748a2 --- /dev/null +++ b/public/book/persistent.html @@ -0,0 +1,1653 @@ + Persistent :: Yesod Web Framework Book- Version 1.6 +
+

Persistent

+ + +

Forms deal with the boundary between the user and the application. Another +boundary we need to deal with is between the application and the storage layer. +Whether it be a SQL database, a YAML file, or a binary blob, odds are your +storage layer does not natively understand your application’s data types, and +you’ll need to perform some marshaling. Persistent is Yesod’s answer to data +storage- a type-safe, universal data store interface for Haskell.

+

Haskell has many different database bindings available. However, most of these +have little knowledge of a schema and therefore do not provide useful static +guarantees. They also force database-dependent APIs and data types on the +programmer.

+

Some Haskellers have attempted a more revolutionary route: creating Haskell +specific data stores that allow one to easily store any strongly typed Haskell +data. These options are great for certain use cases, but they constrain one to +the storage techniques provided by the library and do not interface well with +other languages.

+

In contrast, Persistent allows us to choose among existing databases that are +highly tuned for different data storage use cases, interoperate with other +programming languages, and to use a safe and productive query interface, while +still keeping the type safety of Haskell datatypes.

+

Persistent follows the guiding principles of type safety and concise, +declarative syntax. Some other nice features are:

+
    +
  • +

    +Database-agnostic. There is first class support for PostgreSQL, SQLite, MySQL + and MongoDB, with experimental Redis support. +

    +
  • +
  • +

    +Convenient data modeling. + Persistent lets you model relationships and use them in type-safe ways. + The default type-safe persistent API does not support joins, allowing support for a + wider number of storage layers. + Joins and other SQL specific functionality can be achieved through using + a raw SQL layer (with very little type safety). + An additional library, Esqueleto, + builds on top of the Persistent data model, adding type-safe joins and SQL functionality. +

    +
  • +
  • +

    +Automatic database migrations in non-production environments to speed up + development. +

    +
  • +
+

Persistent works well with Yesod, but it is quite +usable on its own as a standalone library. Most of this chapter will address +Persistent on its own.

+
+

Synopsis

+

The required dependencies for the below are: persistent, persistent-sqlite and persistent-template.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Database.Persist
+import           Database.Persist.Sqlite
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    johnId <- insert $ Person "John Doe" $ Just 35
+    janeId <- insert $ Person "Jane Doe" Nothing
+
+    insert $ BlogPost "My fr1st p0st" johnId
+    insert $ BlogPost "One more for good measure" johnId
+
+    oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+    liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+    john <- get johnId
+    liftIO $ print (john :: Maybe Person)
+
+    delete janeId
+    deleteWhere [BlogPostAuthorId ==. johnId]
+ +
+
+

Solving the boundary issue

+

Suppose you are storing information on people in a SQL database. Your table +might look something like:

+
CREATE TABLE person(id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, age INTEGER)
+

And if you are using a database like PostgreSQL, you can be guaranteed that the +database will never store some arbitrary text in your age field. (The same +cannot be said of SQLite, but let’s forget about that for now.) To mirror this +database table, you would likely create a Haskell datatype that looks something +like:

+
data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+

It looks like everything is type safe: the database schema matches our Haskell +datatypes, the database ensures that invalid data can never make it into our +data store, and everything is generally awesome. Well, until:

+
    +
  • +

    +You want to pull data from the database, and the database layer gives you the + data in an untyped format. +

    +
  • +
  • +

    +You want to find everyone older than 32, and you accidentally write "thirtytwo" + in your SQL statement. Guess what: that will compile just fine, and you won’t + find out you have a problem until runtime. +

    +
  • +
  • +

    +You decide you want to find the first 10 people alphabetically. No problem… + until you make a typo in your SQL. Once again, you don’t find out until + runtime. +

    +
  • +
+

In dynamic languages, the answer to these issues is unit testing. For +everything that can go wrong, make sure you write a test case. But as I am +sure you are aware by now, that doesn’t jive well with the Yesod approach to +things. We like to take advantage of Haskell’s strong typing to save us +wherever possible, and data storage is no exception.

+

So the question remains: how can we use Haskell’s type system to save the day?

+
+

Types

+

Like routing, there is nothing intrinsically difficult about type-safe data +access. It just requires a lot of monotonous, error prone, boiler plate code. +As usual, this means we can use the type system to keep us honest. And to avoid +some of the drudgery, we’ll use a sprinkling of Template Haskell.

+

PersistValue is the basic building block of Persistent. It is a sum type that +can represent data that gets sent to and from a database. Its definition is:

+
data PersistValue
+    = PersistText Text
+    | PersistByteString ByteString
+    | PersistInt64 Int64
+    | PersistDouble Double
+    | PersistRational Rational
+    | PersistBool Bool
+    | PersistDay Day
+    | PersistTimeOfDay TimeOfDay
+    | PersistUTCTime UTCTime
+    | 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
+

A PersistValue correlates to a column in a SQL database. In our person example +above, name and age would be our PersistValuess.

+

Each Persistent backend needs to know how to translate the relevant values into +something the database can understand. However, it would be awkward to have to +express all of our data simply in terms of these basic types. The next layer is +the PersistField typeclass, which defines how an arbitrary Haskell datatype +can be marshaled to and from a PersistValue.

+

To tie up the user side of the code, our last typeclass is PersistEntity. An +instance of PersistEntity correlates with a table in a SQL database. This +typeclass defines a number of functions and some associated types. To review, +we have the following correspondence between Persistent and SQL:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SQLPersistent

Datatypes (VARCHAR, INTEGER, etc)

PersistValue

Column

PersistField

Table

PersistEntity

+
+
+

Code Generation

+

In order to ensure that the PersistEntity instances match up properly with your +Haskell datatypes, Persistent takes responsibility for both. This is also good +from a DRY (Don’t Repeat Yourself) perspective: you only need to define your +entities once. Let’s see a quick example:

+
{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.TH
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+
+mkPersist sqlSettings [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+|]
+

We use a combination of Template Haskell and Quasi-Quotation (like when +defining routes): persistLowerCase is a quasi-quoter which converts a +whitespace-sensitive syntax into a list of entity definitions. "Lower case" +refers to the format of the generated table names. In this scheme, an +entity like SomeTable would become the SQL table some_table. You can also +declare your entities in a separate file using persistFileWith. mkPersist +takes that list of entities and declares:

+
    +
  • +

    +One Haskell datatype for each entity. +

    +
  • +
  • +

    +A PersistEntity instance for each datatype defined. +

    +
  • +
+

The example above generates code that looks like the following:

+
{-# LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings, GADTs #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Control.Monad.IO.Class (liftIO)
+import Control.Applicative
+
+data Person = Person
+    { personName :: !String
+    , personAge :: !Int
+    }
+  deriving Show
+
+type PersonId = Key Person
+
+instance PersistEntity Person where
+    newtype Key Person = PersonKey (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 Unique Person
+    type PersistEntityBackend Person = SqlBackend
+
+    toPersistFields (Person name age) =
+        [ SomePersistField name
+        , SomePersistField age
+        ]
+
+    fromPersistValues [nameValue, ageValue] = Person
+        <$> fromPersistValue nameValue
+        <*> fromPersistValue ageValue
+    fromPersistValues _ = Left "Invalid fromPersistValues input"
+
+    -- Information on each field, used internally to generate SQL statements
+    persistFieldDef PersonId = FieldDef
+        (HaskellName "Id")
+        (DBName "id")
+        (FTTypeCon Nothing "PersonId")
+        SqlInt64
+        []
+        True
+        NoReference
+    persistFieldDef PersonName = FieldDef
+        (HaskellName "name")
+        (DBName "name")
+        (FTTypeCon Nothing "String")
+        SqlString
+        []
+        True
+        NoReference
+    persistFieldDef PersonAge = FieldDef
+        (HaskellName "age")
+        (DBName "age")
+        (FTTypeCon Nothing "Int")
+        SqlInt64
+        []
+        True
+        NoReference
+

As you might expect, our Person datatype closely matches the definition we +gave in the original Template Haskell version. We also have a Generalized +Algebraic Datatype (GADT) which gives a separate constructor for each field. +This GADT encodes both the type of the entity and the type of the field. We use +its constructors throughout Persistent, such as to ensure that when we apply a +filter, the types of the filtering value match the field. There’s another +associated newtype for the database primary key of this entity.

+

We can use the generated Person type like any other Haskell type, and then +pass it off to other Persistent functions.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# 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, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+runSqlite' :: (MonadUnliftIO m) => Text -> ReaderT SqlBackend (NoLoggingT (ResourceT m)) a -> m a
+runSqlite' = runSqlite
+
+main :: IO ()
+main = runSqlite' ":memory:" $ do
+    michaelId <- insert $ Person "Michael" $ Just 26
+    michael <- get michaelId
+    liftIO $ print michael
+ +

We start off with some standard database connection code. In this case, we used +the single-connection functions. Persistent also comes built in with connection +pool functions, which we will generally want to use in production.

+

In this example, we have seen two functions: insert creates a new record in +the database and returns its ID. Like everything else in Persistent, IDs are +type safe. We’ll get into more details of how these IDs work later. So when you +call insert $ Person "Michael" 26, it gives you a value back of type +PersonId.

+

The next function we see is get, which attempts to load a value from the +database using an Id. In Persistent, you never need to worry that you are +using the key from the wrong table: trying to load up a different entity (like +House) using a PersonId will never compile.

+
+
+

PersistStore

+

One last detail is left unexplained from the previous example: what exactly +does runSqlite do, and what is that monad that our database actions are +running in?

+

All database actions require a parameter which is an instance of +PersistStore. As its name implies, every data store (PostgreSQL, SQLite, +MongoDB) has an instance of PersistStore. This is where all the translations +from PersistValue to database-specific values occur, where SQL query +generation happens, and so on.

+ +

runSqlite creates a single connection to a database using its supplied +connection string. For our test cases, we will use :memory:, which uses an +in-memory database. All of the SQL backends share the same instance of +PersistStore: SqlBackend. runSqlite then provides the SqlBackend value +as an environment parameter to the action via runReaderT.

+ +

One important thing to note is that everything which occurs inside a single +call to runSqlite runs in a single transaction. This has two important +implications:

+
    +
  • +

    +For many databases, committing a transaction can be a costly activity. By + putting multiple steps into a single transaction, you can speed up code + dramatically. +

    +
  • +
  • +

    +If an exception is thrown anywhere inside a single call to runSqlite, all + actions will be rolled back (assuming your backend has rollback support). +

    + +
  • +
+
+
+
+

Migrations

+

I’m sorry to tell you, but so far I have lied to you a bit: the example from +the previous section does not actually work. If you try to run it, you will get +an error message about a missing table.

+

For SQL databases, one of the major pains can be managing schema changes. +Instead of leaving this to the user, Persistent steps in to help, but you have +to ask it to help. Let’s see what this looks like:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# 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, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration $ migrate entityDefs $ entityDef (Nothing :: Maybe Person)
+    michaelId <- insert $ Person "Michael" $ Just 26
+    michael <- get michaelId
+    liftIO $ print michael
+

With this one little code change, Persistent will automatically create your +Person table for you. This split between runMigration and migrate allows +you to migrate multiple tables simultaneously.

+ +

This works when dealing with just a few entities, but can quickly get tiresome +once we are dealing with a dozen entities. Instead of repeating yourself, +Persistent provides a helper function, mkMigrate:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int
+    deriving Show
+Car
+    color String
+    make String
+    model String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do runMigration migrateAll
+

mkMigrate is a Template Haskell function which creates a new function that +will automatically call migrate on all entities defined in the persist +block. The share function is just a little helper that passes the information +from the persist block to each Template Haskell function and concatenates the +results.

+

Persistent has very conservative rules about what it will do during a +migration. It starts by loading up table information from the database, +complete with all defined SQL datatypes. It then compares that against the +entity definition given in the code. For the following cases, it will +automatically alter the schema:

+
    +
  • +

    +The datatype of a field changed. However, the database may object to this + modification if the data cannot be translated. +

    +
  • +
  • +

    +A field was added. However, if the field is not null, no default value is + supplied (we’ll discuss defaults later) and there is already data in the + database, the database will not allow this to happen. +

    +
  • +
  • +

    +A field is converted from not null to null. In the opposite case, Persistent + will attempt the conversion, contingent upon the database’s approval. +

    +
  • +
  • +

    +A brand new entity is added. +

    +
  • +
+

However, there are some cases that Persistent will not handle:

+
    +
  • +

    +Field or entity renames: Persistent has no way of knowing that "name" has now + been renamed to "fullName": all it sees is an old field called name and a new + field called fullName. +

    +
  • +
  • +

    +Field removals: since this can result in data loss, Persistent by default + will refuse to perform the action (you can force the issue by using + runMigrationUnsafe instead of runMigration, though it is not + recommended). +

    +
  • +
+

runMigration will print out the migrations it is running on stderr (you can +bypass this by using runMigrationSilent). Whenever possible, it uses ALTER +TABLE calls. However, in SQLite, ALTER TABLE has very limited abilities, and +therefore Persistent must resort to copying the data from one table to another.

+

Finally, if instead of performing a migration, you want Persistent to give +you hints about what migrations are necessary, use the printMigration +function. This function will print out the migrations which runMigration +would perform for you. This may be useful for performing migrations that +Persistent is not capable of, for adding arbitrary SQL to a migration, or just +to log what migrations occurred.

+
+
+

Uniqueness

+

In addition to declaring fields within an entity, you can also declare +uniqueness constraints. A typical example would be requiring that a username be +unique.

+
User
+    username Text
+    UniqueUsername username
+

While each field name must begin with a lowercase letter, the uniqueness +constraints must begin with an uppercase letter, since it will be represented +in Haskell as a data constructor.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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|
+Person
+    firstName String
+    lastName String
+    age Int
+    PersonName firstName lastName
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael" "Snoyman" 26
+    michael <- getBy $ PersonName "Michael" "Snoyman"
+    liftIO $ print michael
+

To declare a unique combination of fields, we add an extra line to our +declaration. Persistent knows that it is defining a unique constructor, since +the line begins with a capital letter. Each following word must be a field in +this entity.

+

The main restriction on uniqueness is that it can only be applied to non-null +fields. The reason for this is that the SQL standard is ambiguous on how +uniqueness should be applied to NULL (e.g., is NULL=NULL true or false?). +Besides that ambiguity, most SQL engines in fact implement rules which would be +contrary to what the Haskell datatypes anticipate (e.g., PostgreSQL says that +NULL=NULL is false, whereas Haskell says Nothing == Nothing is True).

+

In addition to providing nice guarantees at the database level about +consistency of your data, uniqueness constraints can also be used to perform +some specific queries within your Haskell code, like the getBy demonstrated +above. This happens via the Unique associated type. In the example above, we +end up with a new constructor:

+
PersonName :: String -> String -> Unique Person
+ +
+
+

Queries

+

Depending on what your goal is, there are different approaches to querying the +database. Some commands query based on a numeric ID, while others will filter. +Queries also differ in the number of results they return: some lookups should +return no more than one result (if the lookup key is unique) while others can +return many results.

+

Persistent therefore provides a few different query functions. As usual, we try +to encode as many invariants in the types as possible. For example, a query +that can return only 0 or 1 results will use a Maybe wrapper, whereas a query +returning many results will return a list.

+
+

Fetching by ID

+

The simplest query you can perform in Persistent is getting based on an ID. +Since this value may or may not exist, its return type is wrapped in a Maybe.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+maybePerson <- get personId
+case maybePerson of
+    Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+    Just person -> liftIO $ print person
+

This can be very useful for sites that provide URLs like /person/5. However, +in such a case, we don’t usually care about the Maybe wrapper, and just want +the value, returning a 404 message if it is not found. Fortunately, the +get404 (provided by the yesod-persistent package) function helps us out here. +We’ll go into more details when we see integration with Yesod.

+
+
+

Fetching by unique constraint

+

getBy is almost identical to get, except:

+
    +
  1. +

    +it takes a uniqueness constraint; that is, instead of an ID it takes a Unique value. +

    +
  2. +
  3. +

    +it returns an Entity instead of a value. An Entity is a combination of database ID and value. +

    +
  4. +
+
personId <- insert $ Person "Michael" "Snoyman" 26
+maybePerson <- getBy $ PersonName "Michael" "Snoyman"
+case maybePerson of
+    Nothing -> liftIO $ putStrLn "Just kidding, not really there"
+    Just (Entity personId person) -> liftIO $ print person
+

Like get404, there is also a getBy404 function.

+
+
+

Select functions

+

Most likely, you’re going to want more powerful queries. You’ll want to find +everyone over a certain age; all cars available in blue; all users without a +registered email address. For this, you need one of the select functions.

+

All the select functions use a similar interface, with slightly different outputs:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionReturns

selectSource

A Source 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 +recommend reading the +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.

+

selectList is the most commonly used, so we will cover it specifically. Understanding the others should be trivial after that.

+

selectList takes two arguments: a list of Filters, and a list of +SelectOpts. The former is what limits your results based on +characteristics; it allows for equals, less than, is member of, and such. +SelectOpts provides for three different features: sorting, limiting output +to a certain number of rows, and offsetting results by a certain number of +rows.

+ +

Let’s jump straight into an example of filtering, and then analyze it.

+
people <- selectList [PersonAge >. 25, PersonAge <=. 30] []
+liftIO $ print people
+

As simple as that example is, we really need to cover three points:

+
    +
  1. +

    +PersonAge is a constructor for an associated phantom type. That might sound +scary, but what’s important is that it uniquely identifies the "age" column of +the "person" table, and that it knows that the age field is an Int. (That’s +the phantom part.) +

    +
  2. +
  3. +

    +We have a bunch of Persistent filtering operators. They’re all pretty +straight-forward: just tack a period to the end of what you’d expect. There are +three gotchas here, I’ll explain below. +

    +
  4. +
  5. +

    +The list of filters is ANDed together, so that our constraint means "age is +greater than 25 AND age is less than or equal to 30". We’ll describe ORing +later. +

    +
  6. +
+

The one operator that’s surprisingly named is "not equals." We use !=., since +/=. is used for updates (for "divide-and-set", described later). Don’t worry: +if you use the wrong one, the compiler will catch you. The other two surprising +operators are the "is member" and "is not member". They are, respectively, +<-. and /<-. (both end with a period).

+

And regarding ORs, we use the ||. operator. For example:

+
people <- selectList
+    (       [PersonAge >. 25, PersonAge <=. 30]
+        ||. [PersonFirstName /<-. ["Adam", "Bonny"]]
+        ||. ([PersonAge ==. 50] ||. [PersonAge ==. 60])
+    )
+    []
+liftIO $ print people
+

This (completely nonsensical) example means: find people who are 26-30, +inclusive, OR whose names are neither Adam or Bonny, OR whose age is either 50 +or 60.

+
+

SelectOpt

+

All of our selectList calls have included an empty list as the second +parameter. That specifies no options, meaning: sort however the database wants, +return all results, and don’t skip any results. A SelectOpt has four +constructors that can be used to change all that.

+
+
+Asc +
+

+Sort by the given column in ascending order. This uses the same phantom type as filtering, such as PersonAge. +

+
+
+Desc +
+

+Same as Asc, in descending order. +

+
+
+LimitTo +
+

+Takes an Int argument. Only return up to the specified number of results. +

+
+
+OffsetBy +
+

+Takes an Int argument. Skip the specified number of results. +

+
+
+

The following code defines a function that will break down results into pages. +It returns all people aged 18 and over, and then sorts them by age (oldest +person first). For people with the same age, they are sorted alphabetically by +last name, then first name.

+
resultsForPage pageNumber = do
+    let resultsPerPage = 10
+    selectList
+        [ PersonAge >=. 18
+        ]
+        [ Desc PersonAge
+        , Asc PersonLastName
+        , Asc PersonFirstName
+        , LimitTo resultsPerPage
+        , OffsetBy $ (pageNumber - 1) * resultsPerPage
+        ]
+
+
+
+
+

Manipulation

+

Querying is only half the battle. We also need to be able to add data to and +modify existing data in the database.

+
+

Insert

+

It’s all well and good to be able to play with data in the database, but how +does it get there in the first place? The answer is the insert function. You +just give it a value, and it gives back an ID.

+

At this point, it makes sense to explain a bit of the philosophy behind +Persistent. In many other ORM solutions, the datatypes used to hold data are +opaque: you need to go through their defined interfaces to get at and modify +the data. That’s not the case with Persistent: we’re using plain old Algebraic +Data Types for the whole thing. This means you still get all the great benefits +of pattern matching, currying and everything else you’re used to.

+

However, there are a few things we can’t do. For one, there’s no way to +automatically update values in the database every time the record is updated in +Haskell. Of course, with Haskell’s normal stance of purity and immutability, +this wouldn’t make much sense anyway, so I don’t shed any tears over it.

+

However, there is one issue that newcomers are often bothered by: why are IDs +and values completely separate? It seems like it would be very logical to embed +the ID inside the value. In other words, instead of having:

+
data Person = Person { name :: String }
+

have

+
data Person = Person { personId :: PersonId, name :: String }
+

Well, there’s one problem with this right off the bat: how do we do an insert? If a Person needs to have an ID, and we get the ID by inserting, and an insert needs a Person, we have an impossible loop. We could solve this with undefined, but that’s just asking for trouble.

+

OK, you say, let’s try something a bit safer:

+
data Person = Person { personId :: Maybe PersonId, name :: String }
+

I definitely prefer insert $ Person Nothing "Michael" to insert $ Person +undefined "Michael". And now our types will be much simpler, right? For +example, selectList could return a simple [Person] instead of that ugly +[Entity SqlPersist Person].

+

The problem is that the "ugliness" is incredibly useful. Having Entity Person +makes it obvious, at the type level, that we’re dealing with a value that +exists in the database. Let’s say we want to create a link to another page that +requires the PersonId (not an uncommon occurrence as we’ll discuss later). +The Entity Person form gives us unambiguous access to that information; +embedding PersonId within Person with a Maybe wrapper means an extra +runtime check for Just, instead of a more error-proof compile time check.

+

Finally, there’s a semantic mismatch with embedding the ID within the value. +The Person is the value. Two people are identical (in the context of +Haskell) if all their fields are the same. By embedding the ID in the value, +we’re no longer talking about a person, but about a row in the database. +Equality is no longer really equality, it’s identity: is this the same +person, as opposed to an equivalent person.

+

In other words, there are some annoyances with having the ID separated out, but +overall, it’s the right approach, which in the grand scheme of things leads +to better, less buggy code.

+
+
+

Update

+

Now, in the context of that discussion, let’s think about updating. The simplest way to update is:

+
let michael = Person "Michael" 26
+    michaelAfterBirthday = michael { personAge = 27 }
+

But that’s not actually updating anything, it’s just creating a new Person +value based on the old one. When we say update, we’re not talking about +modifications to the values in Haskell. (We better not be of course, since +data in Haskell is immutable.)

+

Instead, we’re looking at ways of modifying rows in a table. And the simplest +way to do that is with the update function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+update personId [PersonAge =. 27]
+

update takes two arguments: an ID, and a list of Updates. The simplest +update is assignment, but it’s not always the best. What if you want to +increase someone’s age by 1, but you don’t have their current age? Persistent +has you covered:

+
haveBirthday personId = update personId [PersonAge +=. 1]
+

And as you might expect, we have all the basic mathematical operators: ++=., -=., *=., and /=. (full stop). These can be convenient for +updating a single record, but they are also essential for proper ACID +guarantees. Imagine the alternative: pull out a Person, increment the age, +and update the new value. If you have two threads/processes working on this +database at the same time, you’re in for a world of hurt (hint: race +conditions).

+

Sometimes you’ll want to update many rows at once (give all your employees a +5% pay increase, for example). updateWhere takes two parameters: a list of +filters, and a list of updates to apply.

+
updateWhere [PersonFirstName ==. "Michael"] [PersonAge *=. 2] -- it's been a long day
+

Occasionally, you’ll just want to completely replace the value in a database +with a different value. For that, you use (surprise) the replace function.

+
personId <- insert $ Person "Michael" "Snoyman" 26
+replace personId $ Person "John" "Doe" 20
+
+
+

Delete

+

As much as it pains us, sometimes we must part with our data. To do so, we have three functions:

+
+
+delete +
+

+Delete based on an ID +

+
+
+deleteBy +
+

+Delete based on a unique constraint +

+
+
+deleteWhere +
+

+Delete based on a set of filters +

+
+
+
personId <- insert $ Person "Michael" "Snoyman" 26
+delete personId
+deleteBy $ PersonName "Michael" "Snoyman"
+deleteWhere [PersonFirstName ==. "Michael"]
+

We can even use deleteWhere to wipe out all the records in a table, we just +need to give some hints to GHC as to what table we’re interested in:

+
    deleteWhere ([] :: [Filter Person])
+
+
+
+

Attributes

+

So far, we have seen a basic syntax for our persistLowerCase blocks: a line +for the name of our entities, and then an indented line for each field with two +words: the name of the field and the datatype of the field. Persistent handles +more than this: you can assign an arbitrary list of attributes after the first +two words on a line.

+

Suppose we want to have a Person entity with an (optional) age and the +timestamp of when he/she was added to the system. For entities already in the +database, we want to just use the current date-time for that timestamp.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+import Control.Monad.IO.Class
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    time <- liftIO getCurrentTime
+    runMigration migrateAll
+    insert $ Person "Michael" (Just 26) time
+    insert $ Person "Greg" Nothing time
+    return ()
+

Maybe is a built in, single word attribute. It makes the field optional. In +Haskell, this means it is wrapped in a Maybe. In SQL, it makes the column +nullable.

+

The default attribute is backend specific, and uses whatever syntax is +understood by the database. In this case, it uses the database’s built-in +CURRENT_TIME function. Suppose that we now want to add a field for a person’s +favorite programming language:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+ +

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:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    created UTCTime default=CURRENT_TIME
+    language String default='Haskell'
+    country String "default='El Salvador'"
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+

One last trick you can do with attributes is to specify the names to be used +for the SQL tables and columns. This can be convenient when interacting with +existing databases.

+
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"
+    PersonName firstName lastName
+    deriving Show
+|]
+

There are a number of other features to the entity definition syntax. An +up-to-date list is maintained +in the Persistent documentation.

+
+
+

Relations

+

Persistent allows references between your data types in a manner that is +consistent with supporting non-SQL databases. We do this by embedding an ID in +the related entity. So if a person has many cars:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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
+    name String
+    deriving Show
+Car
+    ownerId PersonId
+    name String
+    deriving Show
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    bruce <- insert $ Person "Bruce Wayne"
+    insert $ Car bruce "Bat Mobile"
+    insert $ Car bruce "Porsche"
+    -- this could go on a while
+    cars <- selectList [CarOwnerId ==. bruce] []
+    liftIO $ print cars
+

Using this technique, you can define one-to-many relationships. To define +many-to-many relationships, we need a join entity, which has a one-to-many +relationship with each of the original tables. It is also a good idea to use +uniqueness constraints on these. For example, to model a situation where we +want to track which people have shopped in which stores:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Data.Time
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+Store
+    name String
+PersonStore
+    personId PersonId
+    storeId StoreId
+    UniquePersonStore personId storeId
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    bruce <- insert $ Person "Bruce Wayne"
+    michael <- insert $ Person "Michael"
+
+    target <- insert $ Store "Target"
+    gucci <- insert $ Store "Gucci"
+    sevenEleven <- insert $ Store "7-11"
+
+    insert $ PersonStore bruce gucci
+    insert $ PersonStore bruce sevenEleven
+
+    insert $ PersonStore michael target
+    insert $ PersonStore michael sevenEleven
+
+    return ()
+
+
+

Closer look at types

+

So far, we’ve spoken about Person and PersonId without really explaining +what they are. In the simplest sense, for a SQL-only system, the PersonId +could just be type PersonId = Int64. However, that means there is nothing +binding a PersonId at the type level to the Person entity. As a result, you +could accidentally use a PersonId and get a Car. In order to model this +relationship, we could use phantom types. So, our next naive step would be:

+
newtype Key entity = Key Int64
+type PersonId = Key Person
+

And that works out really well, until you get to a backend that doesn’t use +Int64 for its IDs. And that’s not just a theoretical question; MongoDB uses +ByteStrings instead. So what we need is a key value that can contain an +Int and a ByteString. Seems like a great time for a sum type:

+
data Key entity = KeyInt Int64 | KeyByteString ByteString
+

But that’s just asking for trouble. Next we’ll have a backend that uses +timestamps, so we’ll need to add another constructor to Key. This could go on +for a while. Fortunately, we already have a sum type intended for representing +arbitrary data: PersistValue:

+
newtype Key entity = Key PersistValue
+

And this is (more or less) what Persistent did until version 2.0. However, this +has a different problem: it throws away data. For example, when dealing with a +SQL database, we know that the key type will be an Int64 (assuming defaults +are being used). However, you can’t assert that at the type level with this +construction. So instead, starting with Persistent 2.0, we now use an +associated datatype inside the PersistEntity class:

+
class PersistEntity record where
+    data Key record
+    ...
+

When you’re working with a SQL backend, and aren’t using a custom key type, +this becomes a newtype wrapper around an Int64, and the +toSqlKey/fromSqlKey functions can perform that type-safe conversion for +you. With MongoDB, on the other hand, it’s a wrapper around a ByteString.

+
+

More complicated, more generic

+

By default, Persistent will hard-code your datatypes to work with a specific +database backend. When using sqlSettings, this is the SqlBackend type. But +if you want to write Persistent code that can be used on multiple backends, you +can enable more generic types by replacing sqlSettings with sqlSettings { +mpsGeneric = True }.

+

To understand why this is necessary, consider relations. Let’s say we want to +represent blogs and blog posts. We would use the entity definition:

+
Blog
+    title Text
+Post
+    title Text
+    blogId BlogId
+

We know that BlogId is just a type synonym for Key Blog, but how will Key +Blog be defined? We can’t use an Int64, since that won’t work for MongoDB. +And we can’t use ByteString, since that won’t work for SQL databases.

+

To allow for this, once mpsGeneric is set to True, our resulting datatypes have a type parameter to indicate the database backend they use, so that keys can be properly encoded. This looks like:

+
data BlogGeneric backend = Blog { blogTitle :: Text }
+data PostGeneric backend = Post
+    { postTitle  :: Text
+    , postBlogId :: Key (BlogGeneric backend)
+    }
+

Notice that we still keep the short names for the constructors and the records. +Finally, to give a simple interface for normal code, we define some type +synonyms:

+
type Blog   = BlogGeneric SqlBackend
+type BlogId = Key Blog
+type Post   = PostGeneric SqlBackend
+type PostId = Key Post
+

And no, SqlBackend isn’t hard-coded into Persistent anywhere. That +sqlSettings parameter you’ve been passing to mkPersist is what tells us to +use SqlBackend. Mongo code will use mongoSettings instead.

+

This might be quite complicated under the surface, but user code hardly ever +touches this. Look back through this whole chapter: not once did we need to +deal with the Key or Generic stuff directly. The most common place for it +to pop up is in compiler error messages. So it’s important to be aware that +this exists, but it shouldn’t affect you on a day-to-day basis.

+
+
+
+

Custom Fields

+

Occasionally, you will want to define a custom field to be used in your +datastore. The most common case is an enumeration, such as employment status. +For this, Persistent provides a helper Template Haskell function:

+
-- @Employment.hs
+{-# LANGUAGE TemplateHaskell #-}
+module Employment where
+
+import Database.Persist.TH
+
+data Employment = Employed | Unemployed | Retired
+    deriving (Show, Read, Eq)
+derivePersistField "Employment"
+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import Database.Persist.Sqlite
+import Database.Persist.TH
+import Employment
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    employment Employment
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+
+    insert $ Person "Bruce Wayne" Retired
+    insert $ Person "Peter Parker" Unemployed
+    insert $ Person "Michael" Employed
+
+    return ()
+

derivePersistField stores the data in the database using a string field, and +performs marshaling using the Show and Read instances of the datatype. This +may not be as efficient as storing via an integer, but it is much more future +proof: even if you add extra constructors in the future, your data will still +be valid.

+ +
+
+

Persistent: Raw SQL

+

The Persistent package provides a type safe interface to data stores. It tries +to be backend-agnostic, such as not relying on relational features of SQL. My +experience has been you can easily perform 95% of what you need to do with the +high-level interface. (In fact, most of my web apps use the high level +interface exclusively.)

+

But occasionally you’ll want to use a feature that’s specific to a backend. One feature I’ve used in the past is full text search. In this case, we’ll use the SQL "LIKE" operator, which is not modeled in Persistent. We’ll get all people with the last name "Snoyman" and print the records out.

+ +
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+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
+    name Text
+|]
+
+main :: IO ()
+main = runSqlite ":memory:" $ do
+    runMigration migrateAll
+    insert $ Person "Michael Snoyman"
+    insert $ Person "Miriam Snoyman"
+    insert $ Person "Eliezer Snoyman"
+    insert $ Person "Gavriella Snoyman"
+    insert $ Person "Greg Weber"
+    insert $ Person "Rick Richardson"
+
+    -- 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)
+

There is also higher-level support that allows for automated data marshaling. +Please see the Haddock API docs for more details.

+
+
+

Integration with Yesod

+

So you’ve been convinced of the power of Persistent. How do you integrate it +with your Yesod application? If you use the scaffolding, most of the work is +done for you already. But as we normally do, we’ll build up everything manually +here to point out how it works under the surface.

+

The yesod-persistent package provides the meeting point between Persistent and +Yesod. It provides the YesodPersist typeclass, which standardizes access to +the database via the runDB method. Let’s see this in action.

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import Yesod
+import Database.Persist.Sqlite
+import Control.Monad.Trans.Resource (runResourceT)
+import Control.Monad.Logger (runStderrLoggingT)
+
+-- Define our entities as usual
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    firstName String
+    lastName String
+    age Int
+    deriving Show
+|]
+
+-- We keep our connection pool in the foundation. At program initialization, we
+-- create our initial pool, and each time we need to perform an action we check
+-- out a single connection from the pool.
+data PersistTest = PersistTest ConnectionPool
+
+-- We'll create a single route, to access a person. It's a very common
+-- occurrence to use an Id type in routes.
+mkYesod "PersistTest" [parseRoutes|
+/ HomeR GET
+/person/#PersonId PersonR GET
+|]
+
+-- Nothing special here
+instance Yesod PersistTest
+
+-- Now we need to define a YesodPersist instance, which will keep track of
+-- which backend we're using and how to run an action.
+instance YesodPersist PersistTest where
+    type YesodPersistBackend PersistTest = SqlBackend
+
+    runDB action = do
+        PersistTest pool <- getYesod
+        runSqlPool action pool
+
+-- List all people in the database
+getHomeR :: Handler Html
+getHomeR = do
+    people <- runDB $ selectList [] [Asc PersonAge]
+    defaultLayout
+        [whamlet|
+            <ul>
+                $forall Entity personid person <- people
+                    <li>
+                        <a href=@{PersonR personid}>#{personFirstName person}
+        |]
+
+-- We'll just return the show value of a person, or a 404 if the Person doesn't
+-- exist.
+getPersonR :: PersonId -> Handler String
+getPersonR personId = do
+    person <- runDB $ get404 personId
+    return $ show person
+
+openConnectionCount :: Int
+openConnectionCount = 10
+
+main :: IO ()
+main = runStderrLoggingT $ withSqlitePool "test.db3" openConnectionCount $ \pool -> liftIO $ do
+    runResourceT $ flip runSqlPool pool $ do
+        runMigration migrateAll
+        insert $ Person "Michael" "Snoyman" 26
+    warp 3000 $ PersistTest pool
+

There are two important pieces here for general use. runDB is used to run a +DB action from within a Handler. Within the runDB, you can use any of the +functions we’ve spoken about so far, such as insert and selectList.

+ +

The other new feature is get404. It works just like get, but instead of +returning a Nothing when a result can’t be found, it returns a 404 message +page. The getPersonR function is a very common approach used in real-world +Yesod applications: get404 a value and then return a response based on it.

+
+
+

More complex SQL

+

Persistent strives to be backend-agnostic. The advantage of this approach is +code which easily moves from different backend types. The downside is that you +lose out on some backend-specific features. Probably the biggest casualty is +SQL join support.

+

Fortunately, thanks to Felipe Lessa and Chris Allen, you can have your cake and eat it too. The +Esqueleto library provides +support for writing type safe SQL queries, using the existing Persistent +infrastructure. The Haddocks for that package provide a good introduction to +its usage. And since it uses many Persistent concepts, most of your existing +Persistent knowledge should transfer over easily.

+

For a simple example of using Esqueleto, please see the SQL Joins chapter.

+
+
+

Something besides SQLite

+

To keep the examples in this chapter simple, we’ve used the SQLite backend. Just to round things out, here’s our original synopsis rewritten to work with PostgreSQL:

+
{-# LANGUAGE DerivingStrategies         #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.IO.Class  (liftIO)
+import           Control.Monad.Logger    (runStderrLoggingT)
+import           Database.Persist
+import           Database.Persist.Postgresql
+import           Database.Persist.TH
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Person
+    name String
+    age Int Maybe
+    deriving Show
+BlogPost
+    title String
+    authorId PersonId
+    deriving Show
+|]
+
+connStr = "host=localhost dbname=test user=test password=test port=5432"
+
+main :: IO ()
+main = runStderrLoggingT $ withPostgresqlPool connStr 10 $ \pool -> liftIO $ do
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        johnId <- insert $ Person "John Doe" $ Just 35
+        janeId <- insert $ Person "Jane Doe" Nothing
+
+        insert $ BlogPost "My fr1st p0st" johnId
+        insert $ BlogPost "One more for good measure" johnId
+
+        oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+        liftIO $ print (oneJohnPost :: [Entity BlogPost])
+
+        john <- get johnId
+        liftIO $ print (john :: Maybe Person)
+
+        delete janeId
+        deleteWhere [BlogPostAuthorId ==. johnId]
+
+
+

Summary

+

Persistent brings the type safety of Haskell to your data access layer. Instead +of writing error-prone, untyped data access, or manually writing boilerplate +marshal code, you can rely on Persistent to automate the process for you.

+

The goal is to provide everything you need, most of the time. For the times +when you need something a bit more powerful, Persistent gives you direct access +to the underlying data store, so you can write whatever 5-way joins you want.

+

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

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/restful-content.html b/public/book/restful-content.html new file mode 100644 index 00000000..1590e3e1 --- /dev/null +++ b/public/book/restful-content.html @@ -0,0 +1,600 @@ + RESTful Content :: Yesod Web Framework Book- Version 1.6 +
+

RESTful Content

+ + +

One of the stories from the early days of the web is how search engines wiped +out entire websites. When dynamic web sites were still a new concept, +developers didn’t appreciate the difference between a GET and POST request. +As a result, they created pages- accessed with the GET method- that would +delete pages. When search engines started crawling these sites, they could wipe +out all the content.

+

If these web developers had followed the HTTP spec properly, this would not +have happened. A GET request is supposed to cause no side effects (you know, +like wiping out a site). Recently, there has been a move in web development to +properly embrace Representational State Transfer, also known as REST. This +chapter describes the RESTful features in Yesod and how you can use them to +create more robust web applications.

+
+

Request methods

+

In many web frameworks, you write one handler function per resource. In Yesod, +the default is to have a separate handler function for each request method. The +two most common request methods you will deal with in creating web sites are +GET and POST. These are the most well-supported methods in HTML, since they +are the only ones supported by web forms. However, when creating RESTful APIs, +the other methods are very useful.

+

Technically speaking, you can create whichever request methods you like, but it +is strongly recommended to stick to the ones spelled out in the HTTP spec. The +most common of these are:

+
+
+GET +
+

+Read-only requests. Assuming no other changes occur on the server, +calling a GET request multiple times should result in the same response, +barring such things as "current time" or randomly assigned results. +

+
+
+POST +
+

+A general mutating request. A POST request should never be submitted +twice by the user. A common example of this would be to transfer funds from one +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. +

+
+
+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 +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.

+

See the routing and handlers chapter for more information on the syntax +of defining handler functions for each request method.

+
+
+

Representations

+

Suppose we have a Haskell datatype and value:

+
data Person = Person { name :: String, age :: Int }
+michael = Person "Michael" 25
+

We could represent that data as HTML:

+
<table>
+    <tr>
+        <th>Name</th>
+        <td>Michael</td>
+    </tr>
+    <tr>
+        <th>Age</th>
+        <td>25</td>
+    </tr>
+</table>
+

or we could represent it as JSON:

+
{"name":"Michael","age":25}
+

or as XML:

+
<person>
+    <name>Michael</name>
+    <age>25</age>
+</person>
+

Often times, web applications will use a different URL to get each of these +representations; perhaps /person/michael.html, /person/michael.json, etc. +Yesod follows the RESTful principle of a single URL for each resource. So in +Yesod, all of these would be accessed from /person/michael.

+

Then the question becomes how do we determine which representation to serve. +The answer is the HTTP Accept header: it gives a prioritized list of content +types the client is expecting. Yesod provides a pair of functions to abstract +away the details of parsing that header directly, and instead allows you to +talk at a much higher level of representations. Let’s make that last sentence +a bit more concrete with some code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideRep $ return $ object
+        [ "name" .= name
+        , "age" .= age
+        ]
+  where
+    name = "Michael" :: Text
+    age = 28 :: Int
+
+main :: IO ()
+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 +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 +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 +little bit.

+

To test this out, start up the server and then try running the following +different curl commands:

+
curl http://localhost:3000 --header "accept: application/json"
+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 +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 +"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 +from your browser. To try this out, you can visit +http://localhost:3000/?_accept=application/json.

+
+

JSON conveniences

+

Since JSON is such a commonly used data format in web applications today, we +have some built-in helper functions for providing JSON representations. These +are built off of the wonderful aeson library, so let’s start off with a quick +explanation of how that library works.

+

aeson has a core datatype, Value, which represents any valid JSON value. It +also provides two typeclasses- ToJSON and FromJSON- to automate +marshaling to and from JSON values, respectively. For our purposes, we’re +currently interested in ToJSON. Let’s look at a quick example of creating a +ToJSON instance for our ever-recurring Person data type examples.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+import           Data.Aeson
+import qualified Data.ByteString.Lazy.Char8 as L
+import           Data.Text                  (Text)
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+main :: IO ()
+main = L.putStrLn $ encode $ Person "Michael" 28
+

I won’t go into further detail on aeson, as +the Haddock documentation +already provides a great introduction to the library. What I’ve described so +far is enough to understand our convenience functions.

+

Let’s suppose that you have such a Person datatype, with a corresponding +value, and you’d like to use it as the representation for your current page. +For that, you can use the returnJson function.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Value
+getHomeR = returnJson $ Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

returnJson is actually a trivial function; it is implemented as return . +toJSON. However, it makes things just a bit more convenient. Similarly, if you +would like to provide a JSON value as a representation inside a selectRep, +you can use provideJson.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return
+        [shamlet|
+            <p>Hello, my name is #{name} and I am #{age} years old.
+        |]
+    provideJson person
+  where
+    person@Person {..} = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

provideJson is similarly trivial, in this case provideRep . return . toEncoding.

+
+
+

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 +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.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+getHomeR :: Handler TypedContent
+getHomeR =
+    return $ TypedContent mimeType $ toContent $ show person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

There are a few important things to note here.

+
    +
  • +

    +We’ve used the toContent function. This is a typeclass function that can + convert a number of data types to raw data ready to be sent over the wire. In + this case, we’ve used the instance for String, which uses UTF8 encoding. + Other common data types with instances are Text, ByteString, Html, and + 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 + alias for a strict ByteString. +

    +
  • +
+

That’s all well and good, but it bothers me that the type signature for +getHomeR is so uninformative. Also, the implementation of getHomeR looks +pretty boilerplate. I’d rather just have a datatype representing "Haskell Show" +data, and provide some simple means of creating such values. Let’s try this on +for size:

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+
+getHomeR :: Handler HaskellShow
+getHomeR =
+    return $ HaskellShow person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+main = warp 3000 App
+

The magic here lies in two typeclasses. As we mentioned before, ToContent +tells how to convert a value into a raw response. In our case, we would like to +show the original value to get a String, and then convert that String +into the raw content. Often times, instances of ToContent will build on each +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.

+

Finally, let’s make this a bit more complicated, and get this to play well with +selectRep.

+
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE RecordWildCards           #-}
+{-# LANGUAGE TemplateHaskell           #-}
+{-# LANGUAGE TypeFamilies              #-}
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { name :: Text
+    , age  :: Int
+    }
+    deriving Show
+
+instance ToJSON Person where
+    toJSON Person {..} = object
+        [ "name" .= name
+        , "age"  .= age
+        ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+mimeType :: ContentType
+mimeType = "text/haskell-show"
+
+data HaskellShow = forall a. Show a => HaskellShow a
+
+instance ToContent HaskellShow where
+    toContent (HaskellShow x) = toContent $ show x
+instance ToTypedContent HaskellShow where
+    toTypedContent = TypedContent mimeType . toContent
+instance HasContentType HaskellShow where
+    getContentType _ = mimeType
+
+getHomeR :: Handler TypedContent
+getHomeR = selectRep $ do
+    provideRep $ return $ HaskellShow person
+    provideJson person
+  where
+    person = Person "Michael" 28
+
+main :: IO ()
+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. +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.

+ +
+
+
+

Other request headers

+

There are a great deal of other request headers available. Some of them only +affect the transfer of data between the server and client, and should not +affect the application at all. For example, Accept-Encoding informs the +server which compression schemes the client understands, and Host informs the +server which virtual host to serve up.

+

Other headers do affect the application, but are automatically read by Yesod. +For example, the Accept-Language header specifies which human language +(English, Spanish, German, Swiss-German) the client prefers. See the i18n +chapter for details on how this header is used.

+
+
+

Summary

+

Yesod adheres to the following tenets of REST:

+
    +
  • +

    +Use the correct request method. +

    +
  • +
  • +

    +Each resource should have precisely one URL. +

    +
  • +
  • +

    +Allow multiple representations of data on the same URL. +

    +
  • +
  • +

    +Inspect request headers to determine extra information about what the client wants. +

    +
  • +
+

This makes it easy to use Yesod not just for building websites, but for +building APIs. In fact, using techniques such as selectRep/provideRep, you +can serve both a user-friendly, HTML page and a machine-friendly, JSON page +from the same URL.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/route-attributes.html b/public/book/route-attributes.html new file mode 100644 index 00000000..7baf762a --- /dev/null +++ b/public/book/route-attributes.html @@ -0,0 +1,321 @@ + Route attributes :: Yesod Web Framework Book- Version 1.6 +
+

Route attributes

+ + +

Route attributes allow you to set some metadata on each of your routes, in the +routes description itself. The syntax is trivial: just an exclamation point +followed by a value. Using it is also trivial: just use the routeAttrs +function.

+

It’s easiest to understand how it all fits together, and when you might want it, with a motivating example. The case I personally most use this for is annotating administrative routes. Imagine having a website with about 12 different admin actions. You could manually add a call to requireAdmin or some such at the beginning of each action, but:

+
    +
  1. +

    +It’s tedious. +

    +
  2. +
  3. +

    +It’s error prone: you could easily forget one. +

    +
  4. +
  5. +

    +Worse yet, it’s not easy to notice that you’ve missed one. +

    +
  6. +
+

Modifying your isAuthorized method with an explicit list of administrative +routes is a bit better, but it’s still difficult to see at a glance when you’ve +missed one.

+

This is why I like to use route attributes for this: you add a single word to +each relevant part of the route definition, and then you just check for that +attribute in isAuthorized. Let’s see the code!

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/unprotected UnprotectedR GET
+/admin1 Admin1R GET !admin
+/admin2 Admin2R GET !admin
+/admin3 Admin3R GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+    isAuthorized route _writable
+        | "admin" `member` routeAttrs route = do
+            muser <- maybeAuthId
+            case muser of
+                Nothing -> return AuthenticationRequired
+                Just ident
+                    -- Just a hack since we're using the dummy module
+                    | ident == "admin" -> return Authorized
+                    | otherwise -> return $ Unauthorized "Admin access only"
+        | otherwise = return Authorized
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Hacky YesodAuth instance for just the dummy auth plugin
+instance YesodAuth App where
+    type AuthId App = Text
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authPlugins _ = [authDummy]
+    maybeAuthId = lookupSession credsKey
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Route attr homepage"
+    [whamlet|
+        <p>
+            <a href=@{UnprotectedR}>Unprotected
+        <p>
+            <a href=@{Admin1R}>Admin 1
+        <p>
+            <a href=@{Admin2R}>Admin 2
+        <p>
+            <a href=@{Admin3R}>Admin 3
+    |]
+
+getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
+getUnprotectedR = defaultLayout [whamlet|Unprotected|]
+getAdmin1R = defaultLayout [whamlet|Admin1|]
+getAdmin2R = defaultLayout [whamlet|Admin2|]
+getAdmin3R = defaultLayout [whamlet|Admin3|]
+
+main :: IO ()
+main = warp 3000 App
+

And it was so glaring, I bet you even caught the security hole about Admin3R.

+
+

Alternative approach: hierarchical routes

+

Another approach that can be used in some cases is hierarchical routes. This +allows you to group a number of related routes under a single parent. If you +want to keep all of your admin routes under a single URL structure (e.g., +/admin), this can be a good solution. Using them is fairly simple. You need +to add a line to your routes declaration with a path, a name, and a colon, +e.g.:

+
/admin AdminR:
+

Then, you place all children routes beneath that line, and indented at least one space, e.g.:

+
    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+

To refer to these routes using type-safe URLs, you simply wrap them with the +AdminR constructor, e.g. AdminR Admin1R. Here is the previous route +attribute example rewritten to use hierarchical routes:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.Set         (member)
+import           Data.Text        (Text)
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/unprotected UnprotectedR GET
+/admin AdminR:
+    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+/auth AuthR Auth getAuth
+|]
+
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR
+    isAuthorized (AdminR _) _writable = do
+        muser <- maybeAuthId
+        case muser of
+            Nothing -> return AuthenticationRequired
+            Just ident
+                -- Just a hack since we're using the dummy module
+                | ident == "admin" -> return Authorized
+                | otherwise -> return $ Unauthorized "Admin access only"
+    isAuthorized _route _writable = return Authorized
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Hacky YesodAuth instance for just the dummy auth plugin
+instance YesodAuth App where
+    type AuthId App = Text
+
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    authPlugins _ = [authDummy]
+    maybeAuthId = lookupSession credsKey
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Route attr homepage"
+    [whamlet|
+        <p>
+            <a href=@{UnprotectedR}>Unprotected
+        <p>
+            <a href=@{AdminR Admin1R}>Admin 1
+        <p>
+            <a href=@{AdminR Admin2R}>Admin 2
+        <p>
+            <a href=@{AdminR Admin3R}>Admin 3
+    |]
+
+getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
+getUnprotectedR = defaultLayout [whamlet|Unprotected|]
+getAdmin1R = defaultLayout [whamlet|Admin1|]
+getAdmin2R = defaultLayout [whamlet|Admin2|]
+getAdmin3R = defaultLayout [whamlet|Admin3|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Hierarchical routes with attributes

+

Of course, you can mix the two approaches. Children of a hierarchical route will inherit the attributes of their parents, e.g.:

+
/admin AdminR !admin:
+    /1 Admin1R GET !1
+    /2 Admin2R GET !2
+    /3 Admin3R GET !3
+

AdminR Admin1R has the admin and 1 attributes.

+

With this technique, you can use the admin attributes in the isAuthorized function, like in the first example. +You are also sure that you won’t forget any attributes as we did with Admin3R. +Compared to the original code corresponding to the hierarchical route, this method has no real benefit : both methods being somehow equivalent. +We replaced the pattern matching on (AdminR _) with "admin" member routeAttrs route. +However, the benefit becomes more obvious when the admin pages are not all grouped under the same url structures but belong to different subtrees, e.g:

+
/admin AdminR !admin:
+    /1 Admin1R GET
+    /2 Admin2R GET
+    /3 Admin3R GET
+
+/a AR !a:
+  /1 A1R GET
+  /2 A2R GET
+  /admin AAdminR !admin:
+    /1 AAdmin1R GET
+    /2 AAdmin2R GET
+

The pages under /admin and /a/admin have all the admin attribute and can be checked using "admin" member routeAttrs route. Pattern matching on (AdminR _) will not work for this example and only match /admin/* routes but not /a/admin/\*

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/routing-and-handlers.html b/public/book/routing-and-handlers.html new file mode 100644 index 00000000..68b1b9d9 --- /dev/null +++ b/public/book/routing-and-handlers.html @@ -0,0 +1,814 @@ + Routing and Handlers :: Yesod Web Framework Book- Version 1.6 +
+

Routing and Handlers

+ + +

If we look at Yesod as a Model-View-Controller framework, routing and handlers +make up the controller. For contrast, let’s describe two other routing +approaches used in other web development environments:

+
    +
  • +

    +Dispatch based on file name. This is how PHP and ASP work, for example. +

    +
  • +
  • +

    +Have a centralized routing function that parses routes based on regular + expressions. Django and Rails follow this approach. +

    +
  • +
+

Yesod is closer in principle to the latter technique. Even so, there are +significant differences. Instead of using regular expressions, Yesod matches on +pieces of a route. Instead of having a one-way route-to-handler mapping, Yesod +has an intermediate data type (called the route datatype, or a type-safe URL) +and creates two-way conversion functions.

+

Coding this more advanced system manually is tedious and error prone. +Therefore, Yesod defines a Domain Specific Language (DSL) for specifying +routes, and provides Template Haskell functions to convert this DSL to Haskell +code. This chapter will explain the syntax of the routing declarations, give +you a glimpse of what code is generated for you, and explain the interaction +between routing and handler functions.

+
+

Route Syntax

+

Instead of trying to shoe-horn route declarations into an existing syntax, +Yesod’s approach is to use a simplified syntax designed just for routes. This +has the advantage of making the code not only easy to write, but simple enough +for someone with no Yesod experience to read and understand the sitemap of your +application.

+

A basic example of this syntax is:

+
/             HomeR     GET
+/blog         BlogR     GET POST
+/blog/#BlogId BlogPostR GET POST
+
+/static       StaticR   Static getStatic
+

The next few sections will explain the full details of what goes on in the +route declaration.

+
+

Pieces

+

One of the first things Yesod does when it gets a request is split up the +requested path into pieces. The pieces are tokenized at all forward slashes. +For example:

+
toPieces "/" = []
+toPieces "/foo/bar/baz/" = ["foo", "bar", "baz", ""]
+

You may notice that there are some funny things going on with trailing slashes, +or double slashes ("/foo//bar//"), or a few other things. Yesod believes in +having canonical URLs; if users request a URL with a trailing slash, or with a +double slash, they are automatically redirected to the canonical version. This +ensures you have one URL for one resource, and can help with your search +rankings.

+

What this means for you is that you needn’t concern yourself with the exact +structure of your URLs: you can safely think about pieces of a path, and Yesod +automatically handles intercalating the slashes and escaping problematic +characters.

+

If, by the way, you want more fine-tuned control of how paths are split into +pieces and joined together again, you’ll want to look at the cleanPath and +joinPath methods in the Yesod typeclass chapter.

+
+

Types of Pieces

+

When you are declaring your routes, you have three types of pieces at your +disposal:

+
+
+Static +
+

+This is a plain string that must be matched against precisely in the URL. +

+
+
+Dynamic single +
+

+This is a single piece (ie, between two forward slashes), but +represents a user-submitted value. This is the primary method of receiving +extra user input on a page request. These pieces begin with a hash (#) and are +followed by a data type. The datatype must be an instance of PathPiece. +

+
+
+Dynamic multi +
+

+The same as before, but can receive multiple pieces of the URL. +This must always be the last piece in a resource pattern. It is specified by an +asterisk (*) followed by a datatype, which must be an instance of +PathMultiPiece. Multi pieces are not as common as the other two, though they +are very important for implementing features like static trees representing +file structure or wikis with arbitrary hierarchies. +

+
+
+ +

Let us take a look at some standard kinds of resource patterns you may want to +write. Starting simply, the root of an application will just be /. Similarly, +you may want to place your FAQ at /page/faq.

+

Now let’s say you are going to write a Fibonacci website. You may construct +your URLs like /fib/#Int. But there’s a slight problem with this: we do not +want to allow negative numbers or zero to be passed into our application. +Fortunately, the type system can protect us:

+
newtype Natural = Natural Int
+    deriving (Eq, Show, Read)
+
+instance PathPiece Natural where
+    toPathPiece (Natural i) = T.pack $ show i
+    fromPathPiece s =
+        case reads $ T.unpack s of
+            (i, ""):_
+                | i < 1 -> Nothing
+                | otherwise -> Just $ Natural i
+            [] -> Nothing
+

On line 1 we define a simple newtype wrapper around Int to protect ourselves +from invalid input. We can see that PathPiece is a typeclass with two +methods. toPathPiece does nothing more than convert to a Text. +fromPathPiece attempts to convert a Text to our datatype, returning +Nothing when this conversion is impossible. By using this datatype, we can +ensure that our handler function is only ever given natural numbers, allowing +us to once again use the type system to battle the boundary issue.

+ +

Defining a PathMultiPiece is just as simple. Let’s say we want to have a Wiki +with at least two levels of hierarchy; we might define a datatype such as:

+
data Page = Page Text Text [Text] -- 2 or more
+    deriving (Eq, Show, Read)
+
+instance PathMultiPiece Page where
+    toPathMultiPiece (Page x y z) = x : y : z
+    fromPathMultiPiece (x:y:z) = Just $ Page x y z
+    fromPathMultiPiece _ = Nothing
+
+
+

Overlap checking

+

By default, Yesod will ensure that no two routes have the potential to overlap +with each other. So, for example, consider the following routes:

+
/foo/bar   Foo1R GET
+/foo/#Text Foo2R GET
+

This route declaration will be rejected as overlapping, since /foo/bar will +match both routes. However, there are two cases where we may wish to allow +overlapping:

+
    +
  1. +

    +We know by the definition of our datatype that the overlap can never happen. For example, if you replace Text with Int above, it’s easy to convince yourself that there’s no route that exists that will overlap. Yesod is currently not capable of performing such an analysis. +

    +
  2. +
  3. +

    +You have some extra knowledge about how your application operates, and know that such a situation should never be allowed. For example, if the Foo2R route should never be allowed to receive the parameter bar. +

    +
  4. +
+

You can turn off overlap checking by using the exclamation mark at the +beginning of your route. For example, the following will be accepted by Yesod:

+
/foo/bar    Foo1R GET
+!/foo/#Int  Foo2R GET
+!/foo/#Text Foo3R GET
+ +

One issue that overlapping routes introduce is ambiguity. In the example above, +should /foo/bar route to Foo1R or Foo3R? And should /foo/42 route to +Foo2R or Foo3R? Yesod’s rule for this is simple: first route wins.

+
+
+

Empty #String or #Text as dynamic piece

+

Consider the following route declaration:

+
/hello          HelloR     GET
+/hello/#String  HelloNameR GET
+

Let’s say a user requests the path /hello/ – which handler would respond to the request?

+

It will be HelloR because Yesod’s dispatch mechanism removes trailing slashes and +redirects to the canonical form of the URL.

+

If users actually want to address the HelloNameR handler with an +empty string as argument they need to request the path /hello/- +instead. Yesod automatically converts the Minus sign to the empty string.

+

Likewise, the resulting URL for @{HelloNameR ""} would be /hello/-.

+

Also, to disambiguate a single actual -, Yesod prefixes that piece with another Minus sign when rendering the URL. +Consequently, Yesod also prefixes any string consisting only of Minus signs with one single Minus sign.

+ +
+
+
+

Resource name

+

Each resource pattern also has a name associated with it. That name will become +the constructor for the type safe URL datatype associated with your +application. Therefore, it has to start with a capital letter. By convention, +these resource names all end with a capital R. There is nothing forcing you to +do this, it is just common practice.

+

The exact definition of our constructor depends upon the resource pattern it is +attached to. Whatever datatypes are used as single-pieces or multi-pieces of the +pattern become arguments to the datatype. This gives us a 1-to-1 correspondence +between our type-safe URL values and valid URLs in our application.

+ +

Let’s get some real examples going here. If you had the resource patterns +/person/#Text named PersonR, /year/#Int named YearR and /page/faq +named FaqR, you would end up with a route data type roughly looking like:

+
data MyRoute = PersonR Text
+             | YearR Int
+             | FaqR
+

If a user requests /year/2009, Yesod will convert it into the value YearR +2009. /person/Michael becomes PersonR "Michael" and /page/faq becomes +FaqR. On the other hand, /year/two-thousand-nine, /person/michael/snoyman +and /page/FAQ would all result in 404 errors without ever seeing your code.

+
+
+

Handler specification

+

The last piece of the puzzle when declaring your resources is how they will be +handled. There are three options in Yesod:

+
    +
  • +

    +A single handler function for all request methods on a given route. +

    +
  • +
  • +

    +A separate handler function for each request method on a given route. Any + other request method will generate a 405 Method Not Allowed response. +

    +
  • +
  • +

    +You want to pass off to a subsite. +

    +
  • +
+

The first two can be easily specified. A single handler function will be a line +with just a resource pattern and the resource name, such as /page/faq FaqR. +In this case, the handler function must be named handleFaqR.

+

A separate handler for each request method will be the same, plus a list of +request methods. The request methods must be all capital letters. For example, +/person/#String PersonR GET POST DELETE. In this case, you would need to +define three handler functions: getPersonR, postPersonR and +deletePersonR.

+

Subsites are a very useful— but more complicated— topic in Yesod. We will cover +writing subsites later, but using them is not too difficult. The most commonly +used subsite is the static subsite, which serves static files for your +application. In order to serve static files from /static, you would need a +resource line like:

+
/static StaticR Static getStatic
+

In this line, /static just says where in your URL structure to serve the +static files from. There is nothing magical about the word static, you could +easily replace it with /my/non-dynamic/files.

+

The next word, StaticR, gives the resource name. The next two words +specify that we are using a subsite. Static is the name of the subsite +foundation datatype, and getStatic is a function that gets a Static value +from a value of your master foundation datatype.

+

Let’s not get too caught up in the details of subsites now. We will look more +closely at the static subsite in the scaffolded site chapter.

+
+
+
+

Dispatch

+

Once you have specified your routes, Yesod will take care of all the pesky +details of URL dispatch for you. You just need to make sure to provide the +appropriate handler functions. For subsite routes, you do not need to write any +handler functions, but you do for the other two. We mentioned the naming rules +above (MyHandlerR GET becomes getMyHandlerR, MyOtherHandlerR becomes +handleMyOtherHandlerR).

+

Now that we know which functions we need to write, let’s figure out what their +type signatures should be.

+
+

Return Type

+

Let’s look at a simple handler function:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

There are two components to this return type: Handler and Html. Let’s +analyze each in more depth.

+
+

Handler monad

+

Like the Widget type, the Handler data type is not defined anywhere in the +Yesod libraries. Instead, the libraries provide the data type:

+
data HandlerT site m a
+

And like WidgetT, this has three arguments: a base monad m, a monadic value +a, and the foundation data type site. Each application defines a Handler +synonym which constrains site to that application’s foundation data type, and +sets m to IO. If your foundation is MyApp, in other words, you’d have the +synonym:

+
type Handler = HandlerT MyApp IO
+

We need to be able to modify the underlying monad when writing subsites, but +otherwise we’ll use IO.

+

The HandlerT monad provides access to information about the user request +(e.g. query-string parameters), allows modifying the response (e.g., response +headers), and more. This is the monad that most of your Yesod code will live +in.

+

In addition, there’s a type class called MonadHandler. Both HandlerT and +WidgetT are instances of this type class, allowing many common functions to +be used in both monads. If you see MonadHandler in any API documentation, you +should remember that the function can be used in your Handler functions.

+
+
+

Html

+

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 +can be returned?

+

In order to generate a response, we need to know two pieces of information: +the content type (e.g., text/html, image/png) and how to serialize it to a +stream of bytes. This is represented by the TypedContent data type:

+
data TypedContent = TypedContent !ContentType !Content
+

We also have a type class for all data types which can be converted to a +TypedContent:

+
class ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

Many common data types are instances of this type class, including Html, +Value (from the aeson package, representing JSON), Text, and even () (for +representing an empty response).

+
+
+
+

Arguments

+

Let’s return to our simple example from above:

+
mkYesod "Simple" [parseRoutes|
+/ HomeR GET
+|]
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout [whamlet|<h1>This is simple|]
+

Not every route is as simple as this HomeR. Take for instance our PersonR +route from earlier. The name of the person needs to be passed to the handler +function. This translation is very straight-forward, and hopefully intuitive. +For example:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/person/#Text PersonR GET
+/year/#Integer/month/#Text/day/#Int DateR
+/wiki/*Texts WikiR GET
+|]
+
+instance Yesod App
+
+getPersonR :: Text -> Handler Html
+getPersonR name = defaultLayout [whamlet|<h1>Hello #{name}!|]
+
+handleDateR :: Integer -> Text -> Int -> Handler Text -- text/plain
+handleDateR year month day =
+    return $
+        T.concat [month, " ", T.pack $ show day, ", ", T.pack $ show year]
+
+getWikiR :: [Text] -> Handler Text
+getWikiR = return . T.unwords
+
+main :: IO ()
+main = warp 3000 App
+

The arguments have the types of the dynamic pieces for each route, in the order +specified. Also, notice how we are able to use both Html and Text return +values.

+
+
+
+

The Handler functions

+

Since the majority of your code will live in the Handler monad, it’s +important to invest some time in understanding it better. The remainder of this +chapter will give a brief introduction to some of the most common functions +living in the Handler monad. I am specifically not covering any of the +session functions; that will be addressed in the sessions chapter.

+
+

Application Information

+

There are a number of functions that return information about your application +as a whole, and give no information about individual requests. Some of these +are:

+
+
+getYesod +
+

+Returns your application foundation value. If you store configuration +values in your foundation, you will probably end up using this function a lot. +(If you’re so inclined, you can also use ask from Control.Monad.Reader; +getYesod is simply a type-constrained synonym for it.) +

+
+
+getUrlRender +
+

+Returns the URL rendering function, which converts a type-safe +URL into a Text. Most of the time- like with Hamlet- Yesod calls this +function for you, but you may occasionally need to call it directly. +

+
+
+getUrlRenderParams +
+

+A variant of getUrlRender that converts both a type-safe +URL and a list of query-string parameters. This function handles all +percent-encoding necessary. +

+
+
+
+
+

Request Information

+

The most common information you will want to get about the current request is +the requested path, the query string parameters and POSTed form data. The +first of those is dealt with in the routing, as described above. The other two +are best dealt with using the forms module.

+

That said, you will sometimes need to get the data in a more raw format. For +this purpose, Yesod exposes the YesodRequest datatype along with the +getRequest function to retrieve it. This gives you access to the full list of +GET parameters, cookies, and preferred languages. There are some convenient +functions to make these lookups easier, such as lookupGetParam, +lookupCookie and languages. For raw access to the POST parameters, you +should use runRequestBody.

+

If you need even more raw data, like request headers, you can use waiRequest +to access the Web Application Interface (WAI) request value. See the WAI +appendix for more details.

+
+
+

Short Circuiting

+

The following functions immediately end execution of a handler function and +return a result to the user.

+
+
+redirect +
+

+Sends a redirect response to the user (a 303 response). If you want to use a different response code (e.g., a permanent 301 redirect), you can use redirectWith. +

+
+
+ +
+
+notFound +
+

+Return a 404 response. This can be useful if a user requests a +database value that doesn’t exist. +

+
+
+permissionDenied +
+

+Return a 403 response with a specific error message. +

+
+
+invalidArgs +
+

+A 400 response with a list of invalid arguments. +

+
+
+sendFile +
+

+Sends a file from the filesystem with a specified content type. This +is the preferred way to send static files, since the underlying WAI handler may +be able to optimize this to a sendfile system call. Using readFile for +sending static files should not be necessary. +

+
+
+sendResponse +
+

+Send a normal response with a 200 status code. This is really +just a convenience for when you need to break out of some deeply nested code +with an immediate response. Any instance of ToTypedContent may be used. +

+
+
+sendWaiResponse +
+

+When you need to get low-level and send out a raw WAI +response. This can be especially useful for creating streaming responses or a +technique like server-sent events. +

+
+
+
+
+

Response Headers

+
+
+setCookie +
+

+Set a cookie on the client. Instead of taking an expiration date, +this function takes a cookie duration in minutes. Remember, you won’t see this +cookie using lookupCookie until the following request. +

+
+
+deleteCookie +
+

+Tells the client to remove a cookie. Once again, lookupCookie +will not reflect this change until the next request. +

+
+
+addHeader +
+

+Set an arbitrary response header. +

+
+
+setLanguage +
+

+Set the preferred user language, which will show up in the result +of the languages function. +

+
+
+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 +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. +

+
+
+alreadyExpired +
+

+Sets the Expires header to the past. +

+
+
+expiresAt +
+

+Sets the Expires header to the specified date/time. +

+
+
+
+
+
+

I/O and debugging

+

The HandlerT and WidgetT monad transformers are both instances of a number +of typeclasses. For this section, the important typeclasses are MonadIO and +MonadLogger. The former allows you to perform arbitrary IO actions inside +your handler, such as reading from a file. In order to achieve this, you just +need to prepend liftIO to the call.

+

MonadLogger provides a built-in logging system. There are many ways you can +customize this system, including what messages get logged and where logs are +sent. By default, logs are sent to standard output, in development all messages +are logged, and in production, warnings and errors are logged.

+

Often times when logging, we want to know where in the source code the logging +occured. For this, MonadLogger provides a number of convenience Template +Haskell functions which will automatically insert source code location into the +log messages. These functions are $logDebug, $logInfo, $logWarn, and +$logError. Let’s look at a short example of some of these functions.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Control.Exception (IOException, try)
+import           Control.Monad     (when)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    -- This function controls which messages are logged
+    shouldLogIO App src level =
+        return True -- good for development
+        -- level == LevelWarn || level == LevelError -- good for production
+
+getHomeR :: Handler Html
+getHomeR = do
+    $logDebug "Trying to read data file"
+    edata <- liftIO $ try $ readFile "datafile.txt"
+    case edata :: Either IOException String of
+        Left e -> do
+            $logError "Could not read datafile.txt"
+            defaultLayout [whamlet|An error occurred|]
+        Right str -> do
+            $logInfo "Reading of data file succeeded"
+            let ls = lines str
+            when (length ls < 5) $ $logWarn "Less than 5 lines of data"
+            defaultLayout
+                [whamlet|
+                    <ol>
+                        $forall l <- ls
+                            <li>#{l}
+                |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Query string and hash fragments

+

We’ve looked at a number of functions which work on URL-like things, such as redirect. These functions all work with type-safe URLs, but what else do they work with? There’s a typeclass called RedirectUrl which contains the logic for converting some type into a textual URL. This includes type-safe URLs, textual URLs, and two special instances:

+
    +
  1. +

    +A tuple of a URL and a list of key/value pairs of query string parameters. +

    +
  2. +
  3. +

    +The Fragment datatype, used for adding a hash fragment to the end of a URL. +

    +
  4. +
+

Both of these instances allow you to "add on" extra information to a type-safe +URL. Let’s see some examples of how these can be used:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.Text        (Text)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+/link1 Link1R GET
+/link2 Link2R GET
+/link3 Link3R GET
+/link4 Link4R GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "Redirects"
+    [whamlet|
+        <p>
+            <a href=@{Link1R}>Click to start the redirect chain!
+    |]
+
+getLink1R, getLink2R, getLink3R :: Handler ()
+getLink1R = redirect Link2R -- /link2
+getLink2R = redirect (Link3R, [("foo", "bar")]) -- /link3?foo=bar
+getLink3R = redirect $ Link4R :#: ("baz" :: Text) -- /link4#baz
+
+getLink4R :: Handler Html
+getLink4R = defaultLayout
+    [whamlet|
+        <p>You made it!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Of course, inside a Hamlet template this is usually not necessary, as you can simply include the hash after the URL directly, e.g.:

+
<a href=@{Link1R}#somehash>Link to hash
+
+
+

Summary

+

Routing and dispatch is arguably the core of Yesod: it is from here that our +type-safe URLs are defined, and the majority of our code is written within the +Handler monad. This chapter covered some of the most important and central +concepts of Yesod, so it is important that you properly digest it.

+

This chapter also hinted at a number of more complex Yesod topics that we will +be covering later. But you should be able to write some very sophisticated web +applications with just the knowledge you have learned up until here.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/scaffolding-and-the-site-template.html b/public/book/scaffolding-and-the-site-template.html new file mode 100644 index 00000000..7fc2623e --- /dev/null +++ b/public/book/scaffolding-and-the-site-template.html @@ -0,0 +1,545 @@ + Scaffolding and the Site Template :: Yesod Web Framework Book- Version 1.6 +
+

Scaffolding and the Site Template

+ + +

So you’re tired of running small examples, and ready to write a real site? Then +you’re at the right chapter. Even with the entire Yesod library at your +fingertips, there are still a lot of steps you need to go through to get a +production-quality site setup:

+
    +
  • +

    +Config file parsing +

    +
  • +
  • +

    +Signal handling (*nix) +

    +
  • +
  • +

    +More efficient static file serving +

    +
  • +
  • +

    +A good file layout +

    +
  • +
+

The scaffolded site is a combination of many Yesoders' best practices combined +together into a ready-to-use skeleton for your sites. It is highly recommended +for all sites. This chapter will explain the overall structure of the +scaffolding, how to use it, and some of its less-than-obvious features.

+

For the most part, this chapter will not contain code samples. It is +recommended that you follow along with an actual scaffolded site.

+ +
+

How to Scaffold

+

The yesod-bin package installs an executable (conveniently named yesod as +well). This executable provides a few commands (run stack exec -- yesod +--help to get a list). In order to generate a scaffolding, the command is +stack new my-project yesodweb/postgres && cd my-project. This will generate a +scaffolding site with a postgres database backend in a directory named +my-project. You can see the other available templates using the command +stack templates.

+ +

The key thing differing in various available templates (from the +stack templates command) is the database backend. You get a few +choices here, including SQL backends, as well as +"yesodweb/simple" template to include no database support. This last +option also turns off a few extra dependencies, giving you a leaner +overall site. The remainder of this chapter will focus on the +scaffoldings for one of the database backends. There will be minor +differences for the yesodweb/simple backend.

+

After creating your files, install the yesod command line tool inside +the project: stack install yesod-bin --install-ghc. Then do a stack +build inside the directory. In particular, the commands provided +will ensure that any missing dependencies are built and installed.

+

Finally, to launch your development site, you would use stack exec -- yesod devel. +This site will automatically rebuild and reload whenever you change your code.

+
+
+

File Structure

+

The scaffolded site is built as a fully cabalized Haskell package. In addition +to source files, config files, templates, and static files are produced as +well.

+
+

Cabal file

+

Whether directly using stack, or indirectly using stack exec -- yesod devel, building +your code will always go through the cabal file. If you open the file, you’ll +see that there are both library and executable blocks. If the library-only +flag is turned on, then the executable block is not built. This is how yesod +devel calls your app. Otherwise, the executable is built.

+

The library-only flag should only be used by yesod devel; you should never +be explicitly passing it into cabal. There is an additional flag, dev, that +allows cabal to build an executable, but turns on some of the same features as +the library-only flag, i.e., no optimizations and reload versions of the +Shakespearean template functions.

+

In general, you will build as follows:

+
    +
  • +

    +When developing, use yesod devel exclusively. +

    +
  • +
  • +

    +When building a production build, perform stack clean && + stack build. This will produce an optimized executable in your + dist folder. (You can also use the yesod keter command for + this.) +

    +
  • +
+

You might be surprised to see the NoImplicitPrelude extension. We turn this +on since the site includes its own module, Import, with a few changes to the +Prelude that make working with Yesod a little more convenient.

+

The last thing to note is the exposed-modules list. If you add any modules to +your application, you must update this list to get yesod devel to work +correctly. Unfortunately, neither Cabal nor GHC will give you a warning if you +forgot to make this update, and instead you’ll get a very scary-looking error +message from yesod devel.

+
+
+

Routes and entities

+

Multiple times in this book, you’ve seen a comment like “We’re declaring our +routes/entities with quasiquotes for convenience. In a production site, you +should use an external file.” The scaffolding uses such an external file.

+

Routes are defined in config/routes, and entities in config/models. They +have the exact same syntax as the quasiquoting you’ve seen throughout the book, +and yesod devel knows to automatically recompile the appropriate modules when +these files change.

+

The models files is referenced by Model.hs. You are free to declare +whatever you like in this file, but here are some guidelines:

+
    +
  • +

    +Any data types used in entities must be imported/declared in Model.hs, + above the persistFileWith call. +

    +
  • +
  • +

    +Helper utilities should either be declared in Import.hs or, if very + model-centric, in a file within the Model folder and imported into + Import.hs. +

    +
  • +
+
+
+

Foundation and Application modules

+

The mkYesod function which we have used throughout the book declares a few +things:

+
    +
  • +

    +Route type +

    +
  • +
  • +

    +Route render function +

    +
  • +
  • +

    +Dispatch function +

    +
  • +
+

The dispatch function refers to all of the handler functions, and therefore all +of those must either be defined in the same file as the dispatch function, or +be imported into the module containing the dispatch function.

+

Meanwhile, the handler functions will almost certainly refer to the route type. +Therefore, they must be either in the same file where the route type is +defined, or must import that file. If you follow the logic here, your entire +application must essentially live in a single file!

+

Clearly this isn’t what we want. So instead of using mkYesod, the scaffolding +site uses a decomposed version of the function. Foundation.hs calls +mkYesodData, which declares the route type and render function. Since it does +not declare the dispatch function, the handler functions need not be in scope. +Import.hs imports Foundation.hs, and all the handler modules import +Import.hs.

+

In Application.hs, we call mkYesodDispatch, which creates our dispatch +function. For this to work, all handler functions must be in scope, so be sure +to add an import statement for any new handler modules you create.

+

Other than that, Application.hs is pretty simple. It provides two primary +functions: getApplicationDev is used by yesod devel to launch your app, and +makeApplication is used by the executable to launch.

+

Foundation.hs is much more exciting. It:

+
    +
  • +

    +Declares your foundation datatype +

    +
  • +
  • +

    +Declares a number of instances, such as Yesod, YesodAuth, and + YesodPersist +

    +
  • +
  • +

    +Imports the messages files. If you look for the line starting with + mkMessage, you will see that it specifies the folder containing the + messages (messages/) and the default language (en, for English). +

    +
  • +
+

This is the right file for adding extra instances for your foundation, such as +YesodAuthEmail or YesodBreadcrumbs.

+

We’ll be referring back to this file later, as we discussed some of the special +implementations of Yesod typeclass methods.

+
+
+

Import

+

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’m always adding the same five import statements (Data.Text, + Control.Applicative, etc) to every handler module. +

    +
  • +
  • +

    +I want to make sure I never use some evil function (head, readFile, …) from Prelude. +

    +
  • +
+ +

The solution is to turn on the NoImplicitPrelude language extension, +re-export the parts of Prelude we want, add in all the other stuff we want, +define our own functions as well, and then import this file in all handlers.

+ +
+
+

Handler modules

+

Handler modules should go inside the Handler folder. The site template +includes one module: Handler/Home.hs. How you split up your handler functions +into individual modules is your decision, but a good rule of thumb is:

+
    +
  • +

    +Different methods for the same route should go in the same file, e.g. + getBlogR and postBlogR. +

    +
  • +
  • +

    +Related routes can also usually go in the same file, e.g., getPeopleR and + getPersonR. +

    +
  • +
+

Of course, it’s entirely up to you. When you add a new handler file, make sure +you do the following:

+
    +
  • +

    +Add it to version control (you are using version control, right?). +

    +
  • +
  • +

    +Add it to the cabal file. +

    +
  • +
  • +

    +Add it to the Application.hs file. +

    +
  • +
  • +

    +Put a module statement at the top, and an import Import line below it. +

    +
  • +
+

You can use the stack exec -- yesod add-handler command to automate the last three steps.

+
+
+
+

widgetFile

+

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.

+

If you have a handler function:

+
getHomeR = defaultLayout $(widgetFile "homepage")
+

, Yesod will look for the following files:

+
    +
  • +

    +templates/homepage.hamlet +

    +
  • +
  • +

    +templates/homepage.lucius +

    +
  • +
  • +

    +templates/homepage.cassius +

    +
  • +
  • +

    +templates/homepage.julius +

    +
  • +
+

If any of those files are present, they will be automatically included in the +output.

+ +
+
+

defaultLayout

+

One of the first things you’re going to want to customize is the look of your +site. The layout is actually broken up into two files:

+
    +
  • +

    +templates/default-layout-wrapper.hamlet contains just the basic shell of a + page. This file is interpreted as plain Hamlet, not as a Widget, and + therefore cannot refer to other widgets, embed i18n strings, or add extra + CSS/JS. +

    +
  • +
  • +

    +templates/default-layout.hamlet is where you would put the bulk of your + page. You must remember to include the widget value in the page, as that + contains the per-page contents. This file is interpreted as a Widget. +

    +
  • +
+

Also, since default-layout is included via the widgetFile function, any +Lucius, Cassius, or Julius files named default-layout.* will automatically be +included as well.

+
+
+

Static files

+

The scaffolded site automatically includes the static file subsite, optimized +for serving files that will not change over the lifetime of the current build. +What this means is that:

+
    +
  • +

    +When your static file identifiers are generated (e.g., static/mylogo.png + becomes mylogo_png), a query-string parameter is added to it with a hash of + the contents of the file. All of this happens at compile time. +

    +
  • +
  • +

    +When yesod-static serves your static files, it sets expiration headers far + in the future, and includes an etag based on a hash of your content. +

    +
  • +
  • +

    +Whenever you embed a link to mylogo_png, the rendering includes the + query-string parameter. If you change the logo, recompile, and launch your + new app, the query string will have changed, causing users to ignore the + cached copy and download a new version. +

    +
  • +
+

Additionally, you can set a specific static root in your Settings.hs file to +serve from a different domain name. This has the advantage of not requiring +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 +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:

+
    +
  1. +

    +Caching works properly. +

    +
  2. +
  3. +

    +Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with the same hash already exists. +

    +
  4. +
+

Finally, all of your Javascript is automatically minified via hjsmin.

+
+
+

Environment variables

+

Scaffolded apps are preconfigured to support these environment variables :

+
    +
  • +

    +YESOD_STATIC_DIR : Path of static directory. +

    +
  • +
  • +

    +YESOD_HOST : Host/interface the server should bind to. +

    +
  • +
  • +

    +YESOD_PORT : Port to listen on. +

    +
  • +
  • +

    +YESOD_IP_FROM_HEADER : Get the IP address from the header when logging. Useful when sitting behind a reverse proxy. +

    +
  • +
+

They are listed in file /config/settings.yml, where their default values can be defined. +Other variables can be added by uncommenting dedicated lines :

+
    +
  • +

    +YESOD_APPROOT : Explicit base for all generated URLs. +

    +
  • +
  • +

    +PORT : Port for Keter. +

    +
  • +
+
+
+

Conclusion

+

The purpose of this chapter was not to explain every line that exists in the +scaffolded site, but instead to give a general overview to how it works. The +best way to become more familiar with it is to jump right in and start writing +a Yesod site with it.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/sessions.html b/public/book/sessions.html new file mode 100644 index 00000000..ef0a4514 --- /dev/null +++ b/public/book/sessions.html @@ -0,0 +1,487 @@ + Sessions :: Yesod Web Framework Book- Version 1.6 +
+

Sessions

+ + +

HTTP is a stateless protocol. While some view this as a disadvantage, advocates +of RESTful web development laud this as a plus. When state is removed from the +picture, we get some automatic benefits, such as easier scalability and +caching. You can draw many parallels with the non-mutable nature of Haskell in +general.

+

As much as possible, RESTful applications should avoid storing state about an +interaction with a client. However, it is sometimes unavoidable. Features like +shopping carts are the classic example, but other more mundane interactions +like proper login handling can be greatly enhanced by correct usage of sessions.

+

This chapter will describe how Yesod stores session data, how you can access +this data, and some special functions to help you make the most of sessions.

+
+

Clientsession

+

One of the earliest packages spun off from Yesod was clientsession. This +package uses encryption and signatures to store data in a client-side cookie. +The encryption prevents the user from inspecting the data, and the signature +ensures that the session cannot be tampered with.

+

It might sound like a bad idea from an efficiency standpoint to store data in a +cookie. After all, this means that the data must be sent on every request. +However, in practice, clientsession can be a great boon for performance.

+
    +
  • +

    +No server side database lookup is required to service a request. +

    +
  • +
  • +

    +We can easily scale horizontally: each request contains all the information + we need to send a response. +

    +
  • +
  • +

    +To avoid undue bandwidth overhead, production sites can serve their static + content from a separate domain name, thereby skipping transmission of the + session cookie for each request. +

    +
  • +
+

Storing megabytes of information in the session will be a bad idea. But for +that matter, most session implementations recommend against such practices. If +you really need massive storage for a user, it is best to store a lookup key in +the session, and put the actual data in a database.

+

All of the interaction with clientsession is handled by Yesod internally, but +there are a few spots where you can tweak the behavior just a bit.

+
+
+

Controlling sessions

+

By default, your Yesod application will use clientsession for its session +storage, getting the encryption key from the client client-session-key.aes +and giving a session a two hour timeout. (Note: timeout is measured from the +last time the client sent a request to the site, not from when then session +was first created.) However, all of those points can be modified by overriding +the makeSessionBackend method in the Yesod typeclass.

+

One simple way to override this method is to simply turn off session handling; +to do so, return Nothing. If your app has absolutely no session needs, +disabling them can give a bit of a performance increase. But be careful about +disabling sessions: this will also disable such features as Cross-Site Request +Forgery protection.

+
instance Yesod App where
+    makeSessionBackend _ = return Nothing
+

Another common approach is to modify the filepath or timeout value, but +continue using client-session. To do so, use the defaultClientSessionBackend +helper function:

+
instance Yesod App where
+    makeSessionBackend _ =
+        fmap Just $ defaultClientSessionBackend minutes filepath
+      where minutes = 24 * 60 -- 1 day
+            filepath = "mykey.aes"
+

There are a few other functions to grant you more fine-grained control of +client-session, but they will rarely be necessary. Please see Yesod.Core's +documentation if you are interested. It’s also possible to implement some other +form of session, such as a server side session. To my knowledge, at the time of +writing, no other such implementations exist.

+ +
+
+

Hardening via SSL

+

Client sessions over HTTP have an inherent hijacking vulnerability: an attacker +can read the unencrypted traffic, obtain the session cookie from it, and then +make requests to the site with that same cookie to impersonate the user. This +vulnerability is particularly severe if the sessions include any personally +identifiable information or authentication material.

+

The only sure way to defeat this threat is to run your entire site over SSL, +and prevent browsers from attempting to access it over HTTP. You can achieve +the first part of this at the webserver level, either via an SSL solution in +Haskell such as warp-tls, or by using an SSL-enabled load balancer like +Amazon Elastic Load Balancer.

+

To prevent your site from sending cookies over insecure connections, you should +augment your application’s sessions as well as the default yesodMiddleware +implementation with some additional behavior: Apply the sslOnlySessions +transformation to your makeSessionBackend, and compose the +sslOnlyMiddleware transformation with your yesodMiddleware implementation.

+
instance Yesod App where
+    makeSessionBackend _ = sslOnlySessions $
+        fmap Just $ defaultClientSessionBackend 120 "mykey.aes"
+    yesodMiddleware = (sslOnlyMiddleware 120) . defaultYesodMiddleware
+

sslOnlySessions causes all session cookies to be set with the Secure bit on, +so that browsers will not transmit them over HTTP. sslOnlyMiddleware adds a +Strict-Transport-Security header to all responses, which instructs browsers not +to make HTTP requests to your domain or its subdomains for the specified number +of minutes. Be sure to set the timeout for the sslOnlyMiddleware to be at +least as long as your session timeout. Used together, these measures will +ensure that session cookies are not transmitted in the clear.

+
+
+

Session Operations

+

Like most frameworks, a session in Yesod is a key-value store. The base session +API boils down to four functions: lookupSession gets a value for a key (if +available), getSession returns all of the key/value pairs, setSession sets +a value for a key, and deleteSession clears a value for a key.

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+import           Control.Applicative ((<$>), (<*>))
+import qualified Web.ClientSession   as CS
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+|]
+
+getHomeR :: Handler Html
+getHomeR = do
+    sess <- getSession
+    defaultLayout
+        [whamlet|
+            <form method=post>
+                <input type=text name=key>
+                <input type=text name=val>
+                <input type=submit>
+            <h1>#{show sess}
+        |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    (key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
+    case mval of
+        Nothing -> deleteSession key
+        Just val -> setSession key val
+    liftIO $ print (key, mval)
+    redirect HomeR
+
+instance Yesod App where
+    -- Make the session timeout 1 minute so that it's easier to play with
+    makeSessionBackend _ = do
+        backend <- defaultClientSessionBackend 1 "keyfile.aes"
+        return $ Just backend
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Messages

+

One usage of sessions previously alluded to is messages. They come to solve a +common problem in web development: the user performs a POST request, the web +app makes a change, and then the web app wants to simultaneously redirect the +user to a new page and send the user a success message. (This is known as +Post/Redirect/Get.)

+

Yesod provides a pair of functions to enable this workflow: setMessage stores +a value in the session, and getMessage both reads the value most recently put +into the session, and clears the old value so it is not displayed twice.

+

It is recommended to have a call to getMessage in defaultLayout so that any +available message is shown to a user immediately, without having to add +getMessage calls to every handler.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/            HomeR       GET
+/set-message SetMessageR POST
+|]
+
+instance Yesod App where
+    defaultLayout widget = do
+        pc <- widgetToPageContent widget
+        mmsg <- getMessage
+        withUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <p>Your message was: #{msg}
+                        ^{pageBody pc}
+            |]
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{SetMessageR}>
+            My message is: #
+            <input type=text name=message>
+            <button>Go
+    |]
+
+postSetMessageR :: Handler ()
+postSetMessageR = do
+    msg <- runInputPost $ ireq textField "message"
+    setMessage $ toHtml msg
+    redirect HomeR
+
+main :: IO ()
+main = warp 3000 App
+

Initial page load, no message

+ + + + + + +
+

New message entered in text box

+ + + + + + +
+

After form submit, message appears at top of page

+ + + + + + +
+

After refresh, the message is cleared

+ + + + + + +
+
+
+

Ultimate Destination

+

Not to be confused with a horror film, ultimate destination is a technique +originally developed for Yesod’s authentication framework, but which has more +general usefulness. Suppose a user requests a page that requires +authentication. If the user is not yet logged in, you need to send him/her to +the login page. A well-designed web app will then send them back to the first +page they requested. That’s what we call the ultimate destination.

+

redirectUltDest sends the user to the ultimate destination set in his/her +session, clearing that value from the session. It takes a default destination +as well, in case there is no destination set. For setting the session, there +are three options:

+
    +
  • +

    +setUltDest sets the destination to the given URL, which can be given + either as a textual URL or a type-safe URL. +

    +
  • +
  • +

    +setUltDestCurrent sets the destination to the currently requested URL. +

    +
  • +
  • +

    +setUltDestReferer sets the destination based on the Referer header (the + page that led the user to the current page). +

    +
  • +
+

Additionally, there is the clearUltDest function, to drop the ultimate +destination value from the session if present.

+

Let’s look at a small sample app. It will allow the user to set his/her name in +the session, and then tell the user his/her name from another route. If the +name hasn’t been set yet, the user will be redirected to the set name page, +with an ultimate destination set to come back to the current page.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/         HomeR     GET
+/setname  SetNameR  GET POST
+/sayhello SayHelloR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{SetNameR}>Set your name
+        <p>
+            <a href=@{SayHelloR}>Say hello
+    |]
+
+-- Display the set name form
+getSetNameR :: Handler Html
+getSetNameR = defaultLayout
+    [whamlet|
+        <form method=post>
+            My name is #
+            <input type=text name=name>
+            . #
+            <input type=submit value="Set name">
+    |]
+
+-- Retrieve the submitted name from the user
+postSetNameR :: Handler ()
+postSetNameR = do
+    -- Get the submitted name and set it in the session
+    name <- runInputPost $ ireq textField "name"
+    setSession "name" name
+
+    -- After we get a name, redirect to the ultimate destination.
+    -- If no destination is set, default to the homepage
+    redirectUltDest HomeR
+
+getSayHelloR :: Handler Html
+getSayHelloR = do
+    -- Lookup the name value set in the session
+    mname <- lookupSession "name"
+    case mname of
+        Nothing -> do
+            -- No name in the session, set the current page as
+            -- the ultimate destination and redirect to the
+            -- SetName page
+            setUltDestCurrent
+            setMessage "Please tell me your name"
+            redirect SetNameR
+        Just name -> defaultLayout [whamlet|<p>Welcome #{name}|]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Summary

+

Sessions are the primary means by which we bypass the statelessness imposed by +HTTP. We shouldn’t consider this an escape hatch to perform whatever actions we +want: statelessness in web applications is a virtue, and we should respect it +whenever possible. However, there are specific cases where it is vital to +retain some state.

+

The session API in Yesod is very simple. It provides a key-value store, and a +few convenience functions built on top for common use cases. If used properly, +with small payloads, sessions should be an unobtrusive part of your web +development.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/settings-types.html b/public/book/settings-types.html new file mode 100644 index 00000000..7c3e2642 --- /dev/null +++ b/public/book/settings-types.html @@ -0,0 +1,195 @@ + Settings Types :: Yesod Web Framework Book- Version 1.6 +
+

Settings Types

+ + +

Let’s say you’re writing a webserver. You want the server to take a port to +listen on, and an application to run. So you create the following function:

+
run :: Int -> Application -> IO ()
+

But suddenly you realize that some people will want to customize their timeout +durations. So you modify your API:

+
run :: Int -> Int -> Application -> IO ()
+

So, which Int is the timeout, and which is the port? Well, you could create +some type aliases, or comment your code. But there’s another problem creeping +into our code: this run function is getting unmanageable. Soon we’ll need to +take an extra parameter to indicate how exceptions should be handled, and then +another one to control which host to bind to, and so on.

+

A more extensible solution is to introduce a settings datatype:

+
data Settings = Settings
+    { settingsPort :: Int
+    , settingsHost :: String
+    , settingsTimeout :: Int
+    }
+

And this makes the calling code almost self-documenting:

+
run Settings
+    { settingsPort = 8080
+    , settingsHost = "127.0.0.1"
+    , settingsTimeout = 30
+    } myApp
+

Great, couldn’t be clearer, right? True, but what happens when you have 50 +settings to your webserver. Do you really want to have to specify all of those +each time? Of course not. So instead, the webserver should provide a set of +defaults:

+
defaultSettings = Settings 3000 "127.0.0.1" 30
+

And now, instead of needing to write that long bit of code above, we can get +away with:

+
run defaultSettings { settingsPort = 8080 } myApp -- (1)
+

This is great, except for one minor hitch. Let’s say we now decide to add an +extra record to Settings. Any code out in the wild looking like this:

+
run (Settings 8080 "127.0.0.1" 30) myApp -- (2)
+

will be broken, since the Settings constructor now takes 4 arguments. The +proper thing to do would be to bump the major version number so that dependent +packages don’t get broken. But having to change major versions for every minor +setting you add is a nuisance. The solution? Don’t export the Settings +constructor:

+
module MyServer
+    ( Settings
+    , settingsPort
+    , settingsHost
+    , settingsTimeout
+    , run
+    , defaultSettings
+    ) where
+

With this approach, no one can write code like (2), so you can freely add new +records without any fear of code breaking.

+

The one downside of this approach is that it’s not immediately obvious from the +Haddocks that you can actually change the settings via record syntax. That’s +the point of this chapter: to clarify what’s going on in the libraries that use +this technique.

+

I personally use this technique in a few places, feel free to have a look at +the Haddocks to see what I mean.

+
    +
  • +

    +Warp: Settings +

    +
  • +
  • +

    +http-conduit: Request and ManagerSettings +

    +
  • +
  • +

    +xml-conduit +

    +
  • +
  • +

    +Parsing: ParseSettings +

    +
  • +
  • +

    +Rendering: RenderSettings +

    +
  • +
+

As a tangential issue, http-conduit and xml-conduit actually create +instances of the Default typeclass instead of declaring a brand new +identifier. This means you can just type def instead of +defaultParserSettings.

+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/shakespearean-templates.html b/public/book/shakespearean-templates.html new file mode 100644 index 00000000..d6b22960 --- /dev/null +++ b/public/book/shakespearean-templates.html @@ -0,0 +1,1067 @@ + Shakespearean Templates :: Yesod Web Framework Book- Version 1.6 +
+

Shakespearean Templates

+ + +

Yesod uses the Shakespearean family of template languages as its standard +approach to HTML, CSS and Javascript creation. This language family shares some +common syntax, as well as overarching principles:

+
    +
  • +

    +As little interference to the underlying language as possible, while +providing conveniences where unobtrusive. +

    +
  • +
  • +

    +Compile-time guarantees on well-formed content. +

    +
  • +
  • +

    +Static type safety, greatly helping the prevention of + XSS (cross-site + scripting) attacks. +

    +
  • +
  • +

    +Automatic validation of interpolated links, whenever possible, through type-safe + URLs. +

    +
  • +
+

There is nothing inherently tying Yesod to these languages, or the other way +around: each can be used independently of the other. This chapter will address +these template languages on their own, while the remainder of the book will use +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 +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 +Javascript; the only added feature is variable interpolation.

+ +
+

Hamlet (HTML)

+
$doctype 5
+<html>
+    <head>
+        <title>#{pageTitle} - My Site
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1 .page-title>#{pageTitle}
+        <p>Here is a list of your friends:
+        $if null friends
+            <p>Sorry, I lied, you don't have any friends.
+        $else
+            <ul>
+                $forall Friend name age <- friends
+                    <li>#{name} (#{age} years old)
+        <footer>^{copyright}
+
+
+

Lucius (CSS)

+
section.blog {
+    padding: 1em;
+    border: 1px solid #000;
+    h1 {
+        color: #{headingColor};
+        background-image: url(@{MyBackgroundR});
+    }
+}
+
+
+

Cassius (CSS)

+

The following is equivalent to the Lucius example above.

+
section.blog
+    padding: 1em
+    border: 1px solid #000
+    h1
+        color: #{headingColor}
+        background-image: url(@{MyBackgroundR})
+
+
+

Julius (Javascript)

+

String interpolation works slightly differently in Julius. This is important +from a security standpoint — all interpolated values are valid JSON by default +which helps prevent XSS attacks. You’ll need to use the rawJS function to get +a string that isn’t wrapped in double-quotes.

+

Be sure not to use rawJS with any input you don’t trust, e.g., anything +submitted by a user.

+
-- import the rawJS function by importing its typeclass
+import Text.Julius (RawJS (..))
+
$(function(){
+    $("section.#{rawJS sectionClass}").hide();
+    $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
+    ^{addBling}
+});
+
+
+
+

Types

+

Before we jump into syntax, let’s take a look at the various types involved. We +mentioned in the introduction that types help protect us from XSS attacks. For +example, let’s say that we have an HTML template that should display someone’s +name. It might look like this:

+
<p>Hello, my name is #{name}
+ +

What should happen to name, and what should its datatype be? A naive approach +would be to use a Text value, and insert it verbatim. But that would give us +quite a problem when name is equal to something like:

+
<script src='http://nefarious.com/evil.js'></script>
+

What we want is to be able to entity-encode the name, so that < becomes &lt;.

+

An equally naive approach is to simply entity-encode every piece of text that +gets embedded. What happens when you have some preexisting HTML generated from +another process? For example, on the Yesod website, all Haskell code snippets +are run through a colorizing function that wraps up words in appropriate span +tags. If we entity escaped everything, code snippets would be completely +unreadable!

+

Instead, we have an Html datatype. In order to generate an Html value, we +have two options for APIs: the ToMarkup typeclass provides a way to convert +String and Text values into Html, via its toHtml function, +automatically escaping entities along the way. This would be the approach we’d +want for the name above. For the code snippet example, we would use the +preEscapedToMarkup function.

+

When you use variable interpolation in Hamlet (the HTML Shakespeare language), +it automatically applies a toHtml call to the value inside. So if you +interpolate a String, it will be entity-escaped. But if you provide an Html +value, it will appear unmodified. In the code snippet example, we might +interpolate with something like #{preEscapedToMarkup myHaskellHtml}.

+ +

Similarly, we have Css/ToCss, as well as Javascript/ToJavascript. These +provide some compile-time sanity checks that we haven’t accidentally stuck some +HTML in our CSS.

+ +
+

Type-safe URLs

+

Possibly the most unique feature in Yesod is type-safe URLs, and the ability to +use them conveniently is provided directly by Shakespeare. Usage is nearly +identical to variable interpolation; we just use the at-sign (@) instead of the +hash (#). We’ll cover the syntax later; first, let’s clarify the intuition.

+

Suppose we have an application with two routes: +http://example.com/profile/home is the homepage, and +http://example.com/display/time displays the current time. And let’s say we +want to link from the homepage to the time. I can think of three different ways +of constructing the URL:

+
    +
  1. +

    +As a relative link: ../display/time +

    +
  2. +
  3. +

    +As an absolute link, without a domain: /display/time +

    +
  4. +
  5. +

    +As an absolute link, with a domain: http://example.com/display/time +

    +
  6. +
+

There are problems with each approach: the first will break if either URL +changes. Also, it’s not suitable for all use cases; RSS and Atom feeds, for +instance, require absolute URLs. The second is more resilient to change than +the first, but still won’t be acceptable for RSS and Atom. And while the third +works fine for all use cases, you’ll need to update every single URL in your +application whenever your domain name changes. You think that doesn’t happen +often? Just wait till you move from your development to staging and finally +production server.

+

But more importantly, there is one huge problem with all approaches: if you +change your routes at all, the compiler won’t warn you about the broken links. +Not to mention that typos can wreak havoc as well.

+

The goal of type-safe URLs is to let the compiler check things for us as much +as possible. In order to facilitate this, our first step must be to move away +from plain old text, which the compiler doesn’t understand, to some well +defined datatypes. For our simple application, let’s model our routes with a +sum type:

+
data MyRoute = Home | Time
+

Instead of placing a link like /display/time in our template, we can use the +Time constructor. But at the end of the day, HTML is made up of text, not +data types, so we need some way to convert these values to text. We call this a +URL rendering function, and a simple one is:

+
renderMyRoute :: MyRoute -> Text
+renderMyRoute Home = "http://example.com/profile/home"
+renderMyRoute Time = "http://example.com/display/time"
+ +

OK, we have our render function, and we have type-safe URLs embedded in the +templates. How does this fit together exactly? Instead of generating an Html +(or Css or Javascript) value directly, Shakespearean templates actually +produce a function, which takes this render function and produces HTML. To see +this better, let’s have a quick (fake) peek at how Hamlet would work under the +surface. Supposing we had a template:

+
<a href=@{Time}>The time
+

this would translate roughly into the Haskell code:

+
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
+
+
+
+

Syntax

+

All Shakespearean languages share the same interpolation syntax, and are able +to utilize type-safe URLs. They differ in the syntax specific for their target +language (HTML, CSS, or Javascript). Let’s explore each language in turn.

+
+

Hamlet Syntax

+

Hamlet is the most sophisticated of the languages. Not only does it provide +syntax for generating HTML, it also allows for basic control structures: +conditionals, looping, and maybes.

+
+

Tags

+

Obviously tags will play an important part of any HTML template language. In +Hamlet, we try to stick very close to existing HTML syntax to make the language +more comfortable. However, instead of using closing tags to denote nesting, we +use indentation. So something like this in HTML:

+
<body>
+<p>Some paragraph.</p>
+<ul>
+<li>Item 1</li>
+<li>Item 2</li>
+</ul>
+</body>
+

would be

+
<body>
+    <p>Some paragraph.
+    <ul>
+        <li>Item 1
+        <li>Item 2
+

In general, we find this to be easier to follow than HTML once you get +accustomed to it. The only tricky part comes with dealing with whitespace +before and after tags. For example, let’s say you want to create the HTML

+
<p>Paragraph <i>italic</i> end.</p>
+

We want to make sure that whitespace is preserved after the word +"Paragraph" and before the word "end". To do so, we use two simple escape +characters:

+
<p>
+    Paragraph #
+    <i>italic
+    \ end.
+

The whitespace escape rules are actually quite simple:

+
    +
  1. +

    +If the first non-space character in a line is a backslash, the backslash is ignored. (Note: this will also cause any tag on this line to be treated as plain text.) +

    +
  2. +
  3. +

    +If the last character in a line is a hash, it is ignored. +

    +
  4. +
+

One other thing. Hamlet does not escape entities within its content. This is +done on purpose to allow existing HTML to be more easily copied in. So the +example above could also be written as:

+
<p>Paragraph <i>italic</i> end.
+

Notice that the first tag will be automatically closed by Hamlet, while the +inner "i" tag will not. You are free to use whichever approach you want, there +is no penalty for either choice. Be aware, however, that the only time you +use closing tags in Hamlet is for such inline tags; normal tags are not closed.

+

Another outcome of this is that any tags after the first tag do not have +special treatment for IDs and classes. For example, the Hamlet snippet:

+
<p #firstid>Paragraph <i #secondid>italic</i> end.
+

generates the HTML:

+
<p id="firstid">Paragraph <i #secondid>italic</i> end.</p>
+

Notice how the p tag is automatically closed, and its attributes get special +treatment, whereas the i tag is treated as plain text.

+
+
+

Interpolation

+

What we have so far is a nice, simplified HTML, but it doesn’t let us interact +with our Haskell code at all. How do we pass in variables? Simple: with +interpolation:

+
<head>
+    <title>#{title}
+

The hash followed by a pair of braces denotes variable interpolation. In the +case above, the title variable from the scope in which the template was +called will be used. Let me state that again: Hamlet automatically has access +to the variables in scope when it’s called. There is no need to specifically +pass variables in.

+

You can apply functions within an interpolation. You can use string and numeric +literals in an interpolation. You can use qualified modules. Both parentheses +and the dollar sign can be used to group statements together. And at the end, +the toHtml function is applied to the result, meaning any instance of +ToMarkup can be interpolated. Take, for instance, the following code.

+
-- Just ignore the quasiquote stuff for now, and that shamlet thing.
+-- It will be explained later.
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (shamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Char (toLower)
+import Data.List (sort)
+
+data Person = Person
+    { name :: String
+    , age  :: Int
+    }
+
+main :: IO ()
+main = putStrLn $ renderHtml [shamlet|
+<p>Hello, my name is #{name person} and I am #{show $ age person}.
+<p>
+    Let's do some funny stuff with my name: #
+    <b>#{sort $ map toLower (name person)}
+<p>Oh, and in 5 years I'll be #{show ((+) 5 (age person))} years old.
+|]
+  where
+    person = Person "Michael" 26
+

What about our much-touted type-safe URLs? They are almost identical to +variable interpolation in every way, except they start with an at-sign (@) +instead. In addition, there is embedding via a caret (^) which allows you to +embed another template of the same type. The next code sample demonstrates both +of these.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text)
+
+data MyRoute = Home
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+
+footer :: HtmlUrl MyRoute
+footer = [hamlet|
+<footer>
+    Return to #
+    <a href=@{Home}>Homepage
+    .
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ [hamlet|
+<body>
+    <p>This is my page.
+    ^{footer}
+|] render
+

Additionally, there is a variant of URL interpolation which allows you to embed +query string parameters. This can be useful, for example, for creating +paginated responses. Instead of using @{…}, you add a question mark +(@?{…}) to indicate the presence of a query string. The value you provide +must be a two-tuple with the first value being a type-safe URL and the second +being a list of query string parameter pairs. See the next code snippet for an +example.

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+import Data.Text (Text, append, pack)
+import Control.Arrow (second)
+import Network.HTTP.Types (renderQueryText)
+import Data.Text.Encoding (decodeUtf8)
+import Blaze.ByteString.Builder (toByteString)
+
+data MyRoute = SomePage
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render SomePage params = "/home" `append`
+    decodeUtf8 (toByteString $ renderQueryText True (map (second Just) params))
+
+main :: IO ()
+main = do
+    let currPage = 2 :: Int
+    putStrLn $ renderHtml $ [hamlet|
+<p>
+    You are currently on page #{currPage}.
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage - 1)])}>Previous
+    <a href=@?{(SomePage, [("page", pack $ show $ currPage + 1)])}>Next
+|] render
+

This generates the expected HTML:

+
<p>You are currently on page 2.
+<a href="/home?page=1">Previous</a>
+<a href="/home?page=3">Next</a>
+</p>
+
+
+

Attributes

+

In that last example, we put an href attribute on the "a" tag. Let’s elaborate on the syntax:

+
    +
  • +

    +You can have interpolations within the attribute value. +

    +
  • +
  • +

    +The equals sign and value for an attribute are optional, just like in HTML. + So <input type=checkbox checked> is perfectly valid. +

    +
  • +
  • +

    +There are two convenience attributes: for id, you can use the hash, and for + classes, the period. In other words, <p #paragraphid .class1 .class2>. +

    +
  • +
  • +

    +While quotes around the attribute value are optional, they are required if + you want to embed spaces. +

    +
  • +
  • +

    +You can add an attribute optionally by using colons. To make a checkbox only + checked if the variable isChecked is True, you would write + <input type=checkbox :isChecked:checked>. To have a paragraph be optionally red, + you could use <p :isRed:style="color:red">. (This also works for class names, e.g., + <p :isCurrent:.current> will set the class current if isCurrent is True.) +

    +
  • +
  • +

    +Arbitrary key-value pairs can also be interpolated using the *{…} + syntax. The interpolated variable must be a tuple, or list of + tuples, of Text or String. For example: if we have a variable + attrs = [("foo", "bar")], we could interpolate that into an + element like: <p *{attrs}> to get <p foo="bar">. +

    +
  • +
+
+
+

Conditionals

+

Eventually, you’ll want to put in some logic in your page. The goal of Hamlet +is to make the logic as minimalistic as possible, pushing the heavy lifting +into Haskell. As such, our logical statements are very basic… so basic, that +it’s if, elseif, and else.

+
$if isAdmin
+    <p>Welcome to the admin section.
+$elseif isLoggedIn
+    <p>You are not the administrator.
+$else
+    <p>I don't know who you are. Please log in so I can decide if you get access.
+

All the same rules of normal interpolation apply to the content of the conditionals.

+
+
+

Maybe

+

Similarly, we have a special construct for dealing with Maybe values. This +could technically be dealt with using if, isJust and fromJust, but this +is more convenient and avoids partial functions.

+
$maybe name <- maybeName
+    <p>Your name is #{name}
+$nothing
+    <p>I don't know your name.
+

In addition to simple identifiers, you can use a few other, more complicated +values on the left hand side, such as constructors and tuples.

+
$maybe Person firstName lastName <- maybePerson
+    <p>Your name is #{firstName} #{lastName}
+

The right-hand-side follows the same rules as interpolations, allow variables, +function application, and so on.

+
+
+

Forall

+

And what about looping over lists? We have you covered there too:

+
$if null people
+    <p>No people.
+$else
+    <ul>
+        $forall person <- people
+            <li>#{person}
+
+
+

Case

+

Pattern matching is one of the great strengths of Haskell. Sum types let you +cleanly model many real-world types, and case statements let you safely +match, letting the compiler warn you if you missed a case. Hamlet gives you the +same power.

+
$case foo
+    $of Left bar
+        <p>It was left: #{bar}
+    $of Right baz
+        <p>It was right: #{baz}
+
+
+

With

+

Rounding out our statements, we have with. It’s basically just a convenience +for declaring a synonym for a long expression.

+
$with foo <- some very (long ugly) expression that $ should only $ happen once
+    <p>But I'm going to use #{foo} multiple times. #{foo}
+
+
+

Doctype

+

Last bit of syntactic sugar: the doctype statement. We have support for a +number of different versions of a doctype, though we recommend $doctype 5 +for modern web applications, which generates <!DOCTYPE html>.

+
$doctype 5
+<html>
+    <head>
+        <title>Hamlet is Awesome
+    <body>
+        <p>All done.
+ +
+
+
+

Lucius Syntax

+

Lucius is one of two CSS templating languages in the Shakespeare family. It is +intended to be a superset of CSS, leveraging the existing syntax while adding +in a few more features.

+
    +
  • +

    +Like Hamlet, we allow both variable and URL interpolation. +

    +
  • +
  • +

    +CSS blocks are allowed to nest. +

    +
  • +
  • +

    +You can declare variables in your templates. +

    +
  • +
  • +

    +A set of CSS properties can be created as a mixin, and reused in multiple + declarations. +

    +
  • +
+

Starting with the second point: let’s say you want to have some special styling +for some tags within your article. In plain ol' CSS, you’d have to write:

+
article code { background-color: grey; }
+article p { text-indent: 2em; }
+article a { text-decoration: none; }
+

In this case, there aren’t that many clauses, but having to type out article +each time is still a bit of a nuisance. Imagine if you had a dozen or so of +these. Not the worst thing in the world, but a bit of an annoyance. Lucius +helps you out here:

+
article {
+    code { background-color: grey; }
+    p { text-indent: 2em; }
+    a { text-decoration: none; }
+    > h1 { color: green; }
+}
+

Having Lucius variables allows you to avoid repeating yourself. A simple +example would be to define a commonly used color:

+
@textcolor: #ccc; /* just because we hate our users */
+body { color: #{textcolor} }
+a:link, a:visited { color: #{textcolor} }
+

Mixins are a relatively new addition to Lucius. The idea is to declare a mixin +providing a collection of properties, and then embed that mixin in a template +using caret interpolation (^). The following example demonstrates how we +could use a mixin to deal with vendor prefixes.

+
{-# LANGUAGE QuasiQuotes #-}
+import Text.Lucius
+import qualified Data.Text.Lazy.IO as TLIO
+
+-- Dummy render function.
+render = undefined
+
+-- Our mixin, which provides a number of vendor prefixes for transitions.
+transition val =
+    [luciusMixin|
+        -webkit-transition: #{val};
+        -moz-transition: #{val};
+        -ms-transition: #{val};
+        -o-transition: #{val};
+        transition: #{val};
+    |]
+
+-- Our actual Lucius template, which uses the mixin.
+myCSS =
+    [lucius|
+        .some-class {
+            ^{transition "all 4s ease"}
+        }
+    |]
+
+main = TLIO.putStrLn $ renderCss $ myCSS render
+
+
+

Cassius Syntax

+

Cassius is a whitespace-sensitive alternative to Lucius. As mentioned in the +synopsis, it uses the same processing engine as Lucius, but preprocesses all +input to insert braces to enclose subblocks and semicolons to terminate lines. +This means you can leverage all features of Lucius when writing Cassius. As a +simple example:

+
#banner
+    border: 1px solid #{bannerColor}
+    background-image: url(@{BannerImageR})
+
+
+

Julius Syntax

+

Julius is the simplest of the languages discussed here. In fact, some might +even say it’s really just Javascript. Julius allows the three forms of +interpolation we’ve mentioned so far, and otherwise applies no transformations +to your content.

+ +
+
+
+

Calling Shakespeare

+

The question of course arises at some point: how do I actually use this stuff? +There are three different ways to call out to Shakespeare from your Haskell +code:

+
+
+Quasiquotes +
+

+Quasiquotes allow you to embed arbitrary content within your Haskell, and for it to be converted into Haskell code at compile time. +

+
+
+External file +
+

+In this case, the template code is in a separate file which is referenced via Template Haskell. +

+
+
+Reload mode +
+

+Both of the above modes require a full recompile to see any changes. In reload mode, your template is kept in a separate file and referenced via Template Haskell. But at runtime, the external file is reparsed from scratch each time. +

+
+
+ +

One of the first two approaches should be used in production. They both embed +the entirety of the template in the final executable, simplifying deployment +and increasing performance. The advantage of the quasiquoter is the simplicity: +everything stays in a single file. For short templates, this can be a very good +fit. However, in general, the external file approach is recommended because:

+
    +
  • +

    +It follows nicely in the tradition of separating logic from presentation. +

    +
  • +
  • +

    +You can easily switch between external file and debug mode with some simple + CPP macros, meaning you can keep rapid development and still achieve high + performance in production. +

    +
  • +
+

Since these are special QuasiQuoters and Template Haskell functions, you need +to be sure to enable the appropriate language extensions and use correct +syntax. You can see a simple example of each in the following code snippets.

+

Quasiquoter

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE QuasiQuotes #-}
+import Text.Hamlet (HtmlUrl, hamlet)
+import Data.Text (Text)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: Text -> HtmlUrl MyRoute
+template title = [hamlet|
+$doctype 5
+<html>
+    <head>
+        <title>#{title}
+        <link rel=stylesheet href=@{Stylesheet}>
+    <body>
+        <h1>#{title}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml $ template "My Title" render
+

+

External file

+

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-} -- to control production versus debug
+import Text.Lucius (CssUrl, luciusFile, luciusFileReload, renderCss)
+import Data.Text (Text)
+import qualified Data.Text.Lazy.IO as TLIO
+
+data MyRoute = Home | Time | Stylesheet
+
+render :: MyRoute -> [(Text, Text)] -> Text
+render Home _ = "/home"
+render Time _ = "/time"
+render Stylesheet _ = "/style.css"
+
+template :: CssUrl MyRoute
+#if PRODUCTION
+template = $(luciusFile "template.lucius")
+#else
+template = $(luciusFileReload "template.lucius")
+#endif
+
+main :: IO ()
+main = TLIO.putStrLn $ renderCss $ template render
+

+
/* @template.lucius */
+foo { bar: baz }
+

The naming scheme for the functions is very consistent.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageQuasiquoterExternal fileReload

Hamlet

hamlet

hamletFile

N/A

Cassius

cassius

cassiusFile

cassiusFileReload

Lucius

lucius

luciusFile

luciusFileReload

Julius

julius

juliusFile

juliusFileReload

+
+

Alternate Hamlet Types

+

So far, we’ve seen how to generate an HtmlUrl value from Hamlet, which is a +piece of HTML with embedded type-safe URLs. There are currently three other +values we can generate using Hamlet: plain HTML, HTML with URLs and +internationalized messages, and widgets. That last one will be covered in more +detail in the widgets chapter.

+

To generate plain HTML without any embedded URLs, we use "simplified Hamlet". +There are a few changes:

+
    +
  • +

    +We use a different set of functions, prefixed with an "s". So the quasiquoter + is shamlet and the external file function is shamletFile. How we + pronounce those is still up for debate. +

    +
  • +
  • +

    +No URL interpolation is allowed. Doing so will result in a compile-time + error. +

    +
  • +
  • +

    +Embedding (the caret-interpolator) no longer allows arbitrary HtmlUrl + values. The rule is that the embedded value must have the same type as the + template itself, so in this case it must be Html. That means that for + shamlet, embedding can be completely replaced with normal variable + interpolation (with a hash). +

    +
  • +
+

Dealing with internationalization (i18n) in Hamlet is a bit complicated. Hamlet +supports i18n via a message datatype, very similar in concept and +implementation to a type-safe URL. As a motivating example, let’s say we want +to have an application that tells you hello and how many apples you bought. +We could represent those messages with a datatype.

+
data Msg = Hello | Apples Int
+

Next, we would want to be able to convert that into something human-readable, +so we define some render functions:

+
renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+

Now we want to interpolate those Msg values directly in the template. For that, we use underscore interpolation.

+
$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+

This kind of a template now needs some way to turn those values into HTML. So +just like type-safe URLs, we pass in a render function. To represent this, we +define a new type synonym:

+
type Render url = url -> [(Text, Text)] -> Text
+type Translate msg = msg -> Html
+type HtmlUrlI18n msg url = Translate msg -> Render url -> Html
+

At this point, you can pass renderEnglish, renderSpanish, or +renderKlingon to this template, and it will generate nicely translated output +(depending, of course, on the quality of your translators). The complete +program is:

+
{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+import Data.Text (Text)
+import qualified Data.Text as T
+import Text.Hamlet (HtmlUrlI18n, ihamlet)
+import Text.Blaze.Html (toHtml)
+import Text.Blaze.Html.Renderer.String (renderHtml)
+
+data MyRoute = Home | Time | Stylesheet
+
+renderUrl :: MyRoute -> [(Text, Text)] -> Text
+renderUrl Home _ = "/home"
+renderUrl Time _ = "/time"
+renderUrl Stylesheet _ = "/style.css"
+
+data Msg = Hello | Apples Int
+
+renderEnglish :: Msg -> Text
+renderEnglish Hello = "Hello"
+renderEnglish (Apples 0) = "You did not buy any apples."
+renderEnglish (Apples 1) = "You bought 1 apple."
+renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]
+
+template :: Int -> HtmlUrlI18n Msg MyRoute
+template count = [ihamlet|
+$doctype 5
+<html>
+    <head>
+        <title>i18n
+    <body>
+        <h1>_{Hello}
+        <p>_{Apples count}
+|]
+
+main :: IO ()
+main = putStrLn $ renderHtml
+     $ (template 5) (toHtml . renderEnglish) renderUrl
+
+
+
+

Other Shakespeare

+

In addition to HTML, CSS and Javascript helpers, there is also some more +general-purpose Shakespeare available. shakespeare-text provides a simple way +to create interpolated strings, much like people are accustomed to in scripting +languages like Ruby and Python. This package’s utility is definitely not +limited to Yesod.

+
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
+import Text.Shakespeare.Text
+import qualified Data.Text.Lazy.IO as TLIO
+import Data.Text (Text)
+import Control.Monad (forM_)
+
+data Item = Item
+    { itemName :: Text
+    , itemQty :: Int
+    }
+
+items :: [Item]
+items =
+    [ Item "apples" 5
+    , Item "bananas" 10
+    ]
+
+main :: IO ()
+main = forM_ items $ \item -> TLIO.putStrLn
+    [lt|You have #{show $ itemQty item} #{itemName item}.|]
+

Some quick points about this simple example:

+
    +
  • +

    +Notice that we have three different textual datatypes involved (String, + strict Text and lazy Text). They all play together well. +

    +
  • +
  • +

    +We use a quasiquoter named lt, which generates lazy text. There is also + st. +

    +
  • +
  • +

    +Also, there are longer names for these quasiquoters (ltext and stext). +

    +
  • +
  • +

    +The syntax for variable interpolation for Text.Shakespeare.Text is the same + as described above. Note that ^{..} and @{..} are also recognized in + lt and st. If the output of a template should contain ^{..}, a + backslash can be used to prevent the interpolation, + e.g. [lt|2^\{\23}|]. The backslash is removed from the resulting text. +

    +
  • +
+
+
+

General Recommendations

+

Here are some general hints from the Yesod community on how to get the most out +of Shakespeare.

+
    +
  • +

    +For actual sites, use external files. For libraries, it’s OK to use + quasiquoters, assuming they aren’t too long. +

    +
  • +
  • +

    +Patrick Brisbin has put together a + Vim code + highlighter that can help out immensely. +

    +
  • +
  • +

    +You should almost always start Hamlet tags on their own line instead of + embedding start/end tags after an existing tag. The only exception to this is + the occasional <i> or <b> tag inside a large block of text. +

    +
  • +
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/single-process-pubsub.html b/public/book/single-process-pubsub.html new file mode 100644 index 00000000..39f0d698 --- /dev/null +++ b/public/book/single-process-pubsub.html @@ -0,0 +1,326 @@ + Single process pub-sub :: Yesod Web Framework Book- Version 1.6 +
+

Single process pub-sub

+ + +

The previous example was admittedly quite simple. Let’s build on that +foundation (pun intended) to do something a bit more interesting. Suppose we +have a workflow on our site like the following:

+
    +
  1. +

    +Enter some information on page X, and submit. +

    +
  2. +
  3. +

    +Submission starts a background job, and the user is redirected to a page to view status of that job. +

    +
  4. +
  5. +

    +That second page will subscribe to updates from the background job and display them to the user. +

    +
  6. +
+

The core principle here is the ability to let one thread publish updates, and +have another thread subscribe to receive those updates. This is known generally +as pub/sub, and fortunately is very easy to achieve in Haskell via STM.

+

Like the previous chapter, let me start off with the caveat: this technique +only works properly if you have a single web application process. If you have +two different servers and a load balancer, you’d either need sticky sessions or +some other solution to make sure that the requests from a single user are going +to the same machine. In those situations, you may want to consider using an +external pubsub solution, such as Redis.

+

With that caveat out of the way, let’s get started.

+
+

Foundation datatype

+

We’ll need two different mutable references in our foundation. The first will +keep track of the next "job id" we’ll hand out. Each of these background jobs +will be represented by a unique identifier, which will be used in our URLs. +The second piece of data will be a map from the job ID to the broadcast channel +used for publishing updates. In code:

+
data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+

Notice that our TChan contains Maybe Text values. The reason for the +Maybe wrapper is so that we can indicate that the channel is complete, by +providing a Nothing value.

+
+
+

Allocating a job

+

In order to allocate a job, we need to:

+
    +
  1. +

    +Get a job ID. +

    +
  2. +
  3. +

    +Create a new broadcast channel. +

    +
  4. +
  5. +

    +Add the channel to the channel map. +

    +
  6. +
+

Due to the beauty of STM, this is pretty easy.

+
(jobId, chan) <- liftIO $ atomically $ do
+    jobId <- readTVar nextJob
+    writeTVar nextJob $! jobId + 1
+    chan <- newBroadcastTChan
+    m <- readTVar jobs
+    writeTVar jobs $ IntMap.insert jobId chan m
+    return (jobId, chan)
+
+
+

Fork our background job

+

There are many different ways we could go about this, and they depend entirely +on what the background job is going to be. Here’s a minimal example of a +background job that prints out a few messages, with a 1 second delay between +each message. Note how after our final message, we broadcast a Nothing value +and remove our channel from the map of channels.

+
liftIO $ forkIO $ do
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something\n"
+    threadDelay 1000000
+    atomically $ writeTChan chan $ Just "Did something else\n"
+    threadDelay 1000000
+    atomically $ do
+        writeTChan chan $ Just "All done\n"
+        writeTChan chan Nothing
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.delete jobId m
+
+
+

View progress

+

For this demonstration, I’ve elected for a very simple progress viewing: a +plain text page with stream response. There are a few other possibilities here: +an HTML page that auto-refreshes every X seconds or using eventsource or +websockets. I encourage you to give those a shot also, but here’s the simplest +implementation I can think of:

+
getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+

We start off by looking up the channel in the map. If we can’t find it, it +means the job either never existed, or has already been completed. In either +event, we return a 404. (Another possible enhancement would be to store some +information on all previously completed jobs and let the user know if they’re +done.)

+

Assuming the channel exists, we use respondSource to start a streaming +response. We then repeatedly call readTChan until we get a Nothing value, +at which point we exit (via return ()). Notice that on each iteration, we +call both sendChunkText and sendFlush. Without that second call, the user +won’t receive any updates until the output buffer completely fills up, which is +not what we want for a real-time update system.

+
+
+

Complete application

+

For completeness, here’s the full source code for this application:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Control.Concurrent     (forkIO, threadDelay)
+import           Control.Concurrent.STM
+import           Data.IntMap            (IntMap)
+import qualified Data.IntMap            as IntMap
+import           Data.Text              (Text)
+import           Yesod
+
+data App = App
+    { jobs    :: TVar (IntMap (TChan (Maybe Text)))
+    , nextJob :: TVar Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET POST
+/view-progress/#Int ViewProgressR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "PubSub example"
+    [whamlet|
+        <form method=post>
+            <button>Start new background job
+    |]
+
+postHomeR :: Handler ()
+postHomeR = do
+    App {..} <- getYesod
+    (jobId, chan) <- liftIO $ atomically $ do
+        jobId <- readTVar nextJob
+        writeTVar nextJob $! jobId + 1
+        chan <- newBroadcastTChan
+        m <- readTVar jobs
+        writeTVar jobs $ IntMap.insert jobId chan m
+        return (jobId, chan)
+    liftIO $ forkIO $ do
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something\n"
+        threadDelay 1000000
+        atomically $ writeTChan chan $ Just "Did something else\n"
+        threadDelay 1000000
+        atomically $ do
+            writeTChan chan $ Just "All done\n"
+            writeTChan chan Nothing
+            m <- readTVar jobs
+            writeTVar jobs $ IntMap.delete jobId m
+    redirect $ ViewProgressR jobId
+
+getViewProgressR :: Int -> Handler TypedContent
+getViewProgressR jobId = do
+    App {..} <- getYesod
+    mchan <- liftIO $ atomically $ do
+        m <- readTVar jobs
+        case IntMap.lookup jobId m of
+            Nothing -> return Nothing
+            Just chan -> fmap Just $ dupTChan chan
+    case mchan of
+        Nothing -> notFound
+        Just chan -> respondSource typePlain $ do
+            let loop = do
+                    mtext <- liftIO $ atomically $ readTChan chan
+                    case mtext of
+                        Nothing -> return ()
+                        Just text -> do
+                            sendChunkText text
+                            sendFlush
+                            loop
+            loop
+
+main :: IO ()
+main = do
+    jobs <- newTVarIO IntMap.empty
+    nextJob <- newTVarIO 1
+    warp 3000 App {..}
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/sql-joins.html b/public/book/sql-joins.html new file mode 100644 index 00000000..664f866c --- /dev/null +++ b/public/book/sql-joins.html @@ -0,0 +1,560 @@ + SQL Joins :: Yesod Web Framework Book- Version 1.6 +
+

SQL Joins

+ + +

Persistent touts itself as a database-agnostic interface. How, then, are you +supposed to do things which are inherently backend-specific? This most often +comes up in Yesod when you want to join two tables together. There are some +pure-Haskell solutions that are completely backend-agonistic, but there are +also more efficient methods at our disposal. In this chapter, we’ll introduce a +common problem you might want to solve, and then build up more sophisticated +solutions.

+
+

Multi-author blog

+

Since blogs are a well understood problem domain, we’ll use that for our +problem setup. Consider a blog engine that allows you to have multiple authors +in the database, and each blog post will have a single author. In Persistent, +we may model this as:

+
Author
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+

Let’s set up our initial Yesod application to show a blog post index indicating +the blog title and the author:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import           Control.Monad.Logger
+import           Data.Text               (Text)
+import           Database.Persist.Sqlite
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Author
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+|]
+
+data App = App
+    { persistConfig :: SqliteConf
+    , connPool      :: ConnectionPool
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/blog/#BlogId BlogR GET
+|]
+
+instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB = defaultRunDB persistConfig connPool
+instance YesodPersistRunner App where
+    getDBRunner = defaultGetDBRunner connPool
+
+getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ selectList [] []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall Entity blogid blog <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>
+                            #{blogTitle blog} by #{show $ blogAuthor blog}
+        |]
+
+getBlogR :: BlogId -> Handler Html
+getBlogR _ = error "Implementation left as exercise to reader"
+
+main :: IO ()
+main = do
+    -- Use an in-memory database with 1 connection. Terrible for production,
+    -- but useful for testing.
+    let conf = SqliteConf ":memory:" 1
+    pool <- createPoolConfig conf
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        -- Fill in some testing data
+        alice <- insert $ Author "Alice"
+        bob   <- insert $ Author "Bob"
+
+        insert_ $ Blog alice "Alice's first post" "Hello World!"
+        insert_ $ Blog bob "Bob's first post" "Hello World!!!"
+        insert_ $ Blog alice "Alice's second post" "Goodbye World!"
+
+    warp 3000 App
+        { persistConfig = conf
+        , connPool      = pool
+        }
+

That’s all well and good, but let’s look at the output:

+

Authors appear as numeric identifiers

+ + + + + + +
+

All we’re doing is displaying the numeric identifier of each author, instead of +the author’s name. In order to fix this, we need to pull extra information from +the Author table as well. Let’s dive in to getting that done.

+
+
+

Database queries in Widgets

+

I’ll address this one right off the bat, since it catches many users by +surprise. You might think that you can solve this problem in the Hamlet +template itself, e.g.:

+
<ul>
+    $forall Entity blogid blog <- blogs
+        $with author <- runDB $ get404 $ blogAuthor
+            <li>
+                <a href=@{BlogR blogid}>
+                    #{blogTitle blog} by #{authorName author}
+

However, this isn’t allowed, because Hamlet will not allow you to run +database actions inside of it. One of the goals of Shakespearean templates is +to help you keep your pure and impure code separated, with the idea being that +all impure code needs to stay in Haskell.

+

But we can actually tweak the above code to work in Yesod. The idea is to +separate out the code for each blog entry into a Widget function, and then +perform the database action in the Haskell portion of the function:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ selectList [] []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall blogEntity <- blogs
+                    ^{showBlogLink blogEntity}
+        |]
+
+showBlogLink :: Entity Blog -> Widget
+showBlogLink (Entity blogid blog) = do
+    author <- handlerToWidget $ runDB $ get404 $ blogAuthor blog
+    [whamlet|
+        <li>
+            <a href=@{BlogR blogid}>
+                #{blogTitle blog} by #{authorName author}
+    |]
+

We need to use handlerToWidget to turn our Handler action into a Widget +action, but otherwise the code is straightforward. And furthermore, we now get +exactly the output we wanted:

+

Authors appear as names

+ + + + + + +
+
+
+

Joins

+

If we have the exact result we’re looking for, why isn’t this chapter over? The +problem is that this technique is highly inefficient. We’re performing one +database query to load up all of the blog posts, then a separate query for each +blog post to get the author names. This is far less efficient than simply using +a SQL join. The question is: how do we do a join in Persistent? We’ll start off +by writing some raw SQL:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB $ rawSql
+        "SELECT ??, ?? \
+        \FROM blog INNER JOIN author \
+        \ON blog.author=author.id"
+        []
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall (Entity blogid blog, Entity _ author) <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>
+                            #{blogTitle blog} by #{authorName author}
+        |]
+

We pass the rawSql function two parameters: a SQL query, and a list of +additional parameters to replace placeholders in the query. That list is empty, +since we’re not using any placeholders. However, note that we’re using ?? in +our SELECT statement. This is a form of type inspection: rawSql will detect +the type of entities being demanded, and automatically fill in the fields that +are necessary to make the query.

+

rawSql is certainly powerful, but it’s also unsafe. There’s no syntax +checking on your SQL query string, so you can get runtime errors. Also, it’s +easy to end up querying for the wrong type and end up with very confusing +runtime error messages.

+
+
+

Esqueleto

+ +

Persistent has a companion library- Esqueleto- which provides an expressive, +type safe DSL for writing SQL queries. It takes advantage of the Persistent +types to ensure it generates valid SQL queries and produces the results +requested by the program. In order to use Esqueleto, we’re going to add some +imports:

+
import qualified Database.Esqueleto      as E
+import           Database.Esqueleto      ((^.))
+

And then write our query using Esqueleto:

+
getHomeR :: Handler Html
+getHomeR = do
+    blogs <- runDB
+           $ E.select
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    defaultLayout $ do
+        setTitle "Blog posts"
+        [whamlet|
+            <ul>
+                $forall (E.Value blogid, E.Value title, E.Value name) <- blogs
+                    <li>
+                        <a href=@{BlogR blogid}>#{title} by #{name}
+        |]
+

Notice how similar the query looks to the SQL we wrote previously. One thing of +particular interest is the ^. operator, which is a projection. blog ^. +BlogAuthor, for example, means "take the author column of the blog table." +And thanks to the type safety of Esqueleto, you could never accidentally +project AuthorName from blog: the type system will stop you!

+

In addition to safety, there’s also a performance advantage to Esqueleto. +Notice the returned tuple; it explicitly lists the three columns that we +need to generate our listing. This can provide a huge performance boost: unlike +all other examples we’ve had, this one does not require transferring the +(potentially quite large) content column of the blog post to generate the +listing.

+ +

Esqueleto is really the gold standard in writing SQL queries in Persistent. The +rule of thumb should be: if you’re doing something that fits naturally into +Persistent’s query syntax, use Persistent, as it’s database agnostic and a bit +easier to use. But if you’re doing something that would be more efficient with +a SQL-specific feature, you should strongly consider Esqueleto.

+
+
+

Streaming

+

There’s still a problem with our Esqueleto approach. If there are thousands of +blog posts, then the workflow will be:

+
    +
  1. +

    +Read thousands of blog posts into memory on the server. +

    +
  2. +
  3. +

    +Render out the entire HTML page. +

    +
  4. +
  5. +

    +Send the HTML page to the client. +

    +
  6. +
+

This has two downsides: it uses a lot of memory, and it gives high latency for the user. If this is a bad approach, why does Yesod gear you towards it out of the box, instead of tending towards a streaming approach? Two reasons:

+
    +
  • +

    +Correctness: imagine if there was an error reading the 243rd record from the database. By doing a non-streaming response, Yesod can catch the exception and send a meaningful 500 error response. If we were already streaming, the streaming body would simply stop in the middle of a misleading 200 OK respond. +

    +
  • +
  • +

    +Ease of use: it’s usually easier to work with non-streaming bodies. +

    +
  • +
+

The standard recommendation I’d give someone who wants to generate listings +that may be large is to use pagination. This allows you to do less work on the +server, write simple code, get the correctness guarantees Yesod provides out of +the box, and reduce user latency. However, there are times when you’ll really +want to do a streaming response, so let’s cover that here.

+

Switching Esqueleto to a streaming response is easy: replace select with +selectSource. The Esqueleto query itself remains unchanged. Then we’ll use +the respondSourceDB function to generate a streaming database response, and +manually construct our HTML to wrap up the listing.

+
getHomeR :: Handler TypedContent
+getHomeR = do
+    let blogsSrc =
+             E.selectSource
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    render <- getUrlRenderParams
+    respondSourceDB typeHtml $ do
+        sendChunkText "<html><head><title>Blog posts</title></head><body><ul>"
+        blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) ->
+            toFlushBuilder $
+            [hamlet|
+                <li>
+                    <a href=@{BlogR blogid}>#{title} by #{name}
+            |] render
+            )
+        sendChunkText "</ul></body></html>"
+

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 +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 +Flush Builder value, as needed by Yesod’s streaming framework.

+

Unfortunately, we’re no longer able to take advantage of Hamlet to do our +overall page layout, since we need to explicit generate start and end tags +separately. This introduces another point for possible bugs, if we accidentally +create unbalanced tags. We also lose the ability to use defaultLayout, for +exactly the same reason.

+

Streaming HTML responses are a powerful tool, and are sometimes necessary. But +generally speaking, I’d recommend sticking to safer options.

+
+
+

Conclusion

+

This chapter covered a number of ways of doing a SQL join:

+
    +
  • +

    +Avoid the join entirely, and manually grab the associated data in Haskell. This is also known as an application level join. +

    +
  • +
  • +

    +Write the SQL explicitly with rawSql. While somewhat convenient, this loses a lot of Persistent’s type safety. +

    +
  • +
  • +

    +Use Esqueleto’s DSL functionality to create a type-safe SQL query. +

    +
  • +
  • +

    +And if you need it, you can even generate a streaming response from Esqueleto. +

    +
  • +
+

For completeness, here’s the entire body of the final, streaming example:

+
{-# LANGUAGE EmptyDataDecls             #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE ViewPatterns               #-}
+import           Control.Monad.Logger
+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
+    name Text
+Blog
+    author AuthorId
+    title Text
+    content Html
+|]
+
+data App = App
+    { persistConfig :: SqliteConf
+    , connPool      :: ConnectionPool
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/blog/#BlogId BlogR GET
+|]
+
+instance Yesod App
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB = defaultRunDB persistConfig connPool
+instance YesodPersistRunner App where
+    getDBRunner = defaultGetDBRunner connPool
+
+getHomeR :: Handler TypedContent
+getHomeR = do
+    let blogsSrc =
+             E.selectSource
+           $ E.from $ \(blog `E.InnerJoin` author) -> do
+                E.on $ blog ^. BlogAuthor E.==. author ^. AuthorId
+                return
+                    ( blog   ^. BlogId
+                    , blog   ^. BlogTitle
+                    , author ^. AuthorName
+                    )
+
+    render <- getUrlRenderParams
+    respondSourceDB typeHtml $ do
+        sendChunkText "<html><head><title>Blog posts</title></head><body><ul>"
+        blogsSrc $= CL.map (\(E.Value blogid, E.Value title, E.Value name) ->
+            toFlushBuilder $
+            [hamlet|
+                <li>
+                    <a href=@{BlogR blogid}>#{title} by #{name}
+            |] render
+            )
+        sendChunkText "</ul></body></html>"
+
+getBlogR :: BlogId -> Handler Html
+getBlogR _ = error "Implementation left as exercise to reader"
+
+main :: IO ()
+main = do
+    -- Use an in-memory database with 1 connection. Terrible for production,
+    -- but useful for testing.
+    let conf = SqliteConf ":memory:" 1
+    pool <- createPoolConfig conf
+    flip runSqlPersistMPool pool $ do
+        runMigration migrateAll
+
+        -- Fill in some testing data
+        alice <- insert $ Author "Alice"
+        bob   <- insert $ Author "Bob"
+
+        insert_ $ Blog alice "Alice's first post" "Hello World!"
+        insert_ $ Blog bob "Bob's first post" "Hello World!!!"
+        insert_ $ Blog alice "Alice's second post" "Goodbye World!"
+
+    warp 3000 App
+        { persistConfig = conf
+        , connPool      = pool
+        }
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/understanding-request.html b/public/book/understanding-request.html new file mode 100644 index 00000000..2295d0e4 --- /dev/null +++ b/public/book/understanding-request.html @@ -0,0 +1,666 @@ + Understanding a Request :: Yesod Web Framework Book- Version 1.6 +
+

Understanding a Request

+ + +

You can often times get away with using Yesod for quite a while without needing +to understand its internal workings. However, such an understanding is often +times advantageous. This chapter will walk you through the request handling +process for a fairly typical Yesod application. Note that a fair amount of this +discussion involves code changes in Yesod 1.2. Most of the concepts are the +same in previous versions, though the data types involved were a bit messier.

+

Yesod’s usage of Template Haskell to bypass boilerplate code can make it +a bit difficult to understand this process sometimes. If beyond the information +in this chapter you wish to further analyze things, it can be useful to view +GHC’s generated code using -ddump-splices.

+ +
+

Handlers

+

When trying to understand Yesod request handling, we need to look at two +components: how a request is dispatched to the appropriate handler code, and +how handler functions are processed. We’ll start off with the latter, and +then circle back to understanding the dispatch process itself.

+
+

Layers

+

Yesod builds itself on top of WAI, which provides a protocol for web servers +(or, more generally, handlers) and applications to communicate with each +other. This is expressed through two datatypes: Request and Response. Then, an +Application is defined as type:

+
type Application = Request
+                -> (Response -> IO ResponseReceived)
+                -> IO ResponseReceived
+

A WAI handler will take an application and run it.

+ +

Request and Response are both very low-level, trying to represent the HTTP +protocol without too much embellishment. This keeps WAI as a generic tool, but +also leaves out a lot of the information we need in order to implement a web +framework. For example, WAI will provide us with the raw data for all request +headers. But Yesod needs to parse that to get cookie information, and then +parse the cookies in order to extract session information.

+

To deal with this dichotomy, Yesod introduces two new data types: +YesodRequest and YesodResponse. YesodRequest contains a WAI Request, and +also adds in such request information as cookies and session variables, and on +the response side can either be a standard WAI Response, or be a higher-level +representation of such a response including such things as updated session +information and extra response headers. To parallel WAI’s Application, we +have:

+
type YesodApp = YesodRequest -> ResourceT IO YesodResponse
+ +

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 +handler functions, you need to have access to three different things:

+
    +
  • +

    +The YesodRequest value for the current request. +

    +
  • +
  • +

    +Some basic environment information, like how to log messages or handle error conditions. This is provided by the datatype RunHandlerEnv. +

    +
  • +
  • +

    +A mutable variable to keep track of updateable information, such as the headers to be returned and the user session state. This is called GHState. (I know that’s not a great name, but it’s there for historical reasons.) +

    +
  • +
+

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 +a step further and converts all the way to a WAI Application.

+
+
+

Content

+

Our example above, and many others you’ve already seen, give a handler +with a type of Handler Html. We’ve just described what the Handler means, +but how does Yesod know how to deal with Html? The answer lies in the +ToTypedContent typeclass. The relevants bit of code are:

+
data Content = ContentBuilder !BBuilder.Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush BBuilder.Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+data TypedContent = TypedContent !ContentType !Content
+
+class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

The Content datatype represents the different ways you can provide a response +body. The first three mirror WAI’s representation directly. The fourth +(ContentDontEvaluate) is used to indicate to Yesod whether response bodies +should be fully evaluated before being returned to users. The advantage to +fully evaluating is that we can provide meaningful error messages if an +exception is thrown from pure code. The downside is possibly increased time and +memory usage.

+

In any event, Yesod knows how to turn a Content into a response body. The +ToContent typeclass provides a way to allow many different datatypes to be +converted into response bodies. Many commonly used types are already instances +of ToContent, including strict and lazy ByteString and Text, and of course +Html.

+

TypedContent adds an extra piece of information: the content type of the value. +As you might expect, there are ToTypedContent instances for a number of common +datatypes, including Html, aeson’s Value (for JSON), and Text (treated as plain text).

+
instance ToTypedContent J.Value where
+    toTypedContent v = TypedContent typeJson (toContent v)
+instance ToTypedContent Html where
+    toTypedContent h = TypedContent typeHtml (toContent h)
+instance ToTypedContent T.Text where
+    toTypedContent t = TypedContent typePlain (toContent t)
+

Putting this all together: a Handler is able to return any value which is an +instance of ToTypedContent, and Yesod will handle turning it into an +appropriate representation and setting the Content-Type response header.

+
+
+

Short-circuit responses

+

One other oddity is how short-circuiting works. For example, you can call +redirect in the middle of a handler function, and the rest of the function will +not be called. The mechanism we use is standard Haskell exceptions. Calling +redirect just throws an exception of type HandlerContents. The runHandler +function will catch any exceptions thrown and produce an appropriate response. +For HandlerContents, each constructor gives a clear action to perform, be it +redirecting or sending a file. For all other exception types, an error message +is displayed to the user.

+
+
+
+

Dispatch

+

Dispatch is the act of taking an incoming request and generating an appropriate +response. We have a few different constraints regarding how we want to handle +dispatch:

+
    +
  • +

    +Dispatch based on path segments (or pieces). +

    +
  • +
  • +

    +Optionally dispatch on request method. +

    +
  • +
  • +

    +Support subsites: packaged collections of functionality providing multiple routes under a specific URL prefix. +

    +
  • +
  • +

    +Support using WAI applications as subsites, while introducing as little + runtime overhead to the process as possible. In particular, we want to avoid + performing any unnecessary parsing to generate a YesodRequest if it + won’t be used. +

    +
  • +
+

The lowest common denominator for this would be to simply use a WAI +Application. However, this doesn’t provide quite enough information: we +need access to the foundation datatype, the logger, and for subsites how a +subsite route is converted to a parent site route. To address this, we have two +helper data types- YesodRunnerEnv and YesodSubRunnerEnv- providing this extra +information for normal sites and subsites.

+

With those types, dispatch now becomes a relatively simple matter: give me an +environment and a request, and I’ll give you a response. This is +represented by the typeclasses YesodDispatch and YesodSubDispatch:

+
class Yesod site => YesodDispatch site where
+    yesodDispatch :: YesodRunnerEnv site -> W.Application
+
+class YesodSubDispatch sub m where
+    yesodSubDispatch :: YesodSubRunnerEnv sub (HandlerSite m) m
+                     -> W.Application
+

We’ll see a bit later how YesodSubDispatch is used. Let’s first +understand how YesodDispatch comes into play.

+
+

toWaiApp, toWaiAppPlain, and warp

+

Let’s assume for the moment that you have a datatype which is an instance +of YesodDispatch. You’ll want to now actually run this thing somehow. To +do this, we need to convert it into a WAI Application and pass it to some kind +of a WAI handler/server. To start this journey, we use toWaiAppPlain. It +performs any appwide initialization necessary. At the time of writing, this +means allocating a logger and setting up the session backend, but more +functionality may be added in the future. Using this data, we can now create a +YesodRunnerEnv. And when that value is passed to yesodDispatch, we get a WAI +Application.

+

We’re almost done. The final remaining modification is path segment +cleanup. The Yesod typeclass includes a member function named cleanPath which +can be used to create canonical URLs. For example, the default implementation +would remove double slashes and redirect a user from /foo//bar to /foo/bar. +toWaiAppPlain adds in some pre-processing to the normal WAI request by +analyzing the requested path and performing cleanup/redirect as necessary.

+

At this point, we have a fully functional WAI Application. There are two other +helper functions included. toWaiApp wraps toWaiAppPlain and additionally +includes some commonly used WAI middlewares, including request logging and GZIP +compression. (Please see the Haddocks for an up-to-date list.) We finally have +the warp function which, as you might guess, runs your application with Warp.

+ +
+
+

Generated code

+

The last remaining black box is the Template Haskell generated code. This +generated code is responsible for handling some of the tedious, error-prone +pieces of your site. If you want to, you can write these all by hand instead. +We’ll demonstrate what that translation would look like, and in the +process elucidate how YesodDispatch and YesodSubDispatch work. Let’s +start with a fairly typical Yesod application.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+
+instance Yesod App
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req sendResponse = sendResponse $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+

For completeness we’ve provided a full listing, but let’s focus on +just the Template Haskell portion:

+
mkYesod "App" [parseRoutes|
+/only-get       OnlyGetR   GET
+/any-method     AnyMethodR
+/has-param/#Int HasParamR  GET
+/my-subsite     MySubsiteR WaiSubsite getMySubsite
+|]
+

While this generates a few pieces of code, we only need to replicate three +components to make our site work. Let’s start with the simplest: the +Handler type synonym:

+
type Handler = HandlerT App IO
+

Next is the type-safe URL and its rendering function. The rendering function is +allowed to generate both path segments and query string parameters. Standard +Yesod sites never generate query-string parameters, but it is technically +possible. And in the case of subsites, this often does happen. Notice how we +handle the qs parameter for the MySubsiteR case:

+
instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+

You can see that there’s a fairly simple mapping from the higher-level +route syntax and the RenderRoute instance. Each route becomes a constructor, +each URL parameter becomes an argument to its constructor, we embed a route for +the subsite, and use toPathPiece to render parameters to text.

+

The final component is the YesodDispatch instance. Let’s look at this in +a few pieces.

+
instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+

As described above, yesodDispatch is handed both an environment and a WAI +Request value. We can now perform dispatch based on the requested path, or in +WAI terms, the pathInfo. Referring back to our original high-level route +syntax, we can see that our first route is going to be the single piece +only-get, which we pattern match for.

+

Once that match has succeeded, we additionally pattern match on the request +method; if it’s GET, we use the handler function getOnlyGetR. +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 +to the handler function, and then convert the resulting YesodResponse into a +WAI Response.

+

Wonderful; one down, three to go. The next one is even easier.

+
            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+

Unlike OnlyGetR, AnyMethodR will work for any request method, so we don’t +need to perform any further pattern matching.

+
            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+

We add in one extra complication here: a dynamic parameter. While we used +toPathPiece to render to a textual value above, we now use fromPathPiece to +perform the parsing. Assuming the parse succeeds, we then follow a very similar +dispatch system as was used for OnlyGetR. The prime difference is that our +parameter needs to be passed to both the handler function and the route data +constructor.

+

Next we’ll look at the subsite, which is quite different.

+
            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+

Unlike the other pattern matches, here we just look to see if our pattern +prefix matches. Any route beginning with /my-subsite should be passed off to +the subsite for processing. This is where we finally get to use +yesodSubDispatch. This function closely mirrors yesodDispatch. We need to +construct a new environment to be passed to it. Let’s discuss the four +fields:

+
    +
  • +

    +ysreGetSub demonstrates how to get the subsite foundation type from the + master site. We provide getMySubsite, which is the function we provided in + the high-level route syntax. +

    +
  • +
  • +

    +ysreParentRunner provides a means of running a handler function. It may seem + a bit boring to just provide yesodRunner, but by having a separate parameter + we allow the construction of deeply nested subsites, which will wrap and + unwrap many layers of interleaving subsites. (This is a more advanced concept + which we won’t be covering in this chapter.) +

    +
  • +
  • +

    +ysreToParentRoute will convert a route for the subsite into a route for the + parent site. This is the purpose of the MySubsiteR constructor. This allows + subsites to use functions such as getRouteToParent. +

    +
  • +
  • +

    +ysreParentEnv simply passes on the initial environment, which contains a + number of things the subsite may need (such as logger). +

    +
  • +
+

The other interesting thing is how we modify the pathInfo. This allows subsites +to continue dispatching from where the parent site left off. To demonstrate +how this works, see some screenshots of various requests in the following +figure.

+

Path info in subsite

+ + + + + + +
+

And finally, not all requests will be valid routes. For those cases, we just +want to respond with a 404 not found.

+
            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+
+

Complete code

+

Following is the full code for the non-Template Haskell approach.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           Network.HTTP.Types         (status200)
+import           Network.Wai                (pathInfo, rawPathInfo,
+                                             requestMethod, responseLBS)
+import           Yesod
+import           Yesod.Core.Types           (YesodSubRunnerEnv (..))
+
+data App = App
+
+instance RenderRoute App where
+    data Route App = OnlyGetR
+                   | AnyMethodR
+                   | HasParamR Int
+                   | MySubsiteR (Route WaiSubsite)
+        deriving (Show, Read, Eq)
+
+    renderRoute OnlyGetR = (["only-get"], [])
+    renderRoute AnyMethodR = (["any-method"], [])
+    renderRoute (HasParamR i) = (["has-param", toPathPiece i], [])
+    renderRoute (MySubsiteR subRoute) =
+        let (ps, qs) = renderRoute subRoute
+         in ("my-subsite" : ps, qs)
+
+type Handler = HandlerT App IO
+
+instance Yesod App
+
+instance YesodDispatch App where
+    yesodDispatch env req =
+        case pathInfo req of
+            ["only-get"] ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        getOnlyGetR
+                        env
+                        (Just OnlyGetR)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just OnlyGetR)
+                        req
+            ["any-method"] ->
+                yesodRunner handleAnyMethodR env (Just AnyMethodR) req
+            ["has-param", t] | Just i <- fromPathPiece t ->
+                case requestMethod req of
+                    "GET" -> yesodRunner
+                        (getHasParamR i)
+                        env
+                        (Just $ HasParamR i)
+                        req
+                    _ -> yesodRunner
+                        (badMethod >> return ())
+                        env
+                        (Just $ HasParamR i)
+                        req
+            ("my-subsite":rest) -> yesodSubDispatch
+                YesodSubRunnerEnv
+                    { ysreGetSub = getMySubsite
+                    , ysreParentRunner = yesodRunner
+                    , ysreToParentRoute = MySubsiteR
+                    , ysreParentEnv = env
+                    }
+                req { pathInfo = rest }
+            _ -> yesodRunner (notFound >> return ()) env Nothing req
+
+getOnlyGetR :: Handler Html
+getOnlyGetR = defaultLayout
+    [whamlet|
+        <p>Accessed via GET method
+        <form method=post action=@{AnyMethodR}>
+            <button>POST to /any-method
+    |]
+
+handleAnyMethodR :: Handler Html
+handleAnyMethodR = do
+    req <- waiRequest
+    defaultLayout
+        [whamlet|
+            <p>In any-method, method == #{show $ requestMethod req}
+        |]
+
+getHasParamR :: Int -> Handler String
+getHasParamR i = return $ show i
+
+getMySubsite :: App -> WaiSubsite
+getMySubsite _ =
+    WaiSubsite app
+  where
+    app req sendResponse = sendResponse $ responseLBS
+        status200
+        [("Content-Type", "text/plain")]
+        $ L8.pack $ concat
+            [ "pathInfo == "
+            , show $ pathInfo req
+            , ", rawPathInfo == "
+            , show $ rawPathInfo req
+            ]
+
+main :: IO ()
+main = warp 3000 App
+
+
+
+

Conclusion

+

Yesod abstracts away quite a bit of the plumbing from you as a developer. Most +of this is boilerplate code that you’ll be happy to ignore. But it can be +empowering to understand exactly what’s going on under the surface. At +this point, you should hopefully be able- with help from the Haddocks- to write +a site without any of the autogenerated Template Haskell code. Not that +I’d recommend it; I think using the generated code is easier and safer.

+

One particular advantage of understanding this material is seeing where Yesod +sits in the world of WAI. This makes it easier to see how Yesod will interact +with WAI middleware, or how to include code from other WAI framework in a Yesod +application (or vice-versa!).

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/visitor-counter.html b/public/book/visitor-counter.html new file mode 100644 index 00000000..1e1a33e1 --- /dev/null +++ b/public/book/visitor-counter.html @@ -0,0 +1,163 @@ + Visitor counter :: Yesod Web Framework Book- Version 1.6 +
+

Visitor counter

+ + +

Remember back in the good ol' days of the internet, where no website was +complete without a little "you are visitor number 32" thingy? Ahh, those were +the good times! Let’s recreate that wonderful experience in Yesod!

+

Now, if we wanted to do this properly, we’d store this information in some kind +of persistent storage layer, like a database, so that the information could be +shared across multiple horizontally-scaled web servers, and so that the +information would survive an app restart.

+

But our goal here isn’t to demonstrate good practice (after all, if it was +about good practice, I wouldn’t be demonstrating a visitor counter, right?). +Instead, this is meant to provide a simple example of sharing some state among +multiple handlers. A real-world use case would be caching information across +requests. Just remember that when you use the technique we’ll be showing, you +need to be careful about multiple app servers and app restarts.

+

The technique is simple: we create a new field in the foundation datatype for a +mutable reference to some data, and then access it in each handler. The +technique is so simple, it’s worth just diving into the code:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Data.IORef
+import           Yesod
+
+data App = App
+    { visitors :: IORef Int
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    visitorsRef <- fmap visitors getYesod
+    visitors <-
+        liftIO $ atomicModifyIORef visitorsRef $ \i ->
+        (i + 1, i + 1)
+    defaultLayout
+        [whamlet|
+            <p>Welcome, you are visitor number #{visitors}.
+        |]
+
+main :: IO ()
+main = do
+    visitorsRef <- newIORef 0
+    warp 3000 App
+        { visitors = visitorsRef
+        }
+

I used IORef here, since we didn’t need anything more than it provided, but +you’re free to use MVars or TVars as well. In fact, a good exercise for +the reader is to modify the above program to store the visitor count in a +TVar instead.

+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/web-application-interface.html b/public/book/web-application-interface.html new file mode 100644 index 00000000..3a252fb2 --- /dev/null +++ b/public/book/web-application-interface.html @@ -0,0 +1,345 @@ + Web Application Interface :: Yesod Web Framework Book- Version 1.6 +
+

Web Application Interface

+ + + +

It is a problem almost every language used for web development has dealt with: +the low level interface between the web server and the application. The +earliest example of a solution is the venerable and battle-worn Common Gateway Interface (CGI), +providing a language-agnostic interface using only standard input, standard +output and environment variables.

+

Back when Perl was becoming the de facto web programming language, a major +shortcoming of CGI became apparent: the process needed to be started anew for +each request. When dealing with an interpretted language and application +requiring database connection, this overhead became unbearable. FastCGI (and +later SCGI) arose as a successor to CGI, but it seems that much of the +programming world went in a different direction.

+

Each language began creating its own standard for interfacing with servers. +mod_perl. mod_python. mod_php. mod_ruby. Within the same language, multiple +interfaces arose. In some cases, we even had interfaces on top of interfaces. +And all of this led to much duplicated effort: a Python application designed to +work with FastCGI wouldn’t work with mod_python; mod_python only exists for +certain webservers; and these programming language specific web server +extensions need to be written for each programming language.

+

Haskell has its own history. We originally had the cgi package, which provided +a monadic interface. The fastcgi package then provided the same interface. +Meanwhile, it seemed that the majority of Haskell web development focused on +the standalone server. The problem is that each server comes with its own +interface, meaning that you need to target a specific backend. This means that +it is impossible to share common features, like GZIP encoding, development +servers, and testing frameworks.

+

WAI attempts to solve this, by providing a generic and efficient interface +between web servers and applications. Any handler supporting the interface +can serve any WAI application, while any application using the interface can +run on any handler.

+

At the time of writing, there are various backends, including Warp, FastCGI, +and development server. There are even more esoteric backends like +wai-handler-webkit for creating desktop apps. wai-extra provides many common +middleware components like GZIP, JSON-P and virtual hosting. wai-test makes it +easy to write unit tests, and wai-handler-devel lets you develop your +applications without worrying about stopping to compile. Yesod targets WAI, as +well as other Haskell web frameworks such as Scotty and MFlow. It’s also used +by some applications that skip the framework entirely, including Hoogle.

+ +
+

The Interface

+

The interface itself is very straight-forward: an application takes a request +and returns a response. A response is an HTTP status, a list of headers and a +response body. A request contains various information: the requested path, +query string, request body, HTTP version, and so on.

+

In order to handle resource management in an exception-safe manner, we use +continuation passing style for returning the response, similar to how the +bracket function works. This makes our definition of an application look +like:

+
type Application =
+    Request ->
+    (Response -> IO ResponseReceived) ->
+    IO ResponseReceived
+

The first argument is a Request, which shouldn’t be too surprising. The +second argument is the continuation, or what we should do with a Response. +Generally speaking, this will just be sending it to the client. We use the +special ResponseReceived type to ensure that the application does in fact +call the continuation.

+

This may seem a little strange, but usage is pretty straight-forward, as we’ll +demonstrate below.

+
+

Response Body

+

Haskell has a datatype known as a lazy bytestring. By utilizing laziness, you +can create large values without exhausting memory. Using lazy I/O, you can do +such tricks as having a value which represents the entire contents of a file, +yet only occupies a small memory footprint. In theory, a lazy bytestring is the +only representation necessary for a response body.

+

In practice, while lazy byte strings are wonderful for generating "pure" +values, the lazy I/O necessary to read a file introduces some non-determinism +into our programs. When serving thousands of small files a second, the limiting +factor is not memory, but file handles. Using lazy I/O, file handles may not be +freed immediately, leading to resource exhaustion. To deal with this, WAI +provides its own streaming data interface.

+

The core of this streaming interface is the Builder. A Builder represents +an action to fill up a buffer with bytes of data. This is more efficient than +simply passing ByteStrings around, as it can avoid multiple copies of data. +In many cases, an application needs only to provide a single Builder value. +And for that simple case, we have the ResponseBuilder constructor.

+

However, there are times when an Application will need to interleave IO +actions with yielding of data to the client. For that case, we have +ResponseStream. With ResponseStream, you provide a function. This +function in turn takes two actions: a "yield more data" action, and a "flush +the buffer" action. This allows you to yield data, perform IO actions, and +flush, as many times as you need, and with any interleaving desired.

+

There is one further optimization: many operating systems provide a sendfile +system call, which sends a file directly to a socket, bypassing a lot of the +memory copying inherent in more general I/O system calls. For that case, we +have a ResponseFile.

+

Finally, there are some cases where we need to break out of the HTTP mode +entirely. Two examples are WebSockets, where we need to upgrade a half-duplex +HTTP connection to a full-duplex connection, and HTTPS proxying, which requires +our proxy server to establish a connection, and then become a dumb data +transport agent. For these cases, we have the ResponseRaw constructor. Note +that not all WAI handlers can in fact support ResponseRaw, though the most +commonly used handler, Warp, does provide this support.

+
+
+

Request Body

+

Like response bodies, we could theoretically use a lazy ByteString for request +bodies, but in practice we want to avoid lazy I/O. Instead, the request body is +represented with a IO ByteString action (ByteString here being a strict +ByteString). Note that this does not return the entire request body, but +rather just the next chunk of data. Once you’ve consumed the entire request +body, further calls to this action will return an empty ByteString.

+

Note that, unlike response bodies, we have no need for using Builders on +the request side, since our purpose is purely for reading.

+

The request body could in theory contain any type of data, but the most common +are URL encoded and multipart form data. The wai-extra package contains +built-in support for parsing these in a memory-efficient manner.

+
+
+
+

Hello World

+

To demonstrate the simplicity of WAI, let’s look at a hello world example. In +this example, we’re going to use the OverloadedStrings language extension to +avoid explicitly packing string values into bytestrings.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.HTTP.Types (status200)
+import Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $
+  responseLBS status200 [("Content-Type", "text/plain")] "Hello World"
+
+main = run 3000 application
+

Lines 2 through 4 perform our imports. Warp is provided by the warp package, +and is the premiere WAI backend. WAI is also built on top of the http-types +package, which provides a number of datatypes and convenience values, including +status200.

+

First we define our application. Since we don’t care about the specific request +parameters, we ignore the first argument to the function, which contains the +request value. The second argument is our "send a response" function, which we +immediately use. The response value we send is built from a lazy ByteString +(thus responseLBS), with status code 200 ("OK"), a text/plain content type, +and a body containing the words "Hello World". Pretty straight-forward.

+
+
+

Resource allocation

+

Let’s make this a little more interesting, and try to allocate a resource for +our response. We’ll create an MVar in our main function to track the number +of requests, and then hold that MVar while sending each response.

+
{-# LANGUAGE OverloadedStrings #-}
+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)
+
+application countRef _ respond = do
+    modifyMVar countRef $ \count -> do
+        let count' = count + 1
+            msg = fromByteString "You are visitor number: " <>
+                  fromShow count'
+        responseReceived <- respond $ responseBuilder
+            status200
+            [("Content-Type", "text/plain")]
+            msg
+        return (count', responseReceived)
+
+main = do
+    visitorCount <- newMVar 0
+    run 3000 $ application visitorCount
+

This is where WAI’s continuation interface shines. We’re able to use the +standard modifyMVar function to acquire the MVar lock and send our +response. Note how we thread the responseReceived value through, though we +never actually use the value for anything. It is merely witness to the fact +that we have, in fact, sent a response.

+

Notice also how we take advantage of Builders in constructing our msg +value. Instead of concatenating two ByteStrings together directly, we +monoidally append two different Builder values. The advantage to this is that +the results will end up being copied directly into the final output buffer, +instead of first being copied into a temporary ByteString buffer to only +later be copied into the final buffer.

+
+
+

Streaming response

+

Let’s give our streaming interface a test as well:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (fromByteString)
+import           Control.Concurrent       (threadDelay)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai
+import           Network.Wai.Handler.Warp (run)
+
+application _ respond = respond $ responseStream status200 [("Content-Type", "text/plain")]
+    $ \send flush -> do
+        send $ fromByteString "Starting the response...\n"
+        flush
+        threadDelay 1000000
+        send $ fromByteString "All done!\n"
+
+main = run 3000 application
+

We use responseStream, and our third argument is a function which takes our +"send a builder" and "flush the buffer" functions. Notice how we flush after +our first chunk of data, to make sure the client sees the data immediately. +However, there’s no need to flush at the end of a response. WAI requires that +the handler automatically flush at the end of a stream.

+
+
+

Middleware

+

In addition to allowing our applications to run on multiple backends without +code changes, the WAI allows us another benefits: middleware. Middleware is +essentially an application transformer, taking one application and returning +another one.

+

Middleware components can be used to provide lots of services: cleaning up +URLs, authentication, caching, JSON-P requests. But perhaps the most useful and +most intuitive middleware is gzip compression. The middleware works very +simply: it parses the request headers to determine if a client supports +compression, and if so compresses the response body and adds the appropriate +response header.

+

The great thing about middlewares is that they are unobtrusive. Let’s see how +we would apply the gzip middleware to our hello world application.

+
{-# LANGUAGE OverloadedStrings #-}
+import Network.Wai
+import Network.Wai.Handler.Warp (run)
+import Network.Wai.Middleware.Gzip (gzip, def)
+import Network.HTTP.Types (status200)
+
+application _ respond = respond $ responseLBS status200 [("Content-Type", "text/plain")]
+                       "Hello World"
+
+main = run 3000 $ gzip def application
+

We added an import line to actually have access to the middleware, and then +simply applied gzip to our application. You can also chain together multiple +middlewares: a line such as gzip False $ jsonp $ othermiddleware $ +myapplication is perfectly valid. One word of warning: the order the +middleware is applied can be important. For example, jsonp needs to work on +uncompressed data, so if you apply it after you apply gzip, you’ll have +trouble.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/widgets.html b/public/book/widgets.html new file mode 100644 index 00000000..a3f8df22 --- /dev/null +++ b/public/book/widgets.html @@ -0,0 +1,635 @@ + Widgets :: Yesod Web Framework Book- Version 1.6 +
+

Widgets

+ + +

One of the challenges in web development is that we have to coordinate three +different client-side technologies: HTML, CSS and Javascript. Worse still, we +have to place these components in different locations on the page: CSS in a +style tag in the head, Javascript in a script tag before the closing body tag, and HTML in the +body. And never mind if you want to put your CSS and Javascript in separate +files!

+

In practice, this works out fairly nicely when building a single page, because +we can separate our structure (HTML), style (CSS) and logic (Javascript). But +when we want to build modular pieces of code that can be easily composed, it +can be a headache to coordinate all three pieces separately. Widgets are +Yesod’s solution to the problem. They also help with the issue of including +libraries, such as jQuery, one time only.

+

Our four template languages- Hamlet, Cassius, Lucius and Julius- provide the +raw tools for constructing your output. Widgets provide the glue that allows +them to work together seamlessly.

+
+

Synopsis

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+instance Yesod App
+
+getHomeR = defaultLayout $ do
+    setTitle "My Page Title"
+    toWidget [lucius| h1 { color: green; } |]
+    addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"
+    toWidget
+        [julius|
+            $(function() {
+                $("h1").click(function(){
+                    alert("You clicked on the heading!");
+                });
+            });
+        |]
+    toWidgetHead
+        [hamlet|
+            <meta name=keywords content="some sample keywords">
+        |]
+    toWidget
+        [hamlet|
+            <h1>Here's one way of including content
+        |]
+    [whamlet|<h2>Here's another |]
+    toWidgetBody
+        [julius|
+            alert("This is included in the body itself");
+        |]
+
+main = warp 3000 App
+

This produces the following HTML (indentation added):

+
<!DOCTYPE html>
+<html>
+  <head>
+    <title>My Page Title</title>
+    <meta name="keywords" content="some sample keywords">
+    <style>h1{color:green}</style>
+  </head>
+  <body>
+    <h1>Here's one way of including content</h1>
+    <h2>Here's another</h2>
+    <script>
+      alert("This is included in the body itself");
+    </script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">
+    </script><script>
+      $(function() {
+        $('h1').click(function() {
+          alert("You clicked on the heading!");
+        });
+      });
+    </script>
+  </body>
+</html>
+
+
+

What’s in a Widget?

+

At a very superficial level, an HTML document is just a bunch of nested tags. +This is the approach most HTML generation tools take: you define hierarchies of +tags and are done with it. But let’s imagine that I want to write a component +of a page for displaying the navbar. I want this to be "plug and play": I call +the function at the right time, and the navbar is inserted at the correct point +in the hierarchy.

+

This is where our superficial HTML generation breaks down. Our navbar likely +consists of some CSS and JavaScript in addition to HTML. By the time we call +the navbar function, we have already rendered the <head> tag, so it is too +late to add a new <style> tag for our CSS declarations. Under normal +strategies, we would need to break up our navbar function into three parts: +HTML, CSS and JavaScript, and make sure that we always call all three pieces.

+

Widgets take a different approach. Instead of viewing an HTML document as a +monolithic tree of tags, widgets see a number of distinct components in the +page. In particular:

+
    +
  • +

    +The title +

    +
  • +
  • +

    +External stylesheets +

    +
  • +
  • +

    +External Javascript +

    +
  • +
  • +

    +CSS declarations +

    +
  • +
  • +

    +Javascript code +

    +
  • +
  • +

    +Arbitrary <head> content +

    +
  • +
  • +

    +Arbitrary <body> content +

    +
  • +
+

Different components have different semantics. For example, there can only be +one title, but there can be multiple external scripts and stylesheets. However, +those external scripts and stylesheets should only be included once. Arbitrary +head and body content, on the other hand, has no limitation (someone may want +to have five lorem ipsum blocks after all).

+

The job of a widget is to hold onto these disparate components and apply proper +logic for combining different widgets together. This consists of things like +taking the last title set and ignoring others, filtering duplicates from the +list of external scripts and stylesheets, and concatenating head and body +content.

+
+
+

Constructing Widgets

+

In order to use widgets, you’ll obviously need to be able to get your hands on +them. The most common way will be via the ToWidget typeclass, and its +toWidget method. This allows you to convert your Shakespearean templates +directly to a Widget: Hamlet code will appear in the body, Julius scripts +inside a <script>, and Cassius and Lucius in a <style> tag.

+ +

But what if you want to add some <meta> tags, which need to appear in +the head? Or if you want some Javascript to appear in the body instead of the +head? For these purposes, Yesod provides two additional type classes: +ToWidgetHead and ToWidgetBody. These work exactly as they seem they should. One example use case for this is to have fine-grained control of where your <script> tags end up getting inserted.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/      HomeR  GET
+|]
+
+instance Yesod App where
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout $ do
+    setTitle "toWidgetHead and toWidgetBody"
+    toWidgetBody
+        [hamlet|<script src=/included-in-body.js>|]
+    toWidgetHead
+        [hamlet|<script src=/included-in-head.js>|]
+
+main :: IO ()
+main = warp 3000 App
+

Note that even though toWidgetHead was called after toWidgetBody, the +latter <script> tag appears first in the generated HTML.

+

In addition, there are a number of other functions for creating specific kinds +of Widgets:

+
+
+setTitle +
+

+Turns some HTML into the page title. +

+
+
+toWidgetMedia +
+

+Works the same as toWidget, but takes an +additional parameter to indicate what kind of media this applies to. Useful for +creating print stylesheets, for instance. +

+
+
+addStylesheet +
+

+Adds a reference, via a <link> tag, to an external +stylesheet. Takes a type-safe URL. +

+
+
+addStylesheetRemote +
+

+Same as addStylesheet, but takes a normal URL. Useful +for referring to files hosted on a CDN, like Google’s jQuery UI CSS files. +

+
+
+addScript +
+

+Adds a reference, via a <script> tag, to an external script. +Takes a type-safe URL. +

+
+
+addScriptRemote +
+

+Same as addScript, but takes a normal URL. Useful for +referring to files hosted on a CDN, like Google’s jQuery. +

+
+
+
+
+

Combining Widgets

+

The whole idea of widgets is to increase composability. You can take these +individual pieces of HTML, CSS and Javascript, combine them together into +something more complicated, and then combine these larger entities into +complete pages. This all works naturally through the Monad instance of +Widget, meaning you can use do-notation to compose pieces together.

+
myWidget1 = do
+    toWidget [hamlet|<h1>My Title|]
+    toWidget [lucius|h1 { color: green } |]
+
+myWidget2 = do
+    setTitle "My Page Title"
+    addScriptRemote "http://www.example.com/script.js"
+
+myWidget = do
+    myWidget1
+    myWidget2
+
+-- or, if you want
+myWidget' = myWidget1 >> myWidget2
+ +
+
+

Generate IDs

+

If we’re really going for true code reuse here, we’re eventually going to run +into name conflicts. Let’s say that there are two helper libraries that both +use the class name “foo” to affect styling. We want to avoid such a +possibility. Therefore, we have the newIdent function. This function +automatically generates a word that is unique for this handler.

+
getRootR = defaultLayout $ do
+    headerClass <- newIdent
+    toWidget [hamlet|<h1 .#{headerClass}>My Header|]
+    toWidget [lucius| .#{headerClass} { color: green; } |]
+
+
+

whamlet

+

Let’s say you’ve got a fairly standard Hamlet template, that embeds another +Hamlet template to represent the footer:

+
page =
+    [hamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+
+footer =
+    [hamlet|
+        <footer>
+            <p>That's all folks!
+    |]
+

That works fine if the footer is plain old HTML, but what if we want to add +some style? Well, we can easily spice up the footer by turning it into a +Widget:

+
footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+

But now we’ve got a problem: a Hamlet template can only embed another Hamlet +template; it knows nothing about a Widget. This is where whamlet comes in. It +takes exactly the same syntax as normal Hamlet, and variable (#{…}) and URL +(@{…}) interpolation are unchanged. But embedding (^{…}) takes a Widget, +and the final result is a Widget. To use it, we can just do:

+
page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

There is also whamletFile, if you would prefer to keep your template in a +separate file.

+ +
+

Types

+

You may have noticed that I’ve been avoiding type signatures so far. The simple +answer is that each widget is a value of type Widget. But if you look through +the Yesod libraries, you’ll find no definition of the Widget type. What +gives?

+

Yesod defines a very similar type: data WidgetT site m a. This data type is a +monad transformer. The last two arguments are the underlying monad and the +monadic value, respectively. The site parameter is the specific foundation +type for your individual application. Since this type varies for each and every +site, it’s impossible for the libraries to define a single Widget datatype +which would work for every application.

+

Instead, the mkYesod Template Haskell function generates this type synonym +for you. Assuming your foundation data type is called MyApp, your Widget +synonym is defined as:

+
type Widget = WidgetT MyApp IO ()
+

We set the monadic value to be (), since a widget’s value will ultimately be +thrown away. IO is the standard base monad, and will be used in almost all +cases. The only exception is when writing a subsite. Subsites are a more +advanced topic, and will be covered later in their own chapter.

+

Once we know about our Widget type synonym, it’s easy to add signatures to +our previous code samples:

+
footer :: Widget
+footer = do
+    toWidget
+        [lucius|
+            footer {
+                font-weight: bold;
+                text-align: center
+            }
+        |]
+    toWidget
+        [hamlet|
+            <footer>
+                <p>That's all folks!
+        |]
+
+page :: Widget
+page =
+    [whamlet|
+        <p>This is my page. I hope you enjoyed it.
+        ^{footer}
+    |]
+

When we start digging into handler functions some more, we’ll encounter a +similar situation with the HandlerT and Handler types.

+
+
+
+

Using Widgets

+

It’s all well and good that we have these beautiful Widget datatypes, but how +exactly do we turn them into something the user can interact with? The most +commonly used function is defaultLayout, which essentially has the type +signature Widget → Handler Html.

+

defaultLayout is actually a typeclass method, which can be overridden for +each application. This is how Yesod apps are themed. So we’re still left with +the question: when we’re inside defaultLayout, how do we unwrap a Widget? +The answer is widgetToPageContent. Let’s look at some (simplified) types:

+
data PageContent url = PageContent
+    { pageTitle :: Html
+    , pageHead :: HtmlUrl url
+    , pageBody :: HtmlUrl url
+    }
+widgetToPageContent :: Widget -> Handler (PageContent url)
+

This is getting closer to what we need. We now have direct access to the HTML +making up the head and body, as well as the title. At this point, we can use +Hamlet to combine them all together into a single document, along with our site +layout, and we use withUrlRenderer to convert that Hamlet result into actual +HTML that’s ready to be shown to the user. The next example demonstrates this +process.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent widget
+    withUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    <style>body { font-family: verdana }
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

There’s still one thing that bothers me: that style tag. There are a few +problems with it:

+
    +
  • +

    +Unlike Lucius or Cassius, it doesn’t get compile-time checked for + correctness. +

    +
  • +
  • +

    +Granted that the current example is very simple, but in something more + complicated we could get into character escaping issues. +

    +
  • +
  • +

    +We’ll now have two style tags instead of one: the one produced by myLayout, + and the one generated in the pageHead based on the styles set in the + widget. +

    +
  • +
+

We have one more trick in our bag to address this: we apply some last-minute +adjustments to the widget itself before calling widgetToPageContent. It’s +actually very easy to do: we just use do-notation again.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Yesod
+
+data App = App
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+myLayout :: Widget -> Handler Html
+myLayout widget = do
+    pc <- widgetToPageContent $ do
+        widget
+        toWidget [lucius| body { font-family: verdana } |]
+    withUrlRenderer
+        [hamlet|
+            $doctype 5
+            <html>
+                <head>
+                    <title>#{pageTitle pc}
+                    <meta charset=utf-8>
+                    ^{pageHead pc}
+                <body>
+                    <article>
+                        ^{pageBody pc}
+        |]
+
+instance Yesod App where
+    defaultLayout = myLayout
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Hello World!
+    |]
+
+main :: IO ()
+main = warp 3000 App
+
+
+

Using handler functions

+

We haven’t covered too much of the handler functionality yet, but once we do, +the question arises: how do we use those functions in a widget? For example, +what if your widget needs to look up a query string parameter using +lookupGetParam?

+

The first answer is the function handlerToWidget, which can convert a +Handler action into a Widget answer. However, in many cases, this won’t be +necessary. Consider the type signature of lookupGetParam:

+
lookupGetParam :: MonadHandler m => Text -> m (Maybe Text)
+

This function will live in any instance of MonadHandler. And conveniently, +Widget is also a MonadHandler instance. This means that most code can be +run in either Handler or Widget. And if you need to explicitly convert from +Handler to Widget, you can always use handlerToWidget.

+ +
+
+

Summary

+

The basic building block of each page is a widget. Individual snippets of HTML, +CSS, and Javascript can be turned into widgets via the polymorphic toWidget +function. Using do-notation, you can combine these individual widgets into +larger widgets, eventually containing all the content of your page.

+

Unwrapping these widgets is usually performed within the defaultLayout +function, which can be used to apply a unified look-and-feel to all your pages.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/wiki-chat-example.html b/public/book/wiki-chat-example.html new file mode 100644 index 00000000..52d1947a --- /dev/null +++ b/public/book/wiki-chat-example.html @@ -0,0 +1,594 @@ + Wiki: markdown, chat subsite, event source :: Yesod Web Framework Book- Version 1.6 +
+

Wiki: markdown, chat subsite, event source

+ + +

This example will tie together a few different ideas. We’ll start with a chat +subsite, which allows us to embed a chat widget on any page. We’ll use the HTML +5 event source API to handle sending events from the server to the client.

+
+

Subsite: data

+

In order to define a subsite, we first need to create a foundation type for the +subsite, the same as we would do for a normal Yesod application. In our case, +we want to keep a channel of all the events to be sent to the individual +participants of a chat. This ends up looking like:

+
-- @Chat/Data.hs
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+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
+import           Yesod
+import           Yesod.Core.Types (SubHandlerFor)
+
+-- | Our subsite foundation. We keep a channel of events that all connections
+-- will share.
+data Chat = Chat (Chan ServerEvent)
+

We also need to define our subsite routes in the same module. We need to have +two commands: one to send a new message to all users, and another to receive +the stream of messages.

+
-- @Chat/Data.hs
+mkYesodSubData "Chat" [parseRoutes|
+/send SendR POST
+/recv ReceiveR GET
+|]
+
+
+

Subsite: handlers

+

Now that we’ve defined our foundation and routes, we need to create a separate +module for providing the subsite dispatch functionality. We’ll call this +module Chat, and it’s where we’ll start to see how a subsite functions.

+

A subsite always sits as a layer on top of some master site, which will be +provided by the user. In many cases, a subsite will require specific +functionality to be present in the master site. In the case of our chat +subsite, we want user authentication to be provided by the master site. The +subsite needs to be able to query whether the current user is logged into the +site, and to get the user’s name.

+

The way we represent this concept is to define a typeclass that encapsulates +the necessary functionality. Let’s have a look at our YesodChat typeclass:

+
-- @Chat/Data.hs
+class (Yesod master, RenderMessage master FormMessage)
+        => YesodChat master where
+    getUserName :: HandlerFor master Text
+    isLoggedIn :: HandlerFor master Bool
+

Any master site which wants to use the chat subsite will need to provide a +YesodChat instance. (We’ll see in a bit how this requirement is enforced.) +There are a few interesting things to note:

+
    +
  • +

    +We can put further constraints on the master site, such as providing a + Yesod instance and allowing rendering of form messages. The former allows + us to use defaultLayout, while the latter allows us to use standard form + widgets. +

    +
  • +
  • +

    +Previously in the book, we’ve used the Handler monad quite a bit. Remember + that Handler is just an application-specific type synonym around + HandlerFor. Since this code is intended to work with many different + applications, we use the full HandlerFor form of the transformer. +

    +
  • +
+

Speaking of the Handler type synonym, we’re going to want to have +something similar for our subsite. The question is: what does this +monad look like? In a subsite situation, we use SubHandlerFor with +both the subsite data type and the master site type. We’ll define a +helper synonym for this which requires a YesodChat instance on the +master site type, so we end up with:

+
-- @Chat/Data.hs
+type ChatHandler a =
+    forall master. YesodChat master =>
+    SubHandlerFor Chat master a
+

Now that we have our machinery out of the way, it’s time to write our subsite +handler functions. We had two routes: one for sending messages, and one for +receiving messages. Let’s start with sending. We need to:

+
    +
  1. +

    +Get the username for the person sending the message. +

    +
  2. +
  3. +

    +Parse the message from the incoming parameters. (Note that we’re going to use GET parameters for simplicity of the client-side Ajax code.) +

    +
  4. +
  5. +

    +Write the message to the Chan. +

    +
  6. +
+

The trickiest bit of all this code is to know when to use lift. Let’s look at +the implementation, and then discuss those lift usages:

+
-- @Chat/Data.hs
+postSendR :: ChatHandler ()
+postSendR = do
+    from <- liftHandler getUserName
+    body <- runInputGet $ ireq textField "message"
+    Chat chan <- getSubYesod
+    liftIO $ writeChan chan $ ServerEvent Nothing Nothing $ return $
+        fromText from <> fromText ": " <> fromText body
+

getUserName is the function we defined in our YesodChat typeclass earlier. +If we look at that type signature, we see that it lives in the master site’s +Handler monad. Therefore, we need to lift that call out of the subsite.

+

The next call to getSubYesod is not lifted. The reasoning here is simple: +we want to get the subsite’s foundation type in order to access the message +channel. If we instead lifted that call, we’d get the master site’s +foundation type instead, which is not what we want in this case.

+

The final line puts the new message into the channel. Since this is an IO +action, we use liftIO. ServerEvent is part of the wai-eventsource +package, and is the means by which we’re providing server-sent events in this +example.

+

The receiving side is similarly simple:

+
-- @Chat/Data.hs
+getReceiveR :: ChatHandler ()
+getReceiveR = do
+    Chat chan <- getSubYesod
+    sendWaiApplication $ eventSourceAppChan chan
+

The last line in our function exposes the underlying wai-eventsource +application as a Yesod handler, using the sendWaiApplication function to +promote a WAI application to a Yesod handler. eventSourceAppChan duplicates +the chan under the hood, which is a standard method in concurrent Haskel +of creating broadcast channels.

+

Now that we’ve defined our handler functions, we can set up our dispatch. In a +normal application, dispatching is handled by calling mkYesod, which creates +the appropriate YesodDispatch instance. In subsites, things are a little bit +more complicated, since you’ll often want to place constraints on the master +site. The formula we use is the following:

+
-- @Chat.hs
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+module Chat where
+
+import           Chat.Data
+import           Yesod
+
+instance YesodChat master => YesodSubDispatch Chat master where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesChat)
+

We’re stating that our Chat subsite can live on top of any master site which +is an instance of YesodChat. We then use the mkYesodSubDispatch Template +Haskell function to generate all of our dispatching logic. While this is a bit +more difficult to write than mkYesod, it provides necessary flexibility, and +is mostly identical for any subsite you’ll write.

+
+
+

Subsite: widget

+

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.

+

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 +URLs. Let’s start off our widget:

+
-- @Chat.hs
+chatWidget :: YesodChat master
+           => (Route Chat -> Route master)
+           -> WidgetFor master ()
+chatWidget toMaster = do
+

Next, we’re going to generate some identifiers to be used by our widget. It’s +always good practice to let Yesod generate unique identifiers for you instead +of creating them manually to avoid name collisions.

+
-- @Chat.hs
+    chat <- newIdent   -- the containing div
+    output <- newIdent -- the box containing the messages
+    input <- newIdent  -- input field from the user
+

And next we need to check if the user is logged in, using the isLoggedIn +function in our YesodChat typeclass. Since we’re in a Widget and that +function lives in the Handler monad, we need to use handlerToWidget:

+
-- @Chat.hs
+    ili <- handlerToWidget isLoggedIn  -- check if we're already logged in
+

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 +client-side code wrapped in a Widget:

+
-- @Chat.hs
+    if ili
+        then do
+            -- Logged in: show the widget
+            [whamlet|
+                <div ##{chat}>
+                    <h2>Chat
+                    <div ##{output}>
+                    <input ##{input} type=text placeholder="Enter Message">
+            |]
+            -- Just some CSS
+            toWidget [lucius|
+                ##{chat} {
+                    position: absolute;
+                    top: 2em;
+                    right: 2em;
+                }
+                ##{output} {
+                    width: 200px;
+                    height: 300px;
+                    border: 1px solid #999;
+                    overflow: auto;
+                }
+            |]
+            -- And now that Javascript
+            toWidgetBody [julius|
+                // Set up the receiving end
+                var output = document.getElementById(#{toJSON output});
+                var src = new EventSource("@{toMaster ReceiveR}");
+                src.onmessage = function(msg) {
+                    // This function will be called for each new message.
+                    var p = document.createElement("p");
+                    p.appendChild(document.createTextNode(msg.data));
+                    output.appendChild(p);
+
+                    // And now scroll down within the output div so the most recent message
+                    // is displayed.
+                    output.scrollTop = output.scrollHeight;
+                };
+
+                // Set up the sending end: send a message via Ajax whenever the user hits
+                // enter.
+                var input = document.getElementById(#{toJSON input});
+                input.onkeyup = function(event) {
+                    var keycode = (event.keyCode ? event.keyCode : event.which);
+                    if (keycode == '13') {
+                        var xhr = new XMLHttpRequest();
+                        var val = input.value;
+                        input.value = "";
+                        var params = "?message=" + encodeURI(val);
+                        xhr.open("POST", "@{toMaster SendR}" + params);
+                        xhr.send(null);
+                    }
+                }
+            |]
+

And finally, if the user isn’t logged in, we’ll ask them to log in to use the +chat app.

+
-- @Chat.hs
+        else do
+            -- User isn't logged in, give a not-logged-in message.
+            master <- getYesod
+            [whamlet|
+                <p>
+                    You must be #
+                    $maybe ar <- authRoute master
+                        <a href=@{ar}>logged in
+                    $nothing
+                        logged in
+                    \ to chat.
+            |]
+
+
+

Master site: data

+

Now we can proceed with writing our main application. This application will +include the chat subsite and a wiki. The first thing we need to consider is how +to store the wiki contents. Normally, we’d want to put this in some kind of a +Persistent database. For simplicity, we’ll just use an in-memory +representation. Each Wiki page is indicated by a list of names, and the contents of each page is going to be a piece of Text. So our full foundation datatype is:

+
-- @ChatMain.hs
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE ViewPatterns          #-}
+module ChatMain where
+
+import           Chat
+import           Chat.Data
+import           Control.Concurrent.Chan (newChan)
+import           Data.IORef
+import           Data.Map                (Map)
+import qualified Data.Map                as Map
+import           Data.Text               (Text)
+import qualified Data.Text.Lazy          as TL
+import           Text.Markdown
+import           Yesod
+import           Yesod.Auth
+import           Yesod.Auth.Dummy
+import           System.SetEnv
+
+data App = App
+    { getChat     :: Chat
+    , wikiContent :: IORef (Map [Text] Text)
+    }
+

Next we want to set up our routes:

+
-- @ChatMain.hs
+mkYesod "App" [parseRoutes|
+/            HomeR GET      -- the homepage
+/wiki/*Texts WikiR GET POST -- note the multipiece for the wiki hierarchy
+
+/chat        ChatR Chat getChat    -- the chat subsite
+/auth        AuthR Auth getAuth    -- the auth subsite
+|]
+
+
+

Master site: instances

+

We need to make two modifications to the default Yesod instance. Firstly, we +want to provide an implementation of authRoute, so that our chat subsite +widget can provide a proper link to a login page. Secondly, we’ll provide a +override to the defaultLayout. Besides providing login/logout links, this +function will add in the chat widget on every page.

+
-- @ChatMain.hs
+instance Yesod App where
+    authRoute _ = Just $ AuthR LoginR -- get a working login link
+
+    -- Our custom defaultLayout will add the chat widget to every page.
+    -- We'll also add login and logout links to the top.
+    defaultLayout widget = do
+        pc <- widgetToPageContent $ do
+            widget
+            chatWidget ChatR
+        mmsg <- getMessage
+        withUrlRenderer
+            [hamlet|
+                $doctype 5
+                <html>
+                    <head>
+                        <title>#{pageTitle pc}
+                        ^{pageHead pc}
+                    <body>
+                        $maybe msg <- mmsg
+                            <div .message>#{msg}
+                        <nav>
+                            <a href=@{AuthR LoginR}>Login
+                            \ | #
+                            <a href=@{AuthR LogoutR}>Logout
+                        ^{pageBody pc}
+            |]
+

Since we’re using the chat subsite, we have to provide an instance of +YesodChat.

+
-- @ChatMain.hs
+instance YesodChat App where
+    getUserName = do
+        muid <- maybeAuthId
+        case muid of
+            Nothing -> do
+                setMessage "Not logged in"
+                redirect $ AuthR LoginR
+            Just uid -> return uid
+    isLoggedIn = do
+        ma <- maybeAuthId
+        return $ maybe False (const True) ma
+

Our YesodAuth and RenderMessage instances, as well as the homepage handler, +are rather bland:

+
-- @ChatMain.hs
+-- Fairly standard YesodAuth instance. We'll use the dummy plugin so that you
+-- can create any name you want, and store the login name as the AuthId.
+instance YesodAuth App where
+    type AuthId App = Text
+    authPlugins _ = [authDummy]
+    loginDest _ = HomeR
+    logoutDest _ = HomeR
+    getAuthId = return . Just . credsIdent
+    maybeAuthId = lookupSession "_ID"
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+-- Nothing special here, just giving a link to the root of the wiki.
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>Welcome to the Wiki!
+        <p>
+            <a href=@{wikiRoot}>Wiki root
+    |]
+  where
+    wikiRoot = WikiR []
+
+
+

Master site: wiki handlers

+

Now it’s time to write our wiki handlers: a GET for displaying a page, and a +POST for updating a page. We’ll also define a wikiForm function to be used on +both handlers:

+
-- @ChatMain.hs
+-- A form for getting wiki content
+wikiForm :: Maybe Textarea -> Html -> MForm Handler (FormResult Textarea, Widget)
+wikiForm mtext = renderDivs $ areq textareaField "Page body" mtext
+
+-- Show a wiki page and an edit form
+getWikiR :: [Text] -> Handler Html
+getWikiR page = do
+    -- Get the reference to the contents map
+    icontent <- fmap wikiContent getYesod
+
+    -- And read the map from inside the reference
+    content <- liftIO $ readIORef icontent
+
+    -- Lookup the contents of the current page, if available
+    let mtext = Map.lookup page content
+
+    -- Generate a form with the current contents as the default value.
+    -- Note that we use the Textarea wrapper to get a <textarea>.
+    (form, _) <- generateFormPost $ wikiForm $ fmap Textarea mtext
+    defaultLayout $ do
+        case mtext of
+            -- We're treating the input as markdown. The markdown package
+            -- automatically handles XSS protection for us.
+            Just text -> toWidget $ markdown def $ TL.fromStrict text
+            Nothing -> [whamlet|<p>Page does not yet exist|]
+        [whamlet|
+            <h2>Edit page
+            <form method=post>
+                ^{form}
+                <div>
+                    <input type=submit>
+        |]
+
+-- Get a submitted wiki page and updated the contents.
+postWikiR :: [Text] -> Handler Html
+postWikiR page = do
+    icontent <- fmap wikiContent getYesod
+    content <- liftIO $ readIORef icontent
+    let mtext = Map.lookup page content
+    ((res, form), _) <- runFormPost $ wikiForm $ fmap Textarea mtext
+    case res of
+        FormSuccess (Textarea t) -> do
+            liftIO $ atomicModifyIORef icontent $
+                \m -> (Map.insert page t m, ())
+            setMessage "Page updated"
+            redirect $ WikiR page
+        _ -> defaultLayout
+                [whamlet|
+                    <form method=post>
+                        ^{form}
+                        <div>
+                            <input type=submit>
+                |]
+
+
+

Master site: running

+

Finally, we’re ready to run our application. Unlike many of our previous +examples in this book, we need to perform some real initialization in the +main function. The Chat subsite requires an empty Chan to be created, and +we need to create a mutable variable to hold the wiki contents. Once we have +those values, we can create an App value and pass it to the warp function.

+
-- @ChatMain.hs
+main :: IO ()
+main = do
+    -- Create our server event channel
+    chan <- newChan
+
+    -- Initially have a blank database of wiki pages
+    icontent <- newIORef Map.empty
+
+    -- Set web server's listening port required by warpEnv function
+    -- This env var is set up automatically if 'yesod devel' is used
+    setEnv "PORT" "3000"
+
+    -- Run our app
+    warpEnv App
+        { getChat = Chat chan
+        , wikiContent = icontent
+        }
+
+
+

Conclusion

+

This example demonstrated creation of a non-trivial subsite. Some important +points to notice were the usage of typeclasses to express constraints on the +master site, how data initialization was performed in the main function, and +how lifting allowed us to operate in either the subsite or master site +context.

+

If you’re looking for a way to test out your subsite skills, I’d recommend +modifying this example so that the Wiki code also lived in its own subsite.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/xml.html b/public/book/xml.html new file mode 100644 index 00000000..87726962 --- /dev/null +++ b/public/book/xml.html @@ -0,0 +1,833 @@ + xml-conduit :: Yesod Web Framework Book- Version 1.6 +
+

xml-conduit

+ + +

Many developers cringe at the thought of dealing with XML files. XML has the +reputation of having a complicated data model, with obfuscated libraries and +huge layers of complexity sitting between you and your goal. I’d like to posit +that a lot of that pain is actually a language and library issue, not inherent +to XML.

+

Once again, Haskell’s type system allows us to easily break down the problem to +its most basic form. The xml-types package neatly deconstructs the XML data +model (both a streaming and DOM-based approach) into some simple ADTs. +Haskell’s standard immutable data structures make it easier to apply transforms +to documents, and a simple set of functions makes parsing and rendering a +breeze.

+

We’re going to be covering the xml-conduit package. Under the surface, this +package uses a lot of the approaches Yesod in general does for high +performance: blaze-builder, text, conduit and attoparsec. But from a user +perspective, it provides everything from the simplest APIs +(readFile/writeFile) through full control of XML event streams.

+

In addition to xml-conduit, there are a few related packages that come into +play, like xml-hamlet and xml2html. We’ll cover both how to use all these +packages, and when they should be used.

+
+

Synopsis

+
<!-- Input XML file -->
+<document title="My Title">
+    <para>This is a paragraph. It has <em>emphasized</em> and <strong>strong</strong> words.</para>
+    <image href="myimage.png"/>
+</document>
+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import qualified Data.Map        as M
+import           Prelude         hiding (readFile, writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = do
+    -- readFile will throw any parse errors as runtime exceptions
+    -- def uses the default settings
+    Document prologue root epilogue <- readFile def "input.xml"
+
+    -- root is the root element of the document, let's modify it
+    let root' = transform root
+
+    -- And now we write out. Let's indent our output
+    writeFile def
+        { rsPretty = True
+        } "output.html" $ Document prologue root' epilogue
+
+-- We'll turn out <document> into an XHTML document
+transform :: Element -> Element
+transform (Element _name attrs children) = Element "html" M.empty
+    [xml|
+        <head>
+            <title>
+                $maybe title <- M.lookup "title" attrs
+                    \#{title}
+                $nothing
+                    Untitled Document
+        <body>
+            $forall child <- children
+                ^{goNode child}
+    |]
+
+goNode :: Node -> [Node]
+goNode (NodeElement e) = [NodeElement $ goElem e]
+goNode (NodeContent t) = [NodeContent t]
+goNode (NodeComment _) = [] -- hide comments
+goNode (NodeInstruction _) = [] -- and hide processing instructions too
+
+-- convert each source element to its XHTML equivalent
+goElem :: Element -> Element
+goElem (Element "para" attrs children) =
+    Element "p" attrs $ concatMap goNode children
+goElem (Element "em" attrs children) =
+    Element "i" attrs $ concatMap goNode children
+goElem (Element "strong" attrs children) =
+    Element "b" attrs $ concatMap goNode children
+goElem (Element "image" attrs _children) =
+    Element "img" (fixAttr attrs) [] -- images can't have children
+  where
+    fixAttr mattrs
+        | "href" `M.member` mattrs  = M.delete "href" $ M.insert "src" (mattrs M.! "href") mattrs
+        | otherwise                 = mattrs
+goElem (Element name attrs children) =
+    -- don't know what to do, just pass it through...
+    Element name attrs $ concatMap goNode children
+
<?xml version="1.0" encoding="UTF-8"?>
+<!-- Output XHTML -->
+<html>
+    <head>
+        <title>
+            My Title
+        </title>
+    </head>
+    <body>
+        <p>
+            This is a paragraph. It has
+            <i>
+                emphasized
+            </i>
+            and
+            <b>
+                strong
+            </b>
+            words.
+        </p>
+        <img src="myimage.png"/>
+    </body>
+</html>
+
+
+

Types

+

Let’s take a bottom-up approach to analyzing types. This section will also +serve as a primer on the XML data model itself, so don’t worry if you’re not +completely familiar with it.

+

I think the first place where Haskell really shows its strength is with the +Name datatype. Many languages (like Java) struggle with properly expressing +names. The issue is that there are in fact three components to a name: its +local name, its namespace (optional), and its prefix (also optional). Let’s +look at some XML to explain:

+
<no-namespace/>
+<no-prefix xmlns="first-namespace" first-attr="value1"/>
+<foo:with-prefix xmlns:foo="second-namespace" foo:second-attr="value2"/>
+

The first tag has a local name of no-namespace, and no namespace or prefix. +The second tag (local name: no-prefix) also has no prefix, but it does have +a namespace (first-namespace). first-attr, however, does not inherit that +namespace: attribute namespaces must always be explicitly set with a prefix.

+ +

The third tag has a local name of with-prefix, a prefix of foo and a +namespace of second-namespace. Its attribute has a second-attr local name +and the same prefix and namespace. The xmlns and xmlns:foo attributes are +part of the namespace specification, and are not considered attributes of their +respective elements.

+

So let’s review what we need from a name: every name has a local name, and it +can optionally have a prefix and namespace. Seems like a simple fit for a +record type:

+
data Name = Name
+    { nameLocalName :: Text
+    , nameNamespace :: Maybe Text
+    , namePrefix    :: Maybe Text
+    }
+

According the XML namespace standard, two names are considered equivalent +if they have the same localname and namespace. In other words, the prefix is +not important. Therefore, xml-types defines Eq and Ord instances that +ignore the prefix.

+

The last class instance worth mentioning is IsString. It would be very +tedious to have to manually type out Name "p" Nothing Nothing every time we +want a paragraph. If you turn on OverloadedStrings, "p" will resolve to +that all by itself! In addition, the IsString instance recognizes something +called Clark notation, which allows you to prefix the namespace surrounded in +curly brackets. In other words:

+
"{namespace}element" == Name "element" (Just "namespace") Nothing
+"element" == Name "element" Nothing Nothing
+
+

The Four Types of Nodes

+

XML documents are a tree of nested nodes. There are in fact four different +types of nodes allowed: elements, content (i.e., text), comments, and +processing instructions.

+ +

Since processing instructions have two pieces of text associated with them (the +target and the data), we have a simple data type:

+
data Instruction = Instruction
+    { instructionTarget :: Text
+    , instructionData :: Text
+    }
+

Comments have no special datatype, since they are just text. But content is an +interesting one: it could contain either plain text or unresolved entities +(e.g., &copyright-statement;). xml-types keeps those unresolved entities +in all the data types in order to completely match the spec. However, in +practice, it can be very tedious to program against those data types. And in +most use cases, an unresolved entity is going to end up as an error anyway.

+

Therefore, the Text.XML module defines its own set of datatypes for nodes, +elements and documents that removes all unresolved entities. If you need to +deal with unresolved entities instead, you should use the Text.XML.Unresolved +module. From now on, we’ll be focusing only on the Text.XML data types, +though they are almost identical to the xml-types versions.

+

Anyway, after that detour: content is just a piece of text, and therefore it +too does not have a special datatype. The last node type is an element, which +contains three pieces of information: a name, a map of attribute name/value +pairs, and a list of children nodes. (In xml-types, this value could contain +unresolved entities as well.) So our Element is defined as:

+
data Element = Element
+    { elementName :: Name
+    , elementAttributes :: Map Name Text
+    , elementNodes :: [Node]
+    }
+

Which of course begs the question: what does a Node look like? This is where +Haskell really shines: its sum types model the XML data model perfectly.

+
data Node
+    = NodeElement Element
+    | NodeInstruction Instruction
+    | NodeContent Text
+    | NodeComment Text
+
+
+

Documents

+

So now we have elements and nodes, but what about an entire document? Let’s +just lay out the datatypes:

+
data Document = Document
+    { documentPrologue :: Prologue
+    , documentRoot :: Element
+    , documentEpilogue :: [Miscellaneous]
+    }
+
+data Prologue = Prologue
+    { prologueBefore :: [Miscellaneous]
+    , prologueDoctype :: Maybe Doctype
+    , prologueAfter :: [Miscellaneous]
+    }
+
+data Miscellaneous
+    = MiscInstruction Instruction
+    | MiscComment Text
+
+data Doctype = Doctype
+    { doctypeName :: Text
+    , doctypeID :: Maybe ExternalID
+    }
+
+data ExternalID
+    = SystemID Text
+    | PublicID Text Text
+

The XML spec says that a document has a single root element (documentRoot). +It also has an optional doctype statement. Before and after both the doctype +and the root element, you are allowed to have comments and processing +instructions. (You can also have whitespace, but that is ignored in the +parsing.)

+

So what’s up with the doctype? Well, it specifies the root element of the +document, and then optional public and system identifiers. These are used to +refer to DTD files, which give more information about the file (e.g., +validation rules, default attributes, entity resolution). Let’s see some +examples:

+
<!DOCTYPE root> <!-- no external identifier -->
+<!DOCTYPE root SYSTEM "root.dtd"> <!-- a system identifier -->
+<!DOCTYPE root PUBLIC "My Root Public Identifier" "root.dtd"> <!-- public identifiers have a system ID as well -->
+

And that, my friends, is the entire XML data model. For many parsing purposes, +you’ll be able to simply ignore the entire Document datatype and go +immediately to the documentRoot.

+
+
+

Events

+

In addition to the document API, xml-types defines an Event datatype. This +can be used for constructing streaming tools, which can be much more memory +efficient for certain kinds of processing (eg, adding an extra attribute to all +elements). We will not be covering the streaming API currently, though it +should look very familiar after analyzing the document API.

+ +
+
+
+

Text.XML

+

The recommended entry point to xml-conduit is the Text.XML module. This module +exports all of the datatypes you’ll need to manipulate XML in a DOM fashion, as +well as a number of different approaches for parsing and rendering XML content. +Let’s start with the simple ones:

+
readFile  :: ParseSettings  -> FilePath -> IO Document
+writeFile :: RenderSettings -> FilePath -> Document -> IO ()
+

This introduces the ParseSettings and RenderSettings datatypes. You can use +these to modify the behavior of the parser and renderer, such as adding +character entities and turning on pretty (i.e., indented) output. Both these +types are instances of the Default typeclass, so you can simply use def when +these need to be supplied. That is how we will supply these values through the +rest of the chapter; please see the API docs for more information.

+

It’s worth pointing out that in addition to the file-based API, there is also a +text- and bytestring-based API. The bytestring-powered functions all perform +intelligent encoding detections, and support UTF-8, UTF-16 and UTF-32, in +either big or little endian, with and without a Byte-Order Marker (BOM). All +output is generated in UTF-8.

+

For complex data lookups, we recommend using the higher-level cursors API. The +standard Text.XML API not only forms the basis for that higher level, but is +also a great API for simple XML transformations and for XML generation. See the +synopsis for an example.

+
+

A note about file paths

+

In the type signature above, we have a type FilePath. However, this isn’t +Prelude.FilePath. The standard Prelude defines a type synonym type FilePath += [Char]. Unfortunately, there are many limitations to using such an +approach, including confusion of filename character encodings and differences +in path separators.

+

Instead, xml-conduit uses the system-filepath package, which defines an +abstract FilePath type. I’ve personally found this to be a much nicer +approach to work with. The package is fairly easy to follow, so I won’t go into +details here. But I do want to give a few quick explanations of how to use it:

+
    +
  • +

    +Since a FilePath is an instance of IsString, you can type in regular + strings and they will be treated properly, as long as the OverloadedStrings + extension is enabled. (I highly recommend enabling it anyway, as it makes + dealing with Text values much more pleasant.) +

    +
  • +
  • +

    +If you need to explicitly convert to or from Prelude's FilePath, you + should use the encodeString and decodeString, respectively. This takes into + account file path encodings. +

    +
  • +
  • +

    +Instead of manually splicing together directory names and file names with + extensions, use the operators in the Filesystem.Path.CurrentOS module, e.g. + myfolder </> filename <.> extension. +

    +
  • +
+
+
+
+

Cursor

+

Suppose you want to pull the title out of an XHTML document. You could do so +with the Text.XML interface we just described, using standard pattern +matching on the children of elements. But that would get very tedious, very +quickly. Probably the gold standard for these kinds of lookups is XPath, where +you would be able to write /html/head/title. And that’s exactly what inspired +the design of the Text.XML.Cursor combinators.

+

A cursor is an XML node that knows its location in the tree; it’s able to +traverse upwards, sideways, and downwards. (Under the surface, this is achieved +by tying the knot.) +There are two functions available for creating cursors from Text.XML types: +fromDocument and fromNode.

+

We also have the concept of an Axis, defined as type Axis = Cursor -> +[Cursor]. It’s easiest to get started by looking at example axes: child +returns zero or more cursors that are the child of the current one, parent +returns the single parent cursor of the input, or an empty list if the input is +the root element, and so on.

+

In addition, there are some axes that take predicates. element is a commonly +used function that filters down to only elements which match the given name. +For example, element "title" will return the input element if its name is +"title", or an empty list otherwise.

+

Another common function which isn’t quite an axis is content :: Cursor +-> [Text]. For all content nodes, it returns the contained text; +otherwise, it returns an empty list.

+

And thanks to the monad instance for lists, it’s easy to string all of these +together. For example, to do our title lookup, we would write the following +program:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+            child cursor >>= element "head" >>= child
+                         >>= element "title" >>= descendant >>= content
+

What this says is:

+
    +
  1. +

    +Get me all the child nodes of the root element +

    +
  2. +
  3. +

    +Filter down to only the elements named "head" +

    +
  4. +
  5. +

    +Get all the children of all those head elements +

    +
  6. +
  7. +

    +Filter down to only the elements named "title" +

    +
  8. +
  9. +

    +Get all the descendants of all those title elements. (A descendant is a + child, or a descendant of a child. Yes, that was a recursive definition.) +

    +
  10. +
  11. +

    +Get only the text nodes. +

    +
  12. +
+

So for the input document:

+
<html>
+    <head>
+        <title>My <b>Title</b></title>
+    </head>
+    <body>
+        <p>Foo bar baz</p>
+    </body>
+</html>
+

We end up with the output My Title. This is all well and good, but it’s much +more verbose than the XPath solution. To combat this verbosity, Aristid +Breitkreuz added a set of operators to the Cursor module to handle many common +cases. So we can rewrite our example as:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $/ element "head" &/ element "title" &// content
+

$/ says to apply the axis on the right to the children of the cursor on the +left. &/ is almost identical, but is instead used to combine two axes +together. This is a general rule in Text.XML.Cursor: operators beginning with +$ directly apply an axis, while & will combine two together. &// is +used for applying an axis to all descendants.

+

Let’s go for a more complex, if more contrived, example. We have a document +that looks like:

+
<html>
+    <head>
+        <title>Headings</title>
+    </head>
+    <body>
+        <hgroup>
+            <h1>Heading 1 foo</h1>
+            <h2 class="foo">Heading 2 foo</h2>
+        </hgroup>
+        <hgroup>
+            <h1>Heading 1 bar</h1>
+            <h2 class="bar">Heading 2 bar</h2>
+        </hgroup>
+    </body>
+</html>
+

We want to get the content of all the h1 tags which precede an h2 tag with +a class attribute of "bar". To perform this convoluted lookup, we can write:

+
{-# LANGUAGE OverloadedStrings #-}
+import Prelude hiding (readFile)
+import Text.XML
+import Text.XML.Cursor
+import qualified Data.Text as T
+
+main :: IO ()
+main = do
+    doc <- readFile def "test2.xml"
+    let cursor = fromDocument doc
+    print $ T.concat $
+        cursor $// element "h2"
+               >=> attributeIs "class" "bar"
+               >=> precedingSibling
+               >=> element "h1"
+               &// content
+

Let’s step through that. First we get all h2 elements in the document. ($// +gets all descendants of the root element.) Then we filter out only those with +class=bar. That >=> operator is actually the standard operator from +Control.Monad; yet another advantage of the monad instance of lists. +precedingSibling finds all nodes that come before our node and share the +same parent. (There is also a preceding axis which takes all elements earlier +in the tree.) We then take just the h1 elements, and then grab their content.

+ +

While the cursor API isn’t quite as succinct as XPath, it has the advantages of +being standard Haskell code, and of type safety.

+
+
+

xml-hamlet

+

Thanks to the simplicity of Haskell’s data type system, creating XML content +with the Text.XML API is easy, if a bit verbose. The following code:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Data.Map (empty)
+import           Prelude  hiding (writeFile)
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty
+        [ NodeElement $ Element "head" empty
+            [ NodeElement $ Element "title" empty
+                [ NodeContent "My "
+                , NodeElement $ Element "b" empty
+                    [ NodeContent "Title"
+                    ]
+                ]
+            ]
+        , NodeElement $ Element "body" empty
+            [ NodeElement $ Element "p" empty
+                [ NodeContent "foo bar baz"
+                ]
+            ]
+        ]
+

produces

+
<?xml version="1.0" encoding="UTF-8"?>
+<html><head><title>My <b>Title</b></title></head><body><p>foo bar baz</p></body></html>
+

This is leaps and bounds easier than having to deal with an imperative, +mutable-value-based API (cough, Java, cough), but it’s far from pleasant, and +obscures what we’re really trying to achieve. To simplify things, we have the +xml-hamlet package, which using Quasi-Quotation to allow you to type in your +XML in a natural syntax. For example, the above could be rewritten as:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map        (empty)
+import           Prelude         hiding (writeFile)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main =
+    writeFile def "test3.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>
+        My #
+        <b>Title
+<body>
+    <p>foo bar baz
+|]
+

Let’s make a few points:

+
    +
  • +

    +The syntax is almost identical to normal Hamlet, except URL-interpolation + (@{…}) has been removed. As such: +

    +
      +
    • +

      +No close tags. +

      +
    • +
    • +

      +Whitespace-sensitive. +

      +
    • +
    • +

      +If you want to have whitespace at the end of a line, use a # at the end. At + the beginning, use a backslash. +

      +
    • +
    +
  • +
  • +

    +An xml interpolation will return a list of Nodes. So you still need to + wrap up the output in all the normal Document and root Element + constructs. +

    +
  • +
  • +

    +There is no support for the special .class and #id attribute forms. +

    +
  • +
+

And like normal Hamlet, you can use variable interpolation and control +structures. So a slightly more complex example would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+import Text.XML
+import Text.Hamlet.XML
+import Prelude hiding (writeFile)
+import Data.Text (Text, pack)
+import Data.Map (empty)
+
+data Person = Person
+    { personName :: Text
+    , personAge :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Michael" 26
+    , Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Gavriella" 1
+    ]
+
+main :: IO ()
+main =
+    writeFile def "people.xml" $ Document (Prologue [] Nothing []) root []
+  where
+    root = Element "html" empty [xml|
+<head>
+    <title>Some People
+<body>
+    <h1>Some People
+    $if null people
+        <p>There are no people.
+    $else
+        <dl>
+            $forall person <- people
+                ^{personNodes person}
+|]
+
+personNodes :: Person -> [Node]
+personNodes person = [xml|
+<dt>#{personName person}
+<dd>#{pack $ show $ personAge person}
+|]
+

A few more notes:

+
    +
  • +

    +The caret-interpolation (^{…}) takes a list of nodes, and so can easily + embed other xml-quotations. +

    +
  • +
  • +

    +Unlike Hamlet, hash-interpolations (#{…}) are not polymorphic, and can + only accept Text values. +

    +
  • +
+
+
+

xml2html

+

So far in this chapter, our examples have revolved around XHTML. I’ve done that +so far simply because it is likely to be the most familiar form of XML for most +of our readers. But there’s an ugly side to all this that we must acknowledge: +not all XHTML will be correct HTML. The following discrepancies exist:

+
    +
  • +

    +There are some void tags (e.g., img, br) in HTML which do not need to + have close tags, and in fact are not allowed to. +

    +
  • +
  • +

    +HTML does not understand self-closing tags, so + <script></script> and <script/> mean very different + things. +

    +
  • +
  • +

    +Combining the previous two points: you are free to self-close void tags, + though to a browser it won’t mean anything. +

    +
  • +
  • +

    +In order to avoid quirks mode, you should start your HTML documents with a + DOCTYPE statement. +

    +
  • +
  • +

    +We do not want the XML declaration <?xml …?> at the top of an HTML + page. +

    +
  • +
  • +

    +We do not want any namespaces used in HTML, while XHTML is fully namespaced. +

    +
  • +
  • +

    +The contents of <style> and <script> tags should not be + escaped. +

    +
  • +
+

Fortunately, xml-conduit provides ToHtml instances for Nodes, +Documents, and Elements which respect these discrepancies. So by just +using toHtml, we can get the correct output.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+import           Data.Map                        (empty)
+import           Text.Blaze.Html                 (toHtml)
+import           Text.Blaze.Html.Renderer.String (renderHtml)
+import           Text.Hamlet.XML
+import           Text.XML
+
+main :: IO ()
+main = putStr $ renderHtml $ toHtml $ Document (Prologue [] Nothing []) root []
+
+root :: Element
+root = Element "html" empty [xml|
+<head>
+    <title>Test
+    <script>if (5 < 6 || 8 > 9) alert("Hello World!");
+    <style>body > h1 { color: red }
+<body>
+    <h1>Hello World!
+|]
+

Outputs: (whitespace added)

+
<!DOCTYPE HTML>
+<html>
+    <head>
+        <title>Test</title>
+        <script>if (5 < 6 || 8 > 9) alert("Hello World!");</script>
+        <style>body > h1 { color: red }</style>
+    </head>
+    <body>
+        <h1>Hello World!</h1>
+    </body>
+</html>
+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/yesod-for-haskellers.html b/public/book/yesod-for-haskellers.html new file mode 100644 index 00000000..7b1d1513 --- /dev/null +++ b/public/book/yesod-for-haskellers.html @@ -0,0 +1,1235 @@ + Yesod for Haskellers :: Yesod Web Framework Book- Version 1.6 +
+

Yesod for Haskellers

+ + +

The majority of this book is built around giving practical information on how +to get common tasks done, without drilling too much into the details of what’s +going on under the surface. While the book presumes knowledge of Haskell, it +does not follow the typical style of many Haskell libraries introductions. Many +seasoned Haskellers are put off by this hiding of implementation details. The +purpose of this chapter is to address those concerns.

+

In this chapter, we’ll start off from a bare minimum web application, and +build up to more complicated examples, explaining the components and their +types along the way.

+
+

Hello Warp

+

Let’s start off with the most bare minimum application we can think of:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseLBS
+    status200
+    [("Content-Type", "text/plain")]
+    "Hello Warp!"
+

Wait a minute, there’s no Yesod in there! Don’t worry, we’ll get there. +Remember, we’re building from the ground up, and in Yesod, the ground floor is +WAI, the Web Application Interface. WAI sits between a web handler, such as a +web server or a test framework, and a web application. In our case, the +handler is Warp, a high performance web server, and our application is the +app function.

+

What’s this mysterious Application type? It’s a type synonym defined as:

+
type Application = Request
+                -> (Response -> IO ResponseReceived)
+                -> IO ResponseReceived
+

The Request value contains information such as the requested path, query +string, request headers, request body, and the IP address of the client. The +second argument is the “send response” function. Instead of simply having the +application return an IO Response, WAI uses continuation passing style to +allow for full exception safety, similar to how the bracket function works.

+

We can use this to do some simple dispatching:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, pathInfo, responseLBS)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app req sendResponse =
+    case pathInfo req of
+        ["foo", "bar"] -> sendResponse $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested /foo/bar"
+        _ -> sendResponse $ responseLBS
+            status200
+            [("Content-Type", "text/plain")]
+            "You requested something else"
+

WAI mandates that the path be split into individual fragments (the stuff +between forward slashes) and converted into text. This allows for easy pattern +matching. If you need the original, unmodified ByteString, you can use +rawPathInfo. For more information on the available fields, please see the WAI +Haddocks.

+

That addresses the request side; what about responses? We’ve already seen +responseLBS, which is a convenient way of creating a response from a lazy +ByteString. That function takes three arguments: the status code, a list of +response headers (as key/value pairs), and the body itself. But responseLBS +is just a convenience wrapper. Under the surface, WAI uses blaze-builder’s +Builder data type to represent the raw bytes. Let’s dig down another level +and use that directly:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Blaze.ByteString.Builder (Builder, fromByteString)
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseBuilder)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseBuilder
+    status200
+    [("Content-Type", "text/plain")]
+    (fromByteString "Hello from blaze-builder!" :: Builder)
+

This opens up some nice opportunities for efficiently building up response +bodies, since Builder allows for O(1) append operations. We’re also able to +take advantage of blaze-html, which sits on top of blaze-builder. Let’s see our +first HTML application.

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (Application, responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import           Text.Blaze.Html5              (Html, docTypeHtml)
+import qualified Text.Blaze.Html5              as H
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseBuilder
+    status200
+    [("Content-Type", "text/html")] -- yay!
+    (renderHtmlBuilder myPage)
+
+myPage :: Html
+myPage = docTypeHtml $ do
+    H.head $ do
+        H.title "Hello from blaze-html and Warp"
+    H.body $ do
+        H.h1 "Hello from blaze-html and Warp"
+

But there’s a limitation with using a pure Builder value: we need to create +the entire response body before returning the Response value. With lazy +evaluation, that’s not as bad as it sounds, since not all of the body will live in +memory at once. However, if we need to perform some I/O to generate our +response body (such as reading data from a database), we’ll be in trouble.

+

To deal with that situation, WAI provides a means for generating streaming +response bodies. It also allows explicit control of flushing the stream. Let’s +see how this works.

+
{-# LANGUAGE OverloadedStrings #-}
+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)
+import           Network.Wai.Handler.Warp           (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseStream
+    status200
+    [("Content-Type", "text/plain")]
+    myStream
+
+myStream :: (Builder -> IO ()) -> IO () -> IO ()
+myStream send flush = do
+    send $ fromByteString "Starting streaming response.\n"
+    send $ fromByteString "Performing some I/O.\n"
+    flush
+    -- pretend we're performing some I/O
+    threadDelay 1000000
+    send $ fromByteString "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        send $ fromByteString "Got the value: " <>
+               fromShow i <>
+               fromByteString "\n"
+ +

Another common requirement when dealing with a streaming response is safely +allocating a scarce resource- such as a file handle. By safely, I mean +ensuring that the resource will be released, even in the case of some +exception. This is where the continuation passing style mentioned above comes +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)
+import           System.IO                (IOMode (ReadMode), withFile)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = withFile "index.html" ReadMode $ \handle ->
+    sendResponse $ responseStream
+        status200
+        [("Content-Type", "text/html")]
+        $ \send _flush ->
+            let loop = do
+                    bs <- S.hGet handle 4096
+                    if S.null bs
+                        then return ()
+                        else send (fromByteString bs) >> loop
+             in loop
+

Notice how we’re able to take advantage of existing exception safe functions +like withFile to deal with exceptions properly.

+

But in the case of serving files, it’s more efficient to use responseFile, +which can use the sendfile system call to avoid unnecessary buffer copies:

+
{-# LANGUAGE OverloadedStrings #-}
+import           Network.HTTP.Types       (status200)
+import           Network.Wai              (Application, responseFile)
+import           Network.Wai.Handler.Warp (run)
+
+main :: IO ()
+main = run 3000 app
+
+app :: Application
+app _req sendResponse = sendResponse $ responseFile
+    status200
+    [("Content-Type", "text/html")]
+    "index.html"
+    Nothing -- means "serve whole file"
+            -- you can also serve specific ranges in the file
+

There are many aspects of WAI we haven’t covered here. One important topic is WAI middlewares. We also haven’t inspected request bodies at all. But for the purposes of understanding Yesod, we’ve covered enough for the moment.

+
+
+

What about Yesod?

+

In all our excitement about WAI and Warp, we still haven’t seen anything about Yesod! Since we just learnt all about WAI, our first question should be: how does Yesod interact with WAI. The answer to that is one very important function:

+
toWaiApp :: YesodDispatch site => site -> IO Application
+ +

This function takes some site value, which must be an instance of +YesodDispatch, and creates an Application. This function lives in the IO +monad, since it will likely perform actions like allocating a shared logging +buffer. The more interesting question is what this site value is all about.

+

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. +

    +
  • +
  • +

    +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 +Yesod application the hard way, by writing everything manually. We’ll +progressively add more convenience helpers on top as we go along.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.HTTP.Types            (status200)
+import           Network.Wai                   (responseBuilder)
+import           Network.Wai.Handler.Warp      (run)
+import           Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
+import qualified Text.Blaze.Html5              as H
+import           Yesod.Core                    (Html, RenderRoute (..), Yesod,
+                                                YesodDispatch (..), toWaiApp)
+import           Yesod.Core.Types              (YesodRunnerEnv (..))
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+instance YesodDispatch App where
+    yesodDispatch (YesodRunnerEnv _logger site _sessionBackend _ _) _req sendResponse =
+        sendResponse $ responseBuilder
+            status200
+            [("Content-Type", "text/html")]
+            (renderHtmlBuilder $ welcomeMessage site)
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

OK, we’ve added quite a few new pieces here, let’s attack them one at a time. +The first thing we’ve done is created a new datatype, App. This is commonly +used as the foundation data type name for each application, though you’re free +to use whatever name you want. We’ve added one field to this datatype, +welcomeMessage, which will hold the content for our homepage.

+

Next we declare our Yesod instance. We just use the default values for all of +the methods for this example. More interesting is the RenderRoute typeclass. +This is the heart of type-safe URLs. We create an associated data type for +App which lists all of our app’s accepted routes. In this case, we have just +one: the homepage, which we call HomeR. It’s yet another Yesod naming +convention to append R to all of the route data constructors.

+

We also need to create a renderRoute method, which converts each type-safe +route value into a tuple of path pieces and query string parameters. We’ll get +to more interesting examples later, but for now, our homepage has an empty list +for both of those.

+

YesodDispatch determines how our application behaves. It has one method, +yesodDispatch, of type:

+
yesodDispatch :: YesodRunnerEnv site -> Application
+

YesodRunnerEnv provides three 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.

+

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 +our implementation of yesodDispatch is able to use everything we learned +about WAI above. Notice also how we accessed the welcomeMessage from our +foundation data type.

+

Finally, we have the main function. The App value is easy to create and, as +you can see, you could just as easily have performed some I/O to acquire the +welcome message. We use toWaiApp to obtain a WAI application, and then pass +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

+

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.

+

There are many common things you’d want to do when handling a single request, +e.g.:

+
    +
  • +

    +Return some HTML. +

    +
  • +
  • +

    +Redirect to a different URL. +

    +
  • +
  • +

    +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:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+import           Network.Wai              (pathInfo)
+import           Network.Wai.Handler.Warp (run)
+import qualified Text.Blaze.Html5         as H
+import           Yesod.Core               (HandlerT, Html, RenderRoute (..),
+                                           Yesod, YesodDispatch (..), getYesod,
+                                           notFound, toWaiApp, yesodRunner)
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessage :: !Html
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO Html
+getHomeR = do
+    site <- getYesod
+    return $ welcomeMessage site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+main = do
+    -- We could get this message from a file instead if we wanted.
+    let welcome = H.p "Welcome to Yesod!"
+    waiApp <- toWaiApp App
+        { welcomeMessage = welcome
+        }
+    run 3000 waiApp
+

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 +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.

+

Our function body is short. We use the getYesod function to get the +foundation data type value, and then return the welcomeMessage field. We’ll +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 Applications. Let’s look at its type signature:

+
yesodRunner :: (ToTypedContent res, Yesod site)
+            => HandlerT site IO res
+            -> YesodRunnerEnv site
+            -> Maybe (Route site)
+            -> Application
+

We’re already familiar with YesodRunnerEnv from our previous example. As you +can see in our call to yesodRunner above, we pass that value in unchanged. +The Maybe (Route site) is a bit interesting, and gives us more insight into +how type-safe URLs work. Until now, we only saw the rendering side of these +URLs. But just as important is the parsing side: converting a requested path +into a route value. In our example, this code is just a few lines, and we store +the result in maybeRoute.

+ +

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 +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 +template. That template can be overridden in the Yesod typeclass; out of the +box, it’s just a boring HTML page.

+

This almost all makes sense, except for one issue: what’s that ToTypedContent +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 +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, +and sending the response body.

+
+

(To)Content, (To)TypedContent

+

At the very core of Yesod’s content system are the following types:

+
data Content = ContentBuilder !Builder !(Maybe Int) -- ^ The content and optional content length.
+             | ContentSource !(Source (ResourceT IO) (Flush Builder))
+             | ContentFile !FilePath !(Maybe FilePart)
+             | ContentDontEvaluate !Content
+
+type ContentType = ByteString
+data TypedContent = TypedContent !ContentType !Content
+

Content should remind you a bit of the WAI response types. ContentBuilder +is similar to responseBuilder, ContentSource is like responseStream but specialized to conduit, and +ContentFile is like responseFile. Unlike their WAI counterparts, none of +these constructors contain information on the status code or response headers; +that’s handled orthogonally in Yesod.

+

The one completely new constructor is ContentDontEvaluate. By default, when +you create a response body in Yesod, Yesod fully evaluates the body before +generating the response. The reason for this is to ensure that there are no +impure exceptions in your value. Yesod wants to make sure to catch any such +exceptions before starting to send your response so that, if there is an +exception, Yesod can generate a proper 500 internal server error response +instead of simply dying in the middle of sending a non-error response. However, +performing this evaluation can cause more memory usage. Therefore, Yesod +provides a means of opting out of this protection.

+

TypedContent is then a minor addition to Content: it includes the +ContentType as well. Together with a convention that an application returns a +200 OK status unless otherwise specified, we have everything we need from the +TypedContent type to create a response.

+

Yesod could have taken the approach of requiring users to always return +TypedContent from a handler function, but that would have required manually +converting to that type. Instead, Yesod uses a pair of typeclasses for this, +appropriately named ToContent and ToTypedContent. They have exactly the +definitions you’d expect:

+
class ToContent a where
+    toContent :: a -> Content
+class ToContent a => ToTypedContent a where
+    toTypedContent :: a -> TypedContent
+

And Yesod provides instances for many common data types, including Text, +Html, and aeson’s Value type (containing JSON data). That’s how the +getHomeR function was able to return Html: Yesod knows how to convert it to +TypedContent, and from there it can be converted into a WAI response.

+
+
+

HasContentType and representations

+

This typeclass approach allows for one other nice abstraction. For many types, the type system itself lets us know what the content-type for the content should be. For example, Html will always be served with a text/html content-type.

+ +

Some requests to a web application can be displayed with various representation. For example, a request for tabular data could be served with:

+
    +
  • +

    +An HTML table +

    +
  • +
  • +

    +A CSV file +

    +
  • +
  • +

    +XML +

    +
  • +
  • +

    +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 +selectRep/provideRep function combo to provide multiple representations, +and have the framework automatically choose the appropriate one based on the +client headers.

+

The last missing piece to make this all work is the HasContentType typeclass:

+
class ToTypedContent a => HasContentType a where
+    getContentType :: Monad m => m a -> ContentType
+

The parameter m a is just a poor man’s Proxy type. And, in hindsight, we +should have used Proxy, but that would now be a breaking change. There are +instances for this typeclass for most data types supported by ToTypedContent. +Below is our example from above, tweaked just a bit to provide multiple +representations of the data:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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 (..),
+                                           TypedContent, Value, Yesod,
+                                           YesodDispatch (..), getYesod,
+                                           notFound, object, provideRep,
+                                           selectRep, toWaiApp, yesodRunner,
+                                           (.=))
+
+-- | Our foundation datatype.
+data App = App
+    { welcomeMessageHtml :: !Html
+    , welcomeMessageText :: !Text
+    , welcomeMessageJson :: !Value
+    }
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = do
+    site <- getYesod
+    selectRep $ do
+        provideRep $ return $ welcomeMessageHtml site
+        provideRep $ return $ welcomeMessageText site
+        provideRep $ return $ welcomeMessageJson site
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+main = do
+    waiApp <- toWaiApp App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+    run 3000 waiApp
+
+
+

Convenience warp function

+

And one minor convenience you’ll see quite a bit in the Yesod world. It’s very +common to call toWaiApp to create a WAI Application, and then pass that to +Warp’s run function. So Yesod provides a convenience warp wrapper function. +We can replace our previous main function with the following:

+
main :: IO ()
+main =
+    warp 3000 App
+        { welcomeMessageHtml = H.p "Welcome to Yesod!"
+        , welcomeMessageText = "Welcome to Yesod!"
+        , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
+        }
+

There’s also a warpEnv function which reads the port number from the PORT +environment variable, which is useful for working with platforms such as FP +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.

+

In addition to that standard functionality, the following are some common +categories of functions. The only requirement Yesod places on your handler +functions is that, ultimately, they return a type which is an instance of +ToTypedContent.

+

This section is just a short overview of functionality. For more information, +you should either look through the Haddocks, or read the rest of this book.

+
+

Getting request parameters

+

There are a few pieces of information provided by the client in a request:

+
    +
  • +

    +The requested path. This is usually handled by Yesod’s routing framework, and is not directly queried in a handler function. +

    +
  • +
  • +

    +Query string parameters. This can be queried using lookupGetParam. +

    +
  • +
  • +

    +Request bodies. In the case of URL encoded and multipart bodies, you can use lookupPostParam to get the request parameter. For multipart bodies, there’s also lookupFile for file parameters. +

    +
  • +
  • +

    +Request headers can be queried via lookupHeader. (And response headers can be set with addHeader.) +

    +
  • +
  • +

    +Yesod parses cookies for you automatically, and they can be queried using lookupCookie. (Cookies can be set via the setCookie function.) +

    +
  • +
  • +

    +Finally, Yesod provides a user session framework, where data can be set in a cryptographically secure session and associated with each user. This can be queried and set using the functions lookupSession, setSession and deleteSession. +

    +
  • +
+

While you can use these functions directly for such purposes as processing +forms, you usually will want to use the yesod-form library, which provides a +higher level form abstraction based on applicative functors.

+
+
+

Short circuiting

+

In some cases, you’ll want to short circuit the handling of a request. Reasons +for doing this would be:

+
    +
  • +

    +Send an HTTP redirect, via the redirect function. This will default to using the 303 status code. You can use redirectWith to get more control over this. +

    +
  • +
  • +

    +Return a 404 not found with notFound, or a 405 bad method via badMethod. +

    +
  • +
  • +

    +Indicate some error in the request via notAuthenticated, permissionDenied, or invalidArgs. +

    +
  • +
  • +

    +Send a special response, such as with sendFile or sendResponseStatus (to override the status 200 response code) +

    +
  • +
  • +

    +sendWaiResponse to drop down a level of abstraction, bypass some Yesod abstractions, and use WAI itself. +

    +
  • +
+
+
+

Streaming

+

So far, the examples of ToTypedContent instances I gave all involved +non-streaming responses. Html, Text, and Value all get converted into a +ContentBuilder constructor. As such, they cannot interleave I/O with sending +data to the user. What happens if we want to perform such interleaving?

+

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 +Builder. Yesod also provides a number of convenience functions for creating +that Source, such as sendChunk, sendChunkBS, and sendChunkText.

+

Here’s an example, which just converts our initial responseSource example +from WAI to Yesod.

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies      #-}
+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,
+                                                     YesodDispatch (..), liftIO,
+                                                     notFound, respondSource,
+                                                     sendChunk, sendChunkBS,
+                                                     sendChunkText, sendFlush,
+                                                     warp, yesodRunner)
+
+-- | Our foundation datatype.
+data App = App
+
+instance Yesod App
+
+instance RenderRoute App where
+    data Route App = HomeR -- just one accepted URL
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ( [] -- empty path info, means "/"
+                        , [] -- empty query string
+                        )
+
+getHomeR :: HandlerT App IO TypedContent
+getHomeR = respondSource "text/plain" $ do
+    sendChunkBS "Starting streaming response.\n"
+    sendChunkText "Performing some I/O.\n"
+    sendFlush
+    -- pretend we're performing some I/O
+    liftIO $ threadDelay 1000000
+    sendChunkBS "I/O performed, here are some results.\n"
+    forM_ [1..50 :: Int] $ \i -> do
+        sendChunk $ fromByteString "Got the value: " <>
+                    fromShow i <>
+                    fromByteString "\n"
+
+instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute =
+                case pathInfo req of
+                    [] -> Just HomeR
+                    _  -> Nothing
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+main :: IO ()
+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, +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 +Fibonacci number. And if you visit /, it will automatically redirect you to +/fib/1.

+

In the Yesod world, the first question to ask is: how do we model our route +data type? This is pretty straight-forward: data Route App = HomeR | FibR +Int. The question is: how do we want to define our RenderRoute instance? We +need to convert the Int to a Text. What function should we use?

+

Before you answer that, realize that we’ll also need to be able to parse back a Text into an Int for dispatch purposes. So we need to make sure that we have a pair of functions with the property fromText . toText == Just. Show/Read could be a candidate for this, except that:

+
    +
  1. +

    +We’d be required to convert through String. +

    +
  2. +
  3. +

    +The Show/Read instances for Text and String both involve extra escaping, which we don’t want to incur. +

    +
  4. +
+

Instead, the approach taken by Yesod is the path-pieces package, and in +particular the PathPiece typeclass, defined as:

+
class PathPiece s where
+    fromPathPiece :: Text -> Maybe s
+    toPathPiece   :: s    -> Text
+

Using this typeclass, we can write parse and render functions for our route datatype:

+
instance RenderRoute App where
+    data Route App = HomeR | FibR Int
+        deriving (Show, Read, Eq, Ord)
+
+    renderRoute HomeR = ([], [])
+    renderRoute (FibR i) = (["fib", toPathPiece i], [])
+
+parseRoute' [] = Just HomeR
+parseRoute' ["fib", i] = FibR <$> fromPathPiece i
+parseRoute' _ = Nothing
+

And then we can write our YesodDispatch typeclass instance:

+
instance YesodDispatch App where
+    yesodDispatch yesodRunnerEnv req sendResponse =
+        let maybeRoute = parseRoute' (pathInfo req)
+            handler =
+                case maybeRoute of
+                    Nothing -> notFound
+                    Just HomeR -> getHomeR
+                    Just (FibR i) -> getFibR i
+         in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
+
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR i = return $ show $ fibs !! i
+

Notice our call to redirect in getHomeR. We’re able to use the route +datatype as the parameter to redirect, and Yesod takes advantage of our +renderRoute function to create a textual link.

+
+
+

Routing with Template Haskell

+

Now let’s suppose we want to add a new route to our previous application. We’d +have to make the following changes:

+
    +
  1. +

    +Modify the Route datatype itself. +

    +
  2. +
  3. +

    +Add a clause to renderRoute. +

    +
  4. +
  5. +

    +Add a clause to parseRoute', and make sure it corresponds correctly to renderRoute. +

    +
  6. +
  7. +

    +Add a clause to the case statement in yesodDispatch to call our handler function. +

    +
  8. +
  9. +

    +Write our handler function. +

    +
  10. +
+

That’s a lot of changes! And lots of manual, boilerplate changes means lots of +potential for mistakes. Some of the mistakes can be caught by the compiler if +you turn on warnings (forgetting to add a clause in renderRoute or a match in +yesodDispatch's case statement), but others cannot (ensuring that +renderRoute and parseRoute have the same logic, or adding the parseRoute +clause).

+

This is where Template Haskell comes into the Yesod world. Instead of dealing +with all of these changes manually, Yesod declares a high level routing syntax. +This syntax lets you specify your route syntax, dynamic parameters, constructor +names, and accepted request methods, and automatically generates parse, render, +and dispatch functions.

+

To get an idea of how much manual coding this saves, have a look at our +previous example converted to the Template Haskell version:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+{-# LANGUAGE ViewPatterns      #-}
+import           Yesod.Core (RenderRoute (..), Yesod, mkYesod, parseRoutes,
+                             redirect, warp)
+
+-- | Our foundation datatype.
+data App = App
+
+mkYesod "App" [parseRoutes|
+/         HomeR GET
+/fib/#Int FibR  GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler ()
+getHomeR = redirect (FibR 1)
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> Handler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 App
+

What’s wonderful about this is, as the developer, you can now focus on the +important part of your application, and not get involved in the details of +writing parsers and renderers. There are of course some downsides to the usage +of Template Haskell:

+
    +
  • +

    +Compile times are a bit slower. +

    +
  • +
  • +

    +The details of what’s going on behind the scenes aren’t easily apparent. (Though you can use cabal haddock to see what identifiers have been generated for you.) +

    +
  • +
  • +

    +You don’t have as much fine-grained control. For example, in the Yesod route syntax, each dynamic parameter has to be a separate field in the route constructor, as opposed to bundling fields together. This is a conscious trade-off in Yesod between flexibility and complexity. +

    +
  • +
+

This usage of Template Haskell is likely the most controversial decision in +Yesod. I personally think the benefits definitely justify its usage. But if +you’d rather avoid Template Haskell, you’re free to do so. Every example so far +in this chapter has done so, and you can follow those techniques. We also have +another, simpler approach in the Yesod world: LiteApp.

+
+

LiteApp

+

LiteApp allows you to throw away type safe URLs and Template Haskell. It uses +a simple routing DSL in pure Haskell. Once again, as a simple comparison, let’s +rewrite our Fibonacci example to use it.

+
import           Data.Text  (pack)
+import           Yesod.Core (LiteHandler, dispatchTo, dispatchTo, liteApp,
+                             onStatic, redirect, warp, withDynamic)
+
+getHomeR :: LiteHandler ()
+getHomeR = redirect "/fib/1"
+
+fibs :: [Int]
+fibs = 0 : scanl (+) 1 fibs
+
+getFibR :: Int -> LiteHandler String
+getFibR i = return $ show $ fibs !! i
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "fib") $ withDynamic $ \i -> dispatchTo (getFibR i)
+

There you go, a simple Yesod app without any language extensions at all! +However, even this application still demonstrates some type safety. Yesod will +use fromPathPiece to convert the parameter for getFibR from Text to an +Int, so any invalid parameter will be got by Yesod itself. It’s just one less +piece of checking that you have to perform.

+
+
+
+

Shakespeare

+

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.

+

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.

+
import           Data.Text  (pack)
+import           Yesod.Core
+
+getHomeR :: LiteHandler TypedContent
+getHomeR = return $ TypedContent typeHtml $ toContent
+    "<html><head><title>Hi There!</title>\
+    \<link rel='stylesheet' href='/style.css'>\
+    \<script src='/script.js'></script></head>\
+    \<body><h1>Hello World!</h1></body></html>"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

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 +content as Strings, convert them to Content using toContent, then wrap +them with a TypedContent constructor to give them the appropriate +content-type headers.

+

But as usual, we can do better. Dealing with Strings is not very efficient, +and it’s tedious to have to manually put in the content type all the time. But +we already know the solution to those problems: use the Html datatype from +blaze-html. Let’s convert our getHomeR function to use it:

+
import           Data.Text                   (pack)
+import           Text.Blaze.Html5            (toValue, (!))
+import qualified Text.Blaze.Html5            as H
+import qualified Text.Blaze.Html5.Attributes as A
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = return $ H.docTypeHtml $ do
+    H.head $ do
+        H.title $ toHtml "Hi There!"
+        H.link ! A.rel (toValue "stylesheet") ! A.href (toValue "/style.css")
+        H.script ! A.src (toValue "/script.js") $ return ()
+    H.body $ do
+        H.h1 $ toHtml "Hello World!"
+
+getStyleR :: LiteHandler TypedContent
+getStyleR = return $ TypedContent typeCss $ toContent
+    "h1 { color: red }"
+
+getScriptR :: LiteHandler TypedContent
+getScriptR = return $ TypedContent typeJavascript $ toContent
+    "alert('Yay, Javascript works too!');"
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+

Ahh, far nicer. blaze-html provides a convenient combinator library, and will +execute far faster in most cases than whatever String concatenation you might +attempt.

+

If you’re happy with blaze-html combinators, by all means use them. However, +many people like to use a more specialized templating language. Yesod’s +standard provider for this is the Shakespearean languages: Hamlet, Lucius, and +Julius. You are by all means welcome to use a different system if so desired, +the only requirement is that you can get a Content value from the template.

+

Since Shakespearean templates are compile-time checked, their usage requires +either quasiquotation or Template Haskell. We’ll go for the former approach +here. Please see the Shakespeare chapter in the book for more information.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Data.Text   (Text, pack)
+import           Text.Julius (Javascript)
+import           Text.Lucius (Css)
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = withUrlRenderer $
+    [hamlet|
+        $doctype 5
+        <html>
+            <head>
+                <title>Hi There!
+                <link rel=stylesheet href=/style.css>
+                <script src=/script.js>
+            <body>
+                <h1>Hello World!
+    |]
+
+getStyleR :: LiteHandler Css
+getStyleR = withUrlRenderer [lucius|h1 { color: red }|]
+
+getScriptR :: LiteHandler Javascript
+getScriptR = withUrlRenderer [julius|alert('Yay, Javascript works too!');|]
+
+main :: IO ()
+main = warp 3000 $ liteApp $ do
+    dispatchTo getHomeR
+    onStatic (pack "style.css") $ dispatchTo getStyleR
+    onStatic (pack "script.js") $ dispatchTo getScriptR
+
+

URL rendering function

+

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 +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.

+

The recommended approach instead would be to use our type-safe URL datatype in +our template instead of including explicit strings. As mentioned above, +LiteApp doesn’t provide any meaningful type-safe URLs, so we don’t have that +option here. But if you use the Template Haskell generators, you get type-safe +URLs for free.

+

In any event, the Shakespearean templates all expect to receive a function to +handle the rendering of a type-safe URL. Since we don’t actually use any +type-safe URLs, just about any function would work here (the function will be +ignored entirely), but withUrlRenderer is a convenient way of doing this.

+

As we’ll see next, withUrlRenderer isn’t really needed most of the time, +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 +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 +using them.

+
{-# LANGUAGE QuasiQuotes #-}
+import           Yesod.Core
+
+getHomeR :: LiteHandler Html
+getHomeR = defaultLayout $ do
+    setTitle $ toHtml "Hi There!"
+    [whamlet|<h1>Hello World!|]
+    toWidget [lucius|h1 { color: red }|]
+    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 +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 +temporary static files and linking to them).

+

Widgets also have another advantage. The defaultLayout function is a member +of the Yesod typeclass, and can be modified to provide a customized +look-and-feel for your website. Many built-in pieces of Yesod, such as error +messages, take advantage of the widget system, so by using widgets, you get a +consistent feel throughout your site.

+
+
+

Details we won’t cover

+

Hopefully this chapter has pulled back enough of the “magic” in Yesod to let +you understand what’s going on under the surface. We could of course continue +using this approach for analyze the rest of the Yesod ecosystem, but that would +be mostly redundant with the rest of this book. Hopefully you can now feel more +informed as you read chapters on Persistent, forms, sessions, and subsites.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/yesod-typeclass.html b/public/book/yesod-typeclass.html new file mode 100644 index 00000000..40d48f1d --- /dev/null +++ b/public/book/yesod-typeclass.html @@ -0,0 +1,676 @@ + Yesod Typeclass :: Yesod Web Framework Book- Version 1.6 +
+

Yesod Typeclass

+ + +

Every one of our Yesod applications requires an instance of the Yesod +typeclass. So far, we’ve just relied on default implementations of these +methods. In this chapter, we’ll explore the meaning of many of the methods of +the Yesod typeclass.

+

The Yesod typeclass gives us a central place for defining settings for our +application. Everything has a default definition which is often the +right thing. But in order to build a powerful, customized application, you’ll +usually end up wanting to override at least a few of these methods.

+ +
+

Rendering and Parsing URLs

+

We’ve already mentioned how Yesod is able to automatically render type-safe +URLs into a textual URL that can be inserted into an HTML page. Let’s say we +have a route definition that looks like:

+
mkYesod "MyApp" [parseRoutes|
+/some/path SomePathR GET
+]
+

If we place SomePathR into a hamlet template, how does Yesod render it? Yesod +always tries to construct absolute URLs. This is especially useful once we +start creating XML sitemaps and Atom feeds, or sending emails. But in order to +construct an absolute URL, we need to know the domain name of the application.

+

You might think we could get that information from the user’s request, but we +still need to deal with ports. And even if we get the port number from the +request, are we using HTTP or HTTPS? And even if you know that, such an +approach would mean that, depending on how the user submitted a request would +generate different URLs. For example, we would generate different URLs +depending if the user connected to "example.com" or "www.example.com". For +Search Engine Optimization, we want to be able to consolidate on a single +canonical URL.

+

And finally, Yesod doesn’t make any assumption about where you host your +application. For example, I may have a mostly static site +(http://static.example.com/), but I’d like to stick a Yesod-powered Wiki at +/wiki/. There is no reliable way for an application to determine what subpath +it is being hosted from. So instead of doing all of this guesswork, Yesod needs +you to tell it the application root.

+

Using the wiki example, you would write your Yesod instance as:

+
instance Yesod MyWiki where
+    approot = ApprootStatic "http://static.example.com/wiki"
+

Notice that there is no trailing slash there. Next, when Yesod wants to +construct a URL for SomePathR, it determines that the relative path for +SomePathR is /some/path, appends that to your approot and creates +http://static.example.com/wiki/some/path.

+

The default value of approot is guessApproot. +This works fine for the common case of a link within your +application, and your application being hosted at the root of your domain. But +if you have any use cases which demand absolute URLs (such as sending an +email), it’s best to use ApprootStatic.

+

In addition to the ApprootStatic constructor demonstrated above, you can also +use the ApprootMaster and ApprootRequest constructors. The former allows +you to determine the approot from the foundation value, which would let you +load up the approot from a config file, for instance. The latter allows you to +additionally use the request value to determine the approot; using this, you +could for example provide a different domain name depending on how the user +requested the site in the first place.

+

The scaffolded site uses ApprootMaster by default, and pulls your approot +from either the APPROOT environment variable or a config file on launch. +Additionally, it loads different settings for testing and +production builds, so you can easily test on one domain- like localhost- and +serve from a different domain. You can modify these values from the config +file.

+
+

joinPath

+

In order to convert a type-safe URL into a text value, Yesod uses two helper +functions. The first is the renderRoute method of the RenderRoute +typeclass. Every type-safe URL is an instance of this typeclass. renderRoute +converts a value into a list of path pieces. For example, our SomePathR from +above would be converted into ["some", "path"].

+ +

The other function is the joinPath method of the Yesod typeclass. This function takes four arguments:

+
    +
  • +

    +The foundation value +

    +
  • +
  • +

    +The application root +

    +
  • +
  • +

    +A list of path segments +

    +
  • +
  • +

    +A list of query string parameters +

    +
  • +
+

It returns a textual URL. The default implementation does the “right thing”: +it separates the path pieces by forward slashes, prepends the application root, +and appends the query string.

+

If you are happy with default URL rendering, you should not need to modify it. +However, if you want to modify URL rendering to do things like append a +trailing slash, this would be the place to do it.

+
+
+

cleanPath

+

The flip side of joinPath is cleanPath. Let’s look at how it gets used in +the dispatch process:

+
    +
  1. +

    +The path info requested by the user is split into a series of path pieces. +

    +
  2. +
  3. +

    +We pass the path pieces to the cleanPath function. +

    +
  4. +
  5. +

    +If cleanPath indicates a redirect (a Left response), then a 301 response +is sent to the client. This is used to force canonical URLs (eg, remove extra +slashes). +

    +
  6. +
  7. +

    +Otherwise, we try to dispatch using the response from cleanPath (a +Right). If this works, we return a response. Otherwise, we return a 404. +

    +
  8. +
+

This combination allows subsites to retain full control of how their URLs +appear, yet allows master sites to have modified URLs. As a simple example, +let’s see how we could modify Yesod to always produce trailing slashes on URLs:

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# 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)
+import           Yesod
+
+data Slash = Slash
+
+mkYesod "Slash" [parseRoutes|
+/ RootR GET
+/foo FooR GET
+|]
+
+instance Yesod Slash where
+    joinPath _ ar pieces' qs' =
+        fromText ar `mappend` encodePath pieces qs
+      where
+        qs = map (TE.encodeUtf8 *** go) qs'
+        go "" = Nothing
+        go x = Just $ TE.encodeUtf8 x
+        pieces = pieces' ++ [""]
+
+    -- We want to keep canonical URLs. Therefore, if the URL is missing a
+    -- trailing slash, redirect. But the empty set of pieces always stays the
+    -- same.
+    cleanPath _ [] = Right []
+    cleanPath _ s
+        | dropWhile (not . T.null) s == [""] = -- the only empty string is the last one
+            Right $ init s
+        -- Since joinPath will append the missing trailing slash, we simply
+        -- remove empty pieces.
+        | otherwise = Left $ filter (not . T.null) s
+
+getRootR :: Handler Html
+getRootR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{RootR}>RootR
+        <p>
+            <a href=@{FooR}>FooR
+    |]
+
+getFooR :: Handler Html
+getFooR = getRootR
+
+main :: IO ()
+main = warp 3000 Slash
+

First, let’s look at our joinPath implementation. This is copied almost +verbatim from the default Yesod implementation, with one difference: we append +an extra empty string to the end. When dealing with path pieces, an empty +string will append another slash. So adding an extra empty string will force a +trailing slash.

+

cleanPath is a little bit trickier. First, we check for the empty path like +before, and if so pass it through as-is. We use Right to indicate that a +redirect is not necessary. The next clause is actually checking for two +different possible URL issues:

+
    +
  • +

    +There is a double slash, which would show up as an empty string in the middle + of our paths. +

    +
  • +
  • +

    +There is a missing trailing slash, which would show up as the last piece not + being an empty string. +

    +
  • +
+

Assuming neither of those conditions hold, then only the last piece is empty, +and we should dispatch based on all but the last piece. However, if this is not +the case, we want to redirect to a canonical URL. In this case, we strip out +all empty pieces and do not bother appending a trailing slash, since joinPath +will do that for us.

+
+
+
+

defaultLayout

+

Most websites like to apply some general template to all of their pages. +defaultLayout is the recommended approach for this. While you could just as +easily define your own function and call that instead, when you override +defaultLayout all of the Yesod-generated pages (error pages, authentication +pages) automatically get this style.

+

Overriding is very straight-forward: we use widgetToPageContent to convert a +Widget to a title, head tags and body tags, and then use withUrlRenderer to +convert a Hamlet template into an Html value. We can even add extra widget +components, like a Lucius template, from within defaultLayout. For more +information, see the previous chapter on widgets.

+

If you are using the scaffolded site, you can modify the files +templates/default-layout.hamlet and +templates/default-layout-wrapper.hamlet. The former contains most of the +contents of the <body> tag, while the latter has the rest of the HTML, such +as doctype and <head> tag. See those files for more details.

+
+

getMessage

+

Even though we haven’t covered sessions yet, I’d like to mention getMessage +here. A common pattern in web development is setting a message in one handler +and displaying it in another. For example, if a user POSTs a form, you may +want to redirect him/her to another page along with a “Form submission +complete” message. This is commonly known as +Post/Redirect/Get.

+

To facilitate this, Yesod comes built in with a pair of functions: setMessage +sets a message in the user session, and getMessage retrieves the message (and +clears it, so it doesn’t appear a second time). It’s recommended that you put +the result of getMessage into your defaultLayout. For example:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+import Data.Time (getCurrentTime)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App where
+    defaultLayout contents = do
+        PageContent title headTags bodyTags <- widgetToPageContent contents
+        mmsg <- getMessage
+        withUrlRenderer [hamlet|
+            $doctype 5
+
+            <html>
+                <head>
+                    <title>#{title}
+                    ^{headTags}
+                <body>
+                    $maybe msg <- mmsg
+                        <div #message>#{msg}
+                    ^{bodyTags}
+        |]
+
+getHomeR :: Handler Html
+getHomeR = do
+    now <- liftIO getCurrentTime
+    setMessage $ toHtml $ "You previously visited at: " ++ show now
+    defaultLayout [whamlet|<p>Try refreshing|]
+
+main :: IO ()
+main = warp 3000 App
+

We’ll cover getMessage/setMessage in more detail when we discuss sessions.

+
+
+
+

Custom error pages

+

One of the marks of a professional web site is a properly designed error page. +Yesod gets you a long way there by automatically using your defaultLayout for +displaying error pages. But sometimes, you’ll want to go even further. For +this, you’ll want to override the errorHandler method:

+
{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Yesod
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+/error ErrorR GET
+/not-found NotFoundR GET
+|]
+
+instance Yesod App where
+    errorHandler NotFound = fmap toTypedContent $ defaultLayout $ do
+        setTitle "Request page not located"
+        toWidget [hamlet|
+<h1>Not Found
+<p>We apologize for the inconvenience, but the requested page could not be located.
+|]
+    errorHandler other = defaultErrorHandler other
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href=@{ErrorR}>Internal server error
+            <a href=@{NotFoundR}>Not found
+    |]
+
+getErrorR :: Handler ()
+getErrorR = error "This is an error"
+
+getNotFoundR :: Handler ()
+getNotFoundR = notFound
+
+main :: IO ()
+main = warp 3000 App
+

Here we specify a custom 404 error page. We can also use the +defaultErrorHandler when we don’t want to write a custom handler for each +error type. Due to type constraints, we need to start off our methods with +fmap toTypedContent, but otherwise you can write a typical handler function. +(We’ll learn more about TypedContent in the next chapter.)

+

In fact, you could even use special responses like redirects:

+
    errorHandler NotFound = redirect HomeR
+    errorHandler other = defaultErrorHandler other
+ +
+
+

External CSS and Javascript

+ +

One of the most powerful, and most intimidating, methods in the Yesod typeclass +is addStaticContent. Remember that a Widget consists of multiple components, +including CSS and Javascript. How exactly does that CSS/JS arrive in the user’s +browser? By default, they are served in the <head> of the page, inside +<style> and <script> tags, respectively.

+

That might be simple, but it’s far from efficient. Every page load will now +require loading up the CSS/JS from scratch, even if nothing changed! What we +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 +(text/css or text/javascript) and the content itself. It will then return +one of three possible results:

+
+
+Nothing +
+

+No static file saving occurred; embed this content directly in the +HTML. This is the default behavior. +

+
+
+Just (Left Text) +
+

+This content was saved in an external file, and use the +given textual link to refer to it. +

+
+
+Just (Right (Route a, Query)) +
+

+Same, but now use a type-safe URL along with +some query string parameters. +

+
+
+

The Left result is useful if you want to store your static files on an +external server, such as a CDN or memory-backed server. The Right result is +more commonly used, and ties in very well with the static subsite. This is the +recommended approach for most applications, and is provided by the scaffolded +site by default.

+ +

The scaffolded addStaticContent does a number of intelligent things to help +you out:

+
    +
  • +

    +It automatically minifies your Javascript using the hjsmin package. +

    +
  • +
  • +

    +It names the output files based on a hash of the file contents. This means + you can set your cache headers to far in the future without fears of stale + content. +

    +
  • +
  • +

    +Also, since filenames are based on hashes, you can be guaranteed that a file + doesn’t need to be written if a file with the same name already exists. The + scaffold code automatically checks for the existence of that file, and avoids + the costly disk I/O of a write if it’s not necessary. +

    +
  • +
+
+
+

Smarter Static Files

+

Google recommends an important optimization: +serve +static files from a separate domain. The advantage to this approach is that +cookies set on your main domain are not sent when retrieving static files, thus +saving on a bit of bandwidth.

+

To facilitate this, we have the urlParamRenderOverride method. +This method intercepts the normal URL rendering and query string parameters. +Then, it sets a special value for some routes. +For example, the scaffolding defines this method as:

+
urlParamRenderOverride :: site
+                       -> Route site
+                       -> [(T.Text, T.Text)] -- ^ query string
+                       -> Maybe Builder
+urlParamRenderOverride y (StaticR s) _ =
+    Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s
+urlParamRenderOverride _ _ _ = Nothing
+

This means that static routes are served from a special static root, which you +can configure to be a different domain. This is a great example of the power +and flexibility of type-safe URLs: with a single line of code you’re able to +change the rendering of static routes throughout all of your handlers.

+
+
+

Authentication/Authorization

+

For simple applications, checking permissions inside each handler function can +be a simple, convenient approach. However, it doesn’t scale well. Eventually, +you’re going to want to have a more declarative approach. Many systems out +there define ACLs, special config files, and a lot of other hocus-pocus. In +Yesod, it’s just plain old Haskell. There are three methods involved:

+
+
+isWriteRequest +
+

+Determine if the current request is a "read" or "write" operations. By default, Yesod follows RESTful principles, and assumes GET, HEAD, OPTIONS, and TRACE requests are read-only, while all others are writable. +

+
+
+isAuthorized +
+

+Takes a route (i.e., type-safe URL) and a boolean indicating whether or not the request is a write request. It returns an AuthResult, which can have one of three values: +

+
    +
  • +

    +Authorized +

    +
  • +
  • +

    +AuthenticationRequired +

    +
  • +
  • +

    +Unauthorized +

    +
  • +
+
+
+

By default, it returns Authorized for all requests.

+
+
+authRoute +
+

+If isAuthorized returns AuthenticationRequired, then redirect +to the given route. If no route is provided (the default), return a 401 +“authentication required” message. +

+
+
+

These methods tie in nicely with the yesod-auth package, which is used by the +scaffolded site to provide a number of authentication options, such as OpenID, +Mozilla Persona, email, username and Twitter. We’ll cover more concrete +examples in the auth chapter.

+
+
+

Some Simple Settings

+

Not everything in the Yesod typeclass is complicated. Some methods are simple +functions. Let’s just go through the list:

+
+
+maximumContentLength +
+

+To prevent Denial of Service (DoS) attacks, Yesod will +limit the size of request bodies. Some of the time, you’ll want to bump that +limit for some routes (e.g., a file upload page). This is where you’d do that. +

+
+
+fileUpload +
+

+Determines how uploaded files are treated, based on the size of +the request. The two most common approaches are saving the files in memory, or +streaming to temporary files. By default, small requests are kept in memory and +large ones are stored to disk. +

+
+
+shouldLogIO +
+

+Determines if a given log message (with associated source and +level) should be sent to the log. This allows you to put lots of debugging +information into your app, but only turn it on as necessary. +

+
+
+

For the most up-to-date information, please see the Haddock API documentation +for the Yesod typeclass.

+
+
+

Summary

+

The Yesod typeclass has a number of overrideable methods that allow you to +configure your application. They are all optional, and provide sensible +defaults. By using built-in Yesod constructs like defaultLayout and +getMessage, you’ll get a consistent look-and-feel throughout your site, +including pages automatically generated by Yesod such as error pages and +authentication.

+

We haven’t covered all the methods in the Yesod typeclass in this chapter. For +a full listing of methods available, you should consult the Haddock +documentation.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/book/yesods-monads.html b/public/book/yesods-monads.html new file mode 100644 index 00000000..50371c09 --- /dev/null +++ b/public/book/yesods-monads.html @@ -0,0 +1,645 @@ + Yesod’s Monads :: Yesod Web Framework Book- Version 1.6 +
+

Yesod’s Monads

+ + +

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 +Model-View-Controller (MVC) terms, we could consider YesodDB to be the model, +Widget to be the view, and Handler to be the controller.

+

So far, we’ve presented some very straight-forward ways to use these monads: +your main handler will run in Handler, using runDB to execute a YesodDB +query, and defaultLayout to return a Widget, which in turn was created by +calls to toWidget.

+

However, if we have a deeper understanding of these types, we can achieve some +fancier results.

+
+

Monad Transformers

+
+ +Shrek- more or less + +

Monads are like onions. Monads are not like cakes.

+
+

Before we get into the heart of Yesod’s monads, we need to understand a bit +about monad transformers. (If you already know all about monad transformers, +you can likely skip this section.) Different monads provide different +functionality: Reader allows read-only access to some piece of data +throughout a computation, Error allows you to short-circuit computations, and +so on.

+

Often times, however, you would like to be able to combine a few of these +features together. After all, why not have a computation with read-only access +to some settings variable, that could error out at any time? One approach to +this would be to write a new monad like ReaderError, but this has the obvious +downside of exponential complexity: you’ll need to write a new monad for every +single possible combination.

+

Instead, we have monad transformers. In addition to Reader, we have +ReaderT, which adds reader functionality to any other monad. So we could +represent our ReaderError as (conceptually):

+
type ReaderError = ReaderT Error
+

In order to access our settings variable, we can use the ask function. But +what about short-circuiting a computation? We’d like to use throwError, but +that won’t exactly work. Instead, we need to lift our call into the next +monad up. In other words:

+
throwError :: errValue -> Error
+lift . throwError :: errValue -> ReaderT Error
+

There are a few things you should pick up here:

+
    +
  • +

    +A transformer can be used to add functionality to an existing monad. +

    +
  • +
  • +

    +A transformer must always wrap around an existing monad. +

    +
  • +
  • +

    +The functionality available in a wrapped monad will be dependent not only on + the monad transformer, but also on the inner monad that is being wrapped. +

    +
  • +
+

A great example of that last point is the IO monad. No matter how many layers +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

+ +

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.

+

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 +takeaway here is that we can layer our Persistent transformer on top of +Handler or Widget.

+

In order to make it simpler to refer to the relevant Persistent transformer, +the yesod-persistent package defines the YesodPersistBackend associated type. +For example, if I have a site called MyApp and it uses SQL, I would define +something like type instance YesodPersistBackend MyApp = SqlBackend. And for +more convenience, we have a type synonym called YesodDB which is defined as:

+
type YesodDB site = ReaderT (YesodPersistBackend site) (HandlerT site IO)
+

Our database actions will then have types that look like YesodDB MyApp +SomeResult. In order to run these, we can use the standard Persistent unwrap +functions (like runSqlPool) to run the action and get back a normal +Handler. To automate this, we provide the runDB function. Putting it all +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 +can do, by using the handlerToWidget function. So for example, you can run +database queries inside a Widget by using something like handlerToWidget . +runDB.

+
+
+

Example: Database-driven navbar

+

Let’s put some of this new knowledge into action. We want to create a Widget +that generates its output based on the contents of the database. Previously, +our approach would have been to load up the data in a Handler, and then pass +that data into a Widget. Now, we’ll do the loading of data in the Widget +itself. This is a boon for modularity, as this Widget can be used in any +Handler we want, without any need to pass in the database contents.

+
{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE QuasiQuotes                #-}
+{-# LANGUAGE TemplateHaskell            #-}
+{-# LANGUAGE TypeFamilies               #-}
+import           Control.Monad.Logger    (runNoLoggingT)
+import           Data.Text               (Text)
+import           Data.Time
+import           Database.Persist.Sqlite
+import           Yesod
+
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
+Link
+    title Text
+    url Text
+    added UTCTime
+|]
+
+data App = App ConnectionPool
+
+mkYesod "App" [parseRoutes|
+/         HomeR    GET
+/add-link AddLinkR POST
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+instance YesodPersist App where
+    type YesodPersistBackend App = SqlBackend
+    runDB db = do
+        App pool <- getYesod
+        runSqlPool db pool
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <form method=post action=@{AddLinkR}>
+            <p>
+                Add a new link to
+                <input type=url name=url value=http://>
+                titled
+                <input type=text name=title>
+                <input type=submit value="Add link">
+        <h2>Existing links
+        ^{existingLinks}
+    |]
+
+existingLinks :: Widget
+existingLinks = do
+    links <- handlerToWidget $ runDB $ selectList [] [LimitTo 5, Desc LinkAdded]
+    [whamlet|
+        <ul>
+            $forall Entity _ link <- links
+                <li>
+                    <a href=#{linkUrl link}>#{linkTitle link}
+    |]
+
+postAddLinkR :: Handler ()
+postAddLinkR = do
+    url <- runInputPost $ ireq urlField "url"
+    title <- runInputPost $ ireq textField "title"
+    now <- liftIO getCurrentTime
+    runDB $ insert $ Link title url now
+    setMessage "Link added"
+    redirect HomeR
+
+main :: IO ()
+main = runNoLoggingT $ withSqlitePool "links.db3" 10 $ \pool -> liftIO $ do
+    runSqlPersistMPool (runMigration migrateAll) pool
+    warp 3000 $ App pool
+

Pay attention in particular to the existingLinks function. Notice how all we +needed to do was apply handlerToWidget . runDB to a normal database action. +And from within getHomeR, we treated existingLinks like any ordinary +Widget, no special parameters at all. See the figure for the output of this +app.

+ +
+
+

Example: Request information

+

Likewise, you can get request information inside a Widget. Here we can determine the sort order of a list based on a GET parameter.

+
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+import           Data.List (sortOn)
+import           Data.Text (Text)
+import           Yesod
+
+data Person = Person
+    { personName :: Text
+    , personAge  :: Int
+    }
+
+people :: [Person]
+people =
+    [ Person "Miriam" 25
+    , Person "Eliezer" 3
+    , Person "Michael" 26
+    , Person "Gavriella" 1
+    ]
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+instance RenderMessage App FormMessage where
+    renderMessage _ _ = defaultFormMessage
+
+
+getHomeR :: Handler Html
+getHomeR = defaultLayout
+    [whamlet|
+        <p>
+            <a href="?sort=name">Sort by name
+            |
+            <a href="?sort=age">Sort by age
+            |
+            <a href="?">No sort
+        ^{showPeople}
+    |]
+
+showPeople :: Widget
+showPeople = do
+    msort <- runInputGet $ iopt textField "sort"
+    let people' =
+            case msort of
+                Just "name" -> sortOn personName people
+                Just "age"  -> sortOn personAge  people
+                _           -> people
+    [whamlet|
+        <dl>
+            $forall person <- people'
+                <dt>#{personName person}
+                <dd>#{show $ personAge person}
+    |]
+
+main :: IO ()
+main = warp 3000 App
+

Notice that in this case, we didn’t even have to call handlerToWidget. The +reason is that a number of the functions included in Yesod automatically work +for both Handler and Widget, by means of the MonadHandler typeclass. In +fact, MonadHandler will allow these functions to be "autolifted" through +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

+ +

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:

+
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 +application. As a motivating example, let’s consider the +monadcryptorandom +package from Hackage, which defines both a MonadCRandom typeclass for monads +which allow generating cryptographically-secure random values, and CRandT as +a concrete instance of that typeclass. You would like to write some code that +generates a random bytestring, e.g.:

+
import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+getHomeR = do
+    randomBS <- getBytes 128
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+

However, this results in an error message along the lines of:

+
    No instance for (MonadCRandom e0 (HandlerT App IO))
+      arising from a use of ‘getBytes’
+    In a stmt of a 'do' block: randomBS <- getBytes 128
+

How do we get such an instance? One approach is to simply use the CRandT monad transformer when we call getBytes. A complete example of doing so would be:

+
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE TypeFamilies      #-}
+import Yesod
+import Crypto.Random (SystemRandom, newGenIO)
+import Control.Monad.CryptoRandom
+import Data.ByteString.Base16 (encode)
+import Data.Text.Encoding (decodeUtf8)
+
+data App = App
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    gen <- liftIO newGenIO
+    eres <- evalCRandT (getBytes 16) (gen :: SystemRandom)
+    randomBS <-
+        case eres of
+            Left e -> error $ show (e :: GenError)
+            Right gen -> return gen
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+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.

+

But there are two downsides to this approach:

+
    +
  1. +

    +It requires you to jump into this alternate monad each time you want to work with random values. +

    +
  2. +
  3. +

    +It’s inefficient: you need to create a new random seed each time you enter this other monad. +

    +
  4. +
+

The second point could be worked around by storing the random seed in the +foundation datatype, in a mutable reference like an IORef, and then +atomically sampling it each time we enter the CRandT transformer. But we can +even go a step further, and use this trick to make our Handler monad itself +an instance of MonadCRandom! Let’s look at the code, which is in fact a bit +involved:

+
{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE TypeSynonymInstances  #-}
+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           Data.ByteString.Base16     (encode)
+import           Data.IORef
+import           Data.Text.Encoding         (decodeUtf8)
+import           UnliftIO.Exception         (catch)
+import           Yesod
+
+data App = App
+    { randGen :: IORef SystemRandom
+    }
+
+mkYesod "App" [parseRoutes|
+/ HomeR GET
+|]
+
+instance Yesod App
+
+getHomeR :: Handler Html
+getHomeR = do
+    randomBS <- getBytes 16
+    defaultLayout
+        [whamlet|
+            <p>Here's some random data: #{decodeUtf8 $ encode randomBS}
+        |]
+
+instance MonadError GenError Handler where
+    throwError = throwM
+    catchError = catch
+instance MonadCRandom GenError Handler where
+    getCRandom = wrap crandom
+    {-# INLINE getCRandom #-}
+    getBytes i = wrap (genBytes i)
+    {-# INLINE getBytes #-}
+    getBytesWithEntropy i e = wrap (genBytesWithEntropy i e)
+    {-# INLINE getBytesWithEntropy #-}
+    doReseed bs = do
+        genRef <- fmap randGen getYesod
+        join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+            case reseed bs gen of
+                Left e -> (gen, throwM e)
+                Right gen' -> (gen', return ())
+    {-# INLINE doReseed #-}
+
+wrap :: (SystemRandom -> Either GenError (a, SystemRandom)) -> Handler a
+wrap f = do
+    genRef <- fmap randGen getYesod
+    join $ liftIO $ atomicModifyIORef genRef $ \gen ->
+        case f gen of
+            Left e -> (gen, throwM e)
+            Right (x, gen') -> (gen', return x)
+
+main :: IO ()
+main = do
+    gen <- newGenIO
+    genRef <- newIORef gen
+    warp 3000 App
+        { randGen = genRef
+        }
+

This really comes down to a few different concepts:

+
    +
  1. +

    +We modify the App datatype to have a field for an IORef SystemRandom. +

    +
  2. +
  3. +

    +Similarly, we modify the main function to generate an IORef SystemRandom. +

    +
  4. +
  5. +

    +Our getHomeR function became a lot simpler: we can now simply call getBytes without playing with transformers. +

    +
  6. +
  7. +

    +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. +

    +
  8. +
+

Hopefully, this helps get across an important point: the power of the +HandlerT transformer. 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 +ErrorT with this abstraction.

+
+
+

Summary

+

If you completely ignore this chapter, you’ll still be able to use Yesod to +great benefit. The advantage of understanding how Yesod’s monads interact is to +be able to produce cleaner, more modular code. Being able to perform arbitrary +actions in a Widget can be a powerful tool, and understanding how Persistent +and your Handler code interact can help you make more informed design +decisions in your app.

+
+
+
+

Chapters

+ +
+ +
\ No newline at end of file diff --git a/public/contributors.html b/public/contributors.html new file mode 100644 index 00000000..cdd4acb2 --- /dev/null +++ b/public/contributors.html @@ -0,0 +1,12 @@ + Not Found +

Not Found

+

/contributors

+ +
\ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cfb4afaf619a69bc09a7f4df52710a36cdb7c190 GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/public/feed.xml b/public/feed.xml new file mode 100644 index 00000000..a2424f1e --- /dev/null +++ b/public/feed.xml @@ -0,0 +1,122 @@ +Yesod Web Framework Blog2021-02-17T02:21:04-00:00/Yesod Web Framework Team/blog/2021/02/chatwisely-intro2021-02-17T02:21:04-00:00We Made ChatWisely With Haskell<p>Three years ago, I met fellow Haskeller Brian Hurt while working with Obsidian Systems on a Reflex project. Not long after, he started telling me his ideas about how to fix social media. These ideas intrigued me, one thing led to another, and we began building ChatWisely. We thought other Haskellers might like to hear about what we’re doing and how we use Haskell to do it.</p><p>ChatWisely is a member-supported mini-blogging social network currently in open beta. We envision a place where people connect in a spirit of comradery. We see an elevated discourse and a way to show bullies the door by providing a platform to debate safely. Here’s how we’re doing it.</p><h2 align=center>Safety First</h2> +Brian and I built several mechanisms to filter out people looking for a fight or to harm others. First and foremost will be the monthly subscription fee of one dollar. We think that will discourage a large portion of toxic people. Another is a sharable mute list that we believe will help mitigate the rest. And finally, in the event of a serious Terms of Service violation, we ban payment methods rather than just accounts. <h2 align=center>Mini-Blogging</h2> +The big idea here is that sometimes, a short post isn’t enough. Brian and I made a way to link that short post to a longer one. So the timeline looks something like Twitter’s, but with some posts that can expand to something more detailed. These can be connected to other people’s posts to create a continuity in conversation hard to come by on other platforms. So when another member’s post inspires you to write a longer one about your experience with the ghcjs ffi (for example), you can link your post to theirs.<h2 align=center>Ownership of Your Timeline</h2> +Members can organize their timeline and choose to what extent they follow other people’s posts. The typical mainstream social network requires that when you follow someone you must follow everything they post, or nothing. Sure, there are filtered word lists in some cases. But none of it seems to work quite right. Instead, we have groups called caboodles that members can use to decide where other people’s posts fit, and how to share their own. So say someone likes their uncle’s cookie recipes but not his political posts. They can follow one but not the other.<h2 align=center>Geolocated Messaging</h2> +One day this pandemic will be over and we’ll be there to meet that day. At that time, when a member’s movie caboodle wants to organize local screenings of the latest blockbuster from the House of Mouse they can make posts visible to people in their proximity. Perhaps you want to target local Haskellers to organize a meetup, or leave them a message that pops up when they’ve found the meeting place. Also, I think running a scavenger hunt with geolocated clues sounds like a hoot.<h2 align=center>How It’s Built</h2> +Brian and I rely heavily on the Haskell ecosystem to build ChatWisely. Haskell’s type system reduces errors and cognitive load. GHC is our pair programming partner that tightens the delivery cycle and lets us learn how to build ChatWisely while we’re building it. Refactoring is a breeze, and unit testing is constrained to the I/O edges of the system, which means we spend less time on that and more time building the product. Here’s the principal tools we rely on to get the job done. +<br/> <br/> +<h4> Ghcjs </h4> The fact is we all hate javascript. The problem is, we can’t build web apps without it. Ghcjs lets us deal with the realities of building a product that uses a web browser for a client. The FFI lets us wrap our hand-written javascript in Haskell which helps to keep that part of the codebase pretty small.. We especially love what is built on top of that, Reflex-Dom.<h4> Reflex-Dom </h4> Reflex helps us build a system that needs to adapt to changes in data, requirements and platform. We’re learning how to build ChatWisely as we build it, and reflex keeps up with our changing ideas on how to do that. Our first app store product will be a PWA, delivered with reflex.<h4> Servant </h4> Servant delivers the API, and requires us to separate definition from implementation. This helps us keep the backend from turning into a big ball of mud. We can auto-generate clients, which we currently use in unit testing. They even have a way to generate a reflex client, and we’ll be adding that in the near future.<h4>Yesod</h4> We use Yesod for marketing tasks, which largely require static pages. Currently it handles our landing page and unsubscribe mechanism for our emails. The Yesod Widget is a monoid, and therefore composable, which makes structuring html simple. <h2 align=center>Three Reasons Why People Should Use ChatWisely.</h2> +<h4>We are member supported</h4> We won’t have ads, which means we have no need to manipulate people’s timelines in order to serve those ads. Their timeline should be about conversations of interest.<h4>We solve the tweet thread problem</h4>Brian and I find tweet threads hard to follow. Our mini-blog looks like twitter in the sense that you get a timeline of short posts. However if a post is the beginning of something more developed, that message can open up to access it.<h4>Keep the RealWorld conversation going</h4>We have delayed the development of these features for obvious reasons. But one day we’ll be together again. By then we’ll have useful geo-location tools for conference attendees and speakers to continue the conversation.<p>What makes a weekend conference fun for me are the conversations in-between formal talks. I get all caught up in compelling conversations, and want to keep that going. We’ll have a way to do that, without having to know or remember anyone’s email address or phone number.</p><p>Conference speakers will often want to build on the momentum gathered after a successful talk. Brian and I think Twitter hashtags are the terrible but often only way to do this. We’ll have a way to use proximity and common interests to help build that momentum and keep everyone engaged.</p><hr><p>We built ChatWisely as a response to the unpleasantness all too common on mainstream social networks. Depending on our membership for support creates the place we want to meet because ad revenue and data-mining motivates engagement, not conversations and connection. No one is fooled by what mainstream social networks call engagement because that looks to us like derailed conversations, confusing timelines we only have a shallow control over, and unsafe situations.</p><p>Brian and I love the daily experience of building ChatWisely, the Haskell ecosystem brings joy to the experience of running our startup. You can support us on <a href="https://www.patreon.com/chatwisely">patreon</a> and should come by and test the <a href="https://chatwisely.com/i/messages">beta</a>. We look forward to hearing from you about any ideas or questions you may have.</p>/blog/2020/05/blogannouncement2020-05-03T18:35:00-00:00A new Haskell/Yesod Beginners Blog<p>This is the annoncement for a new Haskell Beginners Blog, which first series of articles will handle the building of two Yesod-projects: a personal Blog with slighly powered batteries and in addition examples of functionality from a small-buisiness-website.</p><p>As I have decided to learn Haskell late and not long ago, this Blogpost is written out of a straight programming beginners point of view, and like so will handle also prelimiaries and standarts maybe annoying to people with a different background.</p><p>I was a bit afraid to use Yesod as a first step in my learning Haskell journey - you all know it is a big, non trival framework - but somehow, after a while of proving and looking into it, I felt quite save and secure to not miss the point. I mean, moving around in it&#39;s structure felt somehow good and plausible. Hope to transport that in this Blog.</p><p><a href="https://blog.onepigayear.de">https://blog.onepigayear.de</a></p>/blog/2019/12/bookportuguese2019-12-19T03:29:00-00:00A new Yesod book in Portuguese<p>Yesod users,</p><p>In order to help to spread the Yesod word here in Brazil, we, <a href="https://github.com/romefeller">Alexandre Garcia de Oliveira</a>, <a href="https://github.com/ptkato">Patrick Augusto da Silva</a> (<a href="https://twitter.com/ptkato_"><code>@ptkato_</code></a> on Twitter), and +<a href="https://github.com/cannarozzo">Felipe Cannarozzo Lourenço</a>, wrote a book, recently released, about Yesod +called <a href="https://www.casadocodigo.com.br/products/livro-yesod-haskell">&quot;Yesod e Haskell: Aplicações web com Programação Funcional pura&quot;</a> (&quot;Yesod and Haskell: Web +applications with pure Functional Programming&quot;, in English).</p><p>The book covers the principles of developing a web application with Yesod. Needless +to say, it follows the same model of Alexandre&#39;s first book on Haskell, giving a much needed +insight into the Yesod world within the Brazilian borders.</p><p>The book aims to allow the reader to learn Yesod from scratch, starting from the basics, like setting up +the environment using stack, and using a monolithic example in a single file, to explain the foundations of +the framework. The book goes through the Stackage&#39;s snapshots, the difference between templates and +scaffolding, Shakespearean Templates, <i>type-safe</i> routing, persistent basics, authentication &amp; +authorization, finishing up with a RESTful app example.</p><p>The idea of writing a book about Yesod came to fruition during one of Alexandre&#39;s lectures on Yesod at +<a href="https://fatecrl.edu.br">FATEC-Santos</a>, when it was realized that many students didn&#39;t read English at all +and Yesod documentation in Portuguese was virtually non-existent. Not only that, +the volition to write this book became even stronger when the TAs&#39; (Patrick and Felipe) tutoring classes were almost always spent on going through the same topics, when Portuguese documentation could have easily solved the problem.</p><p>All the pedagogic skills to write this book were founded on top of the fact that it was written in an +academic medium, considering that, it was shaped in such a way that fits the needs of an undergrad student +fairly well. Now, hundreds of FATEC-Santos alumni experienced the joy of using Yesod, Santos indeed is the +city in Brazil with the highest number of people who knows Yesod. We&#39;re committed to pushing Yesod forward through the local market, and maybe in the future, even beyond.</p><p>The book was published by &quot;Casa do Código&quot; and can be found <a href="https://www.casadocodigo.com.br/products/livro-yesod-haskell">here</a>.</p>/blog/2019/02/deprecating-googleemail22019-02-12T15:49:00-00:00Deprecating GoogleEmail2<p>As I&#39;m sure many readers are aware, Google+ is shutting down. This +<a href="https://github.com/yesodweb/yesod/issues/1579">affects Yesod&#39;s authentication +system</a>. In particular, +any users of the <code>Yesod.Auth.GoogleEmail2</code> module will need to +migrate.</p><p>Fortunately for all of us, Patrick Brisbin has written both <a href="https://www.stackage.org/haddock/lts-13.7/yesod-auth-oauth2-0.6.1.0/Yesod-Auth-OAuth2-Google.html">the +code</a> +for using Google Sign-in, but has also put together <a href="https://pbrisbin.com/posts/googleemail2_deprecation/">a great migration +blog post</a>. If +you&#39;re affected, please check that out for a relatively painless +migration.</p><p>Earlier today, I migrated Haskellers.com in two commits: the <a href="https://github.com/snoyberg/haskellers/commit/f77bba90d9684afb532639c68e64449523992535">first +did all of the +work</a>, +and the <a href="https://github.com/snoyberg/haskellers/commit/68198eb05389a96fe3250d2f3e179364531faca5">second made things more +future-proof</a>.</p><p>As you may be able to tell from the <code>GoogleEmail2</code> module, this isn&#39;t +the first time we&#39;ve had to migrate between Google authentication +APIs. Hopefully this one will stick.</p>/blog/2018/01/upcoming-yesod-breaking-changes2018-01-11T21:29:00-00:00Upcoming Yesod breaking changes<p>With all of the talk I&#39;ve had about breaking changes in my libraries, +I definitely didn&#39;t want the Yesod world to feel left out. We&#39;ve been +stable at yesod-core version 1.4 since 2014. But the changes going +through my package ecosystem towards <code>MonadUnliftIO</code> are going to +affect Yesod as well. The question is: how significantly?</p><p>For those not aware, <code>MonadUnliftIO</code> is an alternative typeclass to +both <code>MonadBaseControl</code> and the <code>MonadCatch</code>/<code>MonadMask</code> classes in +<code>monad-control</code> and <code>exceptions</code>, respectively. I&#39;ve mentioned the +advantages of this new approach in a number of places, but the best +resource is probably the +<a href="https://www.fpcomplete.com/blog/2017/07/announcing-new-unliftio-library">release announcement blog post</a>.</p><p>At the simplest level, the breaking change in Yesod would consist of:</p><ul><li>Modifying <code>WidgetT</code>&#39;s internal representation. This is necessary +since, currently, it&#39;s implemented as a <code>WriterT</code>. Instead, to match +with <code>MonadUnliftIO</code>, it needs to be a <code>ReaderT</code> holding an +<code>IORef</code>. This is just about as minor a breaking change as I can +imagine, since it only affects internal modules. (Said another way: +it could even be argued to be a non-breaking change.)</li><li>Drop the <code>MonadBaseControl</code> and <code>MonadCatch</code>/<code>MonadMask</code> +instances. This isn&#39;t strictly necessary, but has two advantages: it +allows reduces the dependency footprint, and further encourages +avoiding dangerous behavior, like using <code>concurrently</code> with a +<code>StateT</code> on top of <code>HandlerT</code>.</li><li>Switch over to the new versions of the dependent libraries that are +changing, in particular conduit and resourcet. (That&#39;s not +technically a breaking change, but I typically consider dropping +support for a major version of a dependency a semi-breaking change.)</li><li>A number of minor cleanups that have been waiting for a breaking +changes. This includes things like adding strictness annotations in +a few places, and removing the defunct <code>GoogleEmail</code> and <code>BrowserId</code> +modules.</li></ul><p>This is a perfectly reasonable set of changes to make, and we can +easily call this Yesod 1.5 (or 2.0) and ship it. I&#39;m going to share +one more slightly larger change I&#39;ve experimented with, and I&#39;d +appreciated feedback on whether it&#39;s worth the breakage to users of +Yesod.</p><h2>Away with transformers!</h2><p><i>NOTE</i> All comments here, as is usually the case in these discussions, +refer to code that must be in <code>IO</code> anyway. Pure code gets a pass.</p><p>You can check out the changes (which appear larger than they actually +are) in +<a href="https://github.com/yesodweb/yesod/pull/1466">the <code>no-transformers</code> branch</a>. You&#39;ll +see shortly that that&#39;s a lie, but it does accurately indicate +intent. If you look at the pattern of the blog posts and recommended +best practices I&#39;ve been discussing for the past year, it ultimately +comes down to a simple claim: we massively overuse monad transformers +in modern Haskell.</p><p>The most extreme response to this claim is that we should get rid of +<i>all</i> transformers, and just have our code live in <code>IO</code>. I&#39;ve made a +slight compromise to this for ergonomics, and decided it&#39;s worth +keeping reader capabilities, because it&#39;s a major pain (or at least +<i>perceived</i> major pain) to pass extra stuff around for, e.g., simple +functions like <code>logInfo</code>.</p><p>The core data type for Yesod is <code>HandlerT</code>, with code that looks like +<code>getHomeR :: HandlerT App IO Html</code>. Under the surface, <code>HandlerT</code> +looks something like:</p><pre><code class="haskell">newtype HandlerT site m a = HandlerT (HandlerData site -&gt; m a)</code></pre><p>Let&#39;s ask a simple question: do we really need <code>HandlerT</code> to be a +transformer? Why not simply rewrite it to be:</p><pre><code class="haskell">newtype HandlerFor site a = HandlerFor (HandlerData site -&gt; IO a)</code></pre><p>All we&#39;ve done is replaced the <code>m</code> type parameter with a concrete +selection of <code>IO</code>. There are already assumptions all over the place +that your handlers will necessarily have <code>IO</code> as the base monad, so +we&#39;re not really losing any generality. But what we gain is:</p><ul><li>Slightly clearer error messages</li><li>Less type constraints, such as <code>MonadUnliftIO m</code>, floating around</li><li>Internally, this actually simplifies quite a few ugly things around +weird type families</li></ul><p>We can also regain a lot of backwards compatibility with a helper type +synonym:</p><pre><code class="haskell">type HandlerT site m = HandlerFor site</code></pre><p>Plus, if you&#39;re using the <code>Handler</code> type synonym generated by the +Template Haskell code, the new version of Yesod would just generate +the right thing. Overall, this is a slight improvement, and we need to +weigh the benefit of it versus the cost of breakage. But let me throw +one other thing into the mix.</p><h2>Handling subsite (yes, transformers)</h2><p>I lied, twice: the new branch <i>does</i> use transformers, and <code>HandlerT</code> +<i>is</i> more general than <code>HandlerFor</code>. In both cases, this has to do +with subsites, which have historically been a real pain to write +(using them hasn&#39;t been too bad). In fact, the entire reason we have +<code>HandlerT</code> today is to try and make subsites work in a nicely layered +way (which I think I failed at). Those who have been using Yesod long +enough likely remember <code>GHandler</code> as a previous approach for this. And +anyone who has played with writing a subsite, and the hell which +ensues when trying to use <code>defaultLayout</code>, will agree that the +situation today is not great.</p><p>So cutting through all of the crap: when writing a subsite, almost +everything is the same as writing normal handler code. The following +differences pop up:</p><ul><li>When you call <code>getYesod</code>, you get the master site&#39;s app data +(e.g. <code>App</code> in a scaffolded site). You need some way to get the +subsite&#39;s data as well (e.g., the <code>Static</code> value in <code>yesod-static</code>).</li><li>When you call <code>getCurrentRoute</code>, it will give you a route in the +master site. If you&#39;re inside <code>yesod-auth</code>, for instance, you don&#39;t +want to deal with all of the possible routes in the parent, but +instead get a route for the subsite itself.</li><li>If I&#39;m generated URLs, I need some way to convert the routes for a +subsite into the parent site.</li></ul><p>In today&#39;s Yesod, we provide these differences inside the <code>HandlerT</code> +type itself. This ends up adding some weird complications around +special-casing the base (and common) case where <code>m</code> is <code>IO</code>. Instead, +in the new branch, we have just one layer of <code>ReaderT</code> sitting on top +of <code>HandlerFor</code>, providing these three pieces of functionality. And if +you want to get a better view of this, +<a href="https://github.com/yesodweb/yesod/blob/3e06942449cad0b52e218cb7e9f2c06b45b85e69/yesod-core/Yesod/Core/Class/Dispatch.hs#L38">check out the code</a>.</p><h2>What to do?</h2><p>Overall, I think this design is more elegant, easier to understand, +and simplifies the codebase. In reality, I don&#39;t think it&#39;s either a +major departure from the past, or a major improvement, which is what +leaves me on the fence about the no transformer changes.</p><p>We&#39;re almost certainly going to have a breaking change in Yesod in the +near future, but it need not include this change. If it doesn&#39;t, the +breaking change will be the very minor one mentioned above. If the +general consensus is in favor of this change, then we may as well +throw it in at the same time.</p> \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..8af10bc5 --- /dev/null +++ b/public/index.html @@ -0,0 +1,52 @@ + Yesod Web Framework for Haskell +
Yesod is a Haskell web framework for productive development of type-safe, RESTful, high performance web applications.

Yesod is a Haskell web framework for productive development of type-safe, RESTful, high performance web applications.

+ +
+

Why Yesod?

+
+
Turn runtime bugs into compile-time errors
+
Yesod believes in the philosophy of making the compiler your ally, not your enemy. We use the type system to enforce as much as possible, from generating proper links, to avoiding XSS attacks, to dealing with character encoding issues. In general, if your code compiles, it works. And instead of declaring types everywhere you let the compiler figure them out for you with type inference.
+ +
Asynchronous made easy
+
The Haskell runtime is asynchronous automatically. Instead of dealing with callbacks, you get to write normal code. By utilizing light-weight green threads and event-based system calls, your code automatically becomes non-blocking, without the pain.
+ +
Scalable and Performant
+
Yesod lets you write simple, high-level code, and gives you good performance. But when you need more, you can tune your compiled code for something even faster. Many of Yesod’s base libraries work exactly this way: providing a nice, safe interface for users while getting near-C performance with direct memory access. The GHC compiler ensures we get fast machine code at the end of the day.
+ +
Light-weight syntax
+
A lot of web development is boilerplate. Setting up routing tables, creating database schemas, and dealing with forms can all be long, repetitive code. Yesod’s has simple DSLs for templating, persistence, routing, and much more. But more importantly the DSLs are correct: they are all compile-time checked to get rid of the runtime bugs.
+
+ +

+ Learn more + or + get started. + You can also learn more about Haskell. +

+
+
+

Getting started

+ + +

Pronunciation

+

/jɪ'sod/ יסוד. yi as in yip, sod as in soda, stress on sod. + +

Book

+ + Developing Web Apps with Haskell and Yesod + +
+ +
\ No newline at end of file diff --git a/public/page/community.html b/public/page/community.html new file mode 100644 index 00000000..e58e8d83 --- /dev/null +++ b/public/page/community.html @@ -0,0 +1,49 @@ + Community +

Yesod has an active, vibrant community that is happy to help you get started. If you want to get involved, you should check out:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Stack Overflowfor help questions. Make sure to include the Yesod and haskell tags.
Yesod google groupfor Yesod specific discussions. There is also a very low traffic web-devel mailing list for more general haskell web development discussions.
IRC channel Freenode#yesodFor help, brainstorming, or just hanging out. Activity varies greatly - don't expect your burning issues to be answered or even anyone to be available to respond - please use StackOverflow and the google group if you must get feedback.
Github Issue Trackerfor reporting bugs. If you are unsure if you are encountering a bug, ask on the mail list or on IRC. If you have an installation error, please report on the mail list.
Yesod WikiThis is a great place to post documentation and tutorials.
Haskell Redditis a good place to share Yesod related blog posts.
+ +

If you want to participate with Yesod even more, we have two highly recommended approaches for beginners:

+ + + +

If you want to dig into the codebase itself, please check out the Yesod issue tracker and pick something to work on. If you have any questions, the mailing list and IRC are great places to get some help.

+
+ +
\ No newline at end of file diff --git a/public/page/quickstart.html b/public/page/quickstart.html new file mode 100644 index 00000000..1aa2b1d4 --- /dev/null +++ b/public/page/quickstart.html @@ -0,0 +1,19 @@ + Yesod quick start guide +
  1. Follow the FP Complete get started guide to get the Stack build tool.
    • On POSIX systems, this is usually curl -sSL https://get.haskellstack.org/ | sh
  2. Create a new scaffolded site: stack new my-project yesodweb/sqlite && cd my-project
  3. Install the yesod command line tool: stack install yesod-bin --install-ghc
  4. Build libraries: stack build
  5. Launch devel server: stack exec -- yesod devel
  6. View your Yesod site at http://localhost:3000/

NOTE: If you get an error message about GHC_PACKAGE_PATH at step (5), you +need to install a newer version of yesod-bin. Try running stack build +yesod-bin-1.4.11 and rerunning stack exec -- yesod devel. Also, if you choose +the "mini" scaffolding, yesod devel will not work.

NOTE: Stack version 2 or later is required to create the scaffold.

System libraries

Note that you will need the dev version of some system libraries to be +available for the above steps to work. For example, on Ubuntu, you may need to +run something like:

sudo apt-get install -y build-essential zlib1g-dev

If you're using a database, you'll likely need to install the system libraries +to talk to it. Some Ubuntu examples are:

sudo apt-get install -y libmysqlclient-dev
+sudo apt-get install -y libpq-dev

Learn more

Now it's time to start coding! You can play around with the code right now, or +if you want to learn more, check out these resources:

+ +
\ No newline at end of file diff --git a/public/page/screencasts.html b/public/page/screencasts.html new file mode 100644 index 00000000..23ab6fa8 --- /dev/null +++ b/public/page/screencasts.html @@ -0,0 +1,148 @@ + Screencasts +
+

Yesod 1.4

+ + +

Making a blog with Yesod, using Yesod 1.4 by Max Tagher. Source code for this screencast is available on Github.

+ +

Yesod 1.0

+ +

Yesod 1.0 Introductory Screencast from Michael Snoyman on Vimeo.

+ +

Older Screencasts

+ +

Some of the specific details shown here may be slightly out of date. The first 3 screencasts are for Yesod 0.5 and are mostly still accurate for Yesod 0.8. The Yesod book is up to date and a more canonical source of information. However, these screencasts still give a great overview in video form.

+ +
+
+

Yammer (Twitter clone)

+ +

Introduction to Yesod Web Framework 0.5

+ +

Hello World

+
+ +
+

Blog, part 1

+
+

Blog Tutorial - Part 1 - Yesod Web Framework 0.4.0 from Michael Snoyman on Vimeo.

+

Here's the source code:

+ +
{-# LANGUAGE TypeFamilies, QuasiQuotes, GeneralizedNewtypeDeriving #-}
+    import Yesod
+    import Database.Persist.Sqlite
+    import Data.Time (Day)
+    
+    mkPersist [$persist|
+    Entry
+        title String
+        day Day Desc
+        content Html'
+        deriving
+    |]
+    
+    data Blog = Blog { pool :: Pool Connection }
+    
+    mkYesod "Blog" [$parseRoutes|
+    / RootR GET
+    /entry/#EntryId EntryR GET
+    |]
+    
+    instance Yesod Blog where
+        approot _ = "http://localhost:3000"
+    
+    instance YesodPersist Blog where
+        type YesodDB Blog = SqliteReader
+        runDB db = fmap pool getYesod>>= runSqlite db
+    
+    getRootR = do
+        entries <- runDB $ select [] [EntryDayDesc]
+        applyLayoutW $ do
+            setTitle $ string "Yesod Blog Tutorial Homepage"
+            addBody [$hamlet|
+    %h1 Archive
+    %ul
+        $forall entries entry
+            %li
+                %a!href=@EntryR.fst.entry@ $entryTitle.snd.entry$
+    |]
+    
+    getEntryR entryid = do
+        entry <- runDB $ get404 entryid
+        applyLayoutW $ do
+            setTitle $ string $ entryTitle entry
+            addBody [$hamlet|
+    %h1 $entryTitle.entry$
+    %h2 $show.entryDay.entry$
+    $entryContent.entry$
+    |]
+    
+    withBlog f = withSqlite "blog.db3" 8 $ \pool -> do
+        flip runSqlite pool $ initialize (undefined :: Entry)
+        f $ Blog pool
+    
+    main = withBlog $ basicHandler 3000
+
+

Blog, part 2

+
+

Blog Tutorial - Part 2 - Yesod Web Framework 0.4.0 from Michael Snoyman on Vimeo.

+

Here's the source code:

+
 
+{-# LANGUAGE TypeFamilies, QuasiQuotes, GeneralizedNewtypeDeriving #-}
+import Yesod
+import Yesod.Helpers.Crud
+import Database.Persist.Sqlite
+import Data.Time (Day)
+ 
+share2 mkToForm mkPersist [$persist|
+Entry
+    title String
+    day JqueryDay Desc
+    content NicHtml
+    deriving
+|]
+ 
+instance Item Entry where
+    itemTitle = entryTitle
+ 
+data Blog = Blog { pool :: Pool Connection }
+ 
+type EntryCrud = Crud Blog Entry
+ 
+mkYesod "Blog" [$parseRoutes|
+/ RootR GET
+/entry/#EntryId EntryR GET
+/admin AdminR EntryCrud defaultCrud
+|]
+ 
+instance Yesod Blog where
+    approot _ = "http://localhost:3000"
+ 
+instance YesodPersist Blog where
+    type YesodDB Blog = SqliteReader
+    runDB db = fmap pool getYesod>gt;>gt;= runSqlite db
+ 
+getRootR = do
+    entries gt; do
+    flip runSqlite pool $ do
+        initialize (undefined :: Entry)
+    f $ Blog pool
+ 
+main = withBlog $ basicHandler 3000
+
+
+
+
+ +
\ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..7d329b1d --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +User-agent: * diff --git a/public/static/7of9.jpg b/public/static/7of9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..74f83752f198bf7943826f416d62f90e1ed36b33 GIT binary patch literal 44955 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

s5e0a@=O1}Sw#(6e^lo9UAl}@( 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 ZfsD+icM{SjThW061R>JnDpVs0|JlE>?0^6O literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/2011-03-17/extra-large.png b/public/static/benchmarks/2011-03-17/extra-large.png new file mode 100644 index 0000000000000000000000000000000000000000..10f9e93ae11c3aee09ba05c8f45d4d1763829a2d GIT binary patch literal 16446 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 literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/bigtable/cgipackages.png b/public/static/benchmarks/bigtable/cgipackages.png new file mode 100644 index 0000000000000000000000000000000000000000..735780546088ccf615079199c6eb3c734c906c90 GIT binary patch literal 13856 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 literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/bigtable/fastcgi2.png b/public/static/benchmarks/bigtable/fastcgi2.png new file mode 100644 index 0000000000000000000000000000000000000000..3d41e8b34971442cceca100a99e51bdee31c074a GIT binary patch literal 12385 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 Y-@7f_E{j6{bpWQVp{HJ?dgI=I0Zk{n@Bjb+ literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/bigtable/rawcgi.png b/public/static/benchmarks/bigtable/rawcgi.png new file mode 100644 index 0000000000000000000000000000000000000000..4aa044e2e921418bdda64de503ef8c5c8a41b976 GIT binary patch literal 13771 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 literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/bigtable/standalone.png b/public/static/benchmarks/bigtable/standalone.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df3d72af0a086129dc4b03746f84bc28bb2150 GIT binary patch literal 12428 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 literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/warp03/pong.png b/public/static/benchmarks/warp03/pong.png new file mode 100755 index 0000000000000000000000000000000000000000..7cb335fa5c2e885f7d1c0edb559d31bf2181f404 GIT binary patch literal 6216 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 literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/warp03/static-file.png b/public/static/benchmarks/warp03/static-file.png new file mode 100755 index 0000000000000000000000000000000000000000..21efa9a127528a3b3e39fefbff7e99603f713193 GIT binary patch literal 6913 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 literal 0 HcmV?d00001 diff --git a/public/static/benchmarks/warp0321.png b/public/static/benchmarks/warp0321.png new file mode 100644 index 0000000000000000000000000000000000000000..0339fadebe8d838d8f67f35f07ef0dd95fda60b4 GIT binary patch literal 17546 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 F{{#IoJVgKi literal 0 HcmV?d00001 diff --git a/public/static/blog.png b/public/static/blog.png new file mode 100644 index 0000000000000000000000000000000000000000..e8147d58e9f6cc38e786a0f0ab8a88baa8a6236f GIT binary patch literal 890 zcmV-=1BLvFP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyY- z5-<$#+lJ!+00R0+L_t(I%T1H-YgKg^z(3FPJ?GrByS0s(BbdXCLZTvnEFr>*S(Y$* zqgzogYDM8fNJxkhi3lNrAZTxTAw&tqkTNQW5F#t2#DZGb<`(EKQq87wyLac@@2_6m z75)LA&+~lXd1N2Rlf%7x@+>`^Wy#GV5m7;+2NhHeqJV;m5Cw{cn_0YE9z6Q--^b+i zaPRQ-*HtHCX;A|v6Ou|o$Aiy7;}Y)I&k?(X$#c{QCK1F4D}^#Od1XWZ0Nrp8<(KcV^7;_A@c7d4$Fp1T zg@blQMFd%@wfL@n=4h1;bt^%$9?n+ae2O#qOGPYM_-|wo5eT5 z*??Pxg98FM?`GtO-MzybZ|j~&#bK{5BOC4`RJy63If1K}cqh1PC$GX;PflHm5z}_I zeztFo`K@TO$;L`gI6xgxPCjL71FUH|xZYgpx QE&u=k07*qoM6N<$f`SE=qW}N^ literal 0 HcmV?d00001 diff --git a/public/static/js/modernizr.js b/public/static/js/modernizr.js new file mode 100644 index 00000000..76dc0298 --- /dev/null +++ b/public/static/js/modernizr.js @@ -0,0 +1,4 @@ +/* Modernizr 2.0.6 (Custom Build) | MIT & BSD + * Build: http://www.modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-iepp-cssclasses-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load + */ +;window.Modernizr=function(a,b,c){function H(){e.input=function(a){for(var b=0,c=a.length;b",a,""].join(""),k.id=i,k.innerHTML+=f,g.appendChild(k),h=c(k,a),k.parentNode.removeChild(k);return!!h},w=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=C(e[d],"function"),C(e[d],c)||(e[d]=c),e.removeAttribute(d))),e=null;return f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),x,y={}.hasOwnProperty,z;!C(y,c)&&!C(y.call,c)?z=function(a,b){return y.call(a,b)}:z=function(a,b){return b in a&&C(a.constructor.prototype[b],c)};var G=function(c,d){var f=c.join(""),g=d.length;v(f,function(c,d){var f=b.styleSheets[b.styleSheets.length-1],h=f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"",i=c.childNodes,j={};while(g--)j[i[g].id]=i[g];e.touch="ontouchstart"in a||j.touch.offsetTop===9,e.csstransforms3d=j.csstransforms3d.offsetLeft===9,e.generatedcontent=j.generatedcontent.offsetHeight>=1,e.fontface=/src/i.test(h)&&h.indexOf(d.split(" ")[0])===0},g,d)}(['@font-face {font-family:"font";src:url("https://")}',["@media (",o.join("touch-enabled),("),i,")","{#touch{top:9px;position:absolute}}"].join(""),["@media (",o.join("transform-3d),("),i,")","{#csstransforms3d{left:9px;position:absolute}}"].join(""),['#generatedcontent:after{content:"',m,'";visibility:hidden}'].join("")],["fontface","touch","csstransforms3d","generatedcontent"]);r.flexbox=function(){function c(a,b,c,d){a.style.cssText=o.join(b+":"+c+";")+(d||"")}function a(a,b,c,d){b+=":",a.style.cssText=(b+o.join(c+";"+b)).slice(0,-b.length)+(d||"")}var d=b.createElement("div"),e=b.createElement("div");a(d,"display","box","width:42px;padding:0;"),c(e,"box-flex","1","width:10px;"),d.appendChild(e),g.appendChild(d);var f=e.offsetWidth===42;d.removeChild(e),g.removeChild(d);return f},r.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},r.canvastext=function(){return!!e.canvas&&!!C(b.createElement("canvas").getContext("2d").fillText,"function")},r.webgl=function(){return!!a.WebGLRenderingContext},r.touch=function(){return e.touch},r.geolocation=function(){return!!navigator.geolocation},r.postmessage=function(){return!!a.postMessage},r.websqldatabase=function(){var b=!!a.openDatabase;return b},r.indexedDB=function(){for(var b=-1,c=p.length;++b7)},r.history=function(){return!!a.history&&!!history.pushState},r.draganddrop=function(){return w("dragstart")&&w("drop")},r.websockets=function(){for(var b=-1,c=p.length;++b";return(a.firstChild&&a.firstChild.namespaceURI)==q.svg},r.smil=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"animate")))},r.svgclippaths=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"clipPath")))};for(var I in r)z(r,I)&&(x=I.toLowerCase(),e[x]=r[I](),u.push((e[x]?"":"no-")+x));e.input||H(),A(""),j=l=null,a.attachEvent&&function(){var a=b.createElement("div");a.innerHTML="";return a.childNodes.length!==1}()&&function(a,b){function s(a){var b=-1;while(++bzxTeA6WKkR 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 literal 0 HcmV?d00001 diff --git a/public/static/logo-home2-no-esod-smaller1.png b/public/static/logo-home2-no-esod-smaller1.png new file mode 100644 index 0000000000000000000000000000000000000000..67349716ac8a7c7509da9ef5181b664e89a141da GIT binary patch literal 11360 zcmV-mET7YfP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z001G2Nkl{U#F-I4GxK@J z1}KCQ2`I`4>O1y=<1pi^lt4lY3B4LgLTG^`q=)pv?f2}pp7)Qn_CEWZd+yCmPcVzm zCgdjUv(MhY{XDbue%o(35?uf!7V_fG1~wa(rpF z+cL(q`t0qpl4vDf0=^o-quH|-+4Koi)c_|0_XD#779@O$u=?ZYd?E*pNccNqzz8Cy z5i+Xa6~*JdHz=n94`%1z3F{Ex>j=k-7+1e;iSI-Ji>jjPQSZZdsDoxcuxeXhl!#C& zl~}WSHIF^=V6ZmZd42ip_mcp3$1gS-_;yRc6mL%bodQ)v1XcC}y#hGPCP}(-#fv=l z=);_H>O1J}>TSd6tu1dxa=fF|5@?>)vloOAoB_g4ZR*aAzqE!H>@ zIoT!&BH`L42fLseyt?-u^@@`eb*}j4pC5s<&A?KwROs#P<(8ZN$l5h)=M|AEV0hrAz4P=_W}M(lnh5Tmt+SD2o^* z1CEKnsavM_rh+vd=Ti`aPt!BJcmD?ba0dhC!(zr=D3!`os?{BKL+k;E9+UmIi{}5MA~G@u zO?;agO;7f-_;(b$kPSG4pZmzjqqlU0syRl^teb0RHWq&tjT>_5^U{03IvRWf>C> zL5$QbQM3{w90qckDyCX(W6^^7bacE#k|Y@@$00hu!BR#3V=x%87ztpqk`NwLutqj1 zVUy%g5xEp-+YvWNM9#IgbfQfXtSw=a5=ki`u?dMu!nH~g?n&zhHx5|1)o-@7BuT>4 zvu81F%I!S+%u}?tkHA`6qYf+=OdNia6~C~)=;xHo5rG5>0k0AfijiERW) zb`(8#;P8s7{txi`t#L)xTH4!3uxjNBuKm@|c<}yvNlGQE)i!qKY6Y6k_=c=QHQWUq z8a$aL%3PQT|P{e%4+N5G_a7&3zuq1V$NRvq5v+v?imo7qeJdkeb1&kayijJ3F;_>N^ za>uQ=(AV2r53P6dDsr0QS`JtdIjv5J>&(5G$v6H?039VFhXRv~F==ma4}0ykHwPSe z5Y9QTsN^A*h6KrC! zVxvp0-w#p2YXFLO4)0Q&Pt`m3bzth2>MbI|1NTj1_N*Dqn(=tf_jZNViqjO=f2eXd zXd!geK?P$eMq1&fehm0~V+?)0z1)85Ev#O(l2XtT7JBa|c;{WX;o-)IkJ3hwD${hZ z7%K&9lY9t~&u@qO@;+ni7p+MU8;P&jf*X?D`ewR}ciG`^we))6H=8Ycm&+Jq=`1C)qdeurs?X}kqCAwSco(iIzenXY@IaqScXbV?Wa8m(10x}Lb&L#;iOKMoLME;C8eZj8m+ zNc^KU#7GWkRmJ1twTEAwt1Sh+aC5XIRi#p?uyVzV%zS)0zyA48STKJc?d|O(Nol7L z-7R%bMK>h^rxn7eg2$2pM7ADrDFSZwp8+q7R<*O!(pSxM2o^Kgb$0zzJ~Bn zjF`+x)jvq0*r?$WJc@V$bYE3}0LsK|B>G*HB&AO!N#>T4APb0TkQc)Hqd1@9Qjhu? zX>j$db7v8XD)DLBGOoptF zgPcu_2A+BP$w+DqpbNZnw7#daUSY*WLEZ1T+8-t8!HUj4$Mfh5(KGublSMDgry8X1R8_}2 z=N5YBaH+$m4$aQ51GSt$iY+D*Rb!K}B60~Ko4naBT0}0eHhGgVNp58{{kl#it z_Mobi%N1;r@XS+Ba_4Q6S-fZgs-AKrz`N7h6sIY!$AMEM0HB3sY1D6G z)k8fzg;# z{4Gv>$R1~>&n!;NM-?;WAq+27#F$HI0`(@={zl9MYm#jI(?+gK}82(JTR!r{_% z3Ep2YK*WputyC`Kz2~XfPjJt)DZDUmE^Y1Y*^9#Nx;ED?J`DWdIw{u8CBGrhUW+X3v_z@@2WTsj2suI+rf>K2*R3;P^&v zQy|BN;HE;ToM4mEabnB_L`?qKg;a{#Hcr z-t)rzdBxE48TD?wFIpS#3dC5OT%ZIYE*It!Y?7R5t$n*qN+d}MlO$2sF{jz2bWTos zqlhO`NR}4EYxQ|DG92y};O9a9ud;N>A|8I=ULJeoLA>`QNs`qMy=>R!gxt-m7kEM7 zL!j+N47N-$42Y^QBK#G|%}HW6ELyk#=TgQVbu`X7fZ2+^T8ue?NP5Kzs5B-IV~Rmy z>Z1N>Qc)NKdXpH_Z%ZXaWM47%ch;818dD14wM_*Vjk2t&JVhbo4TCPnqMt3T1ko+YB1t4*Y#E z5uX70jEG>ZW%<&jbar-RxvA&WyDPnSKIb5In35>gvVN}I#$7$FAo?R;!WN%79#j_S_=D%A>)KKvkao_U(~_ICc#*0ylI8x=iQ zkV&W?m7908(3Vmp#h@bKT`a`bvcM$@50%T6g)hu|o<$2@;MK2vJv}|$sOoQ>OW%jc zS)t#^Acj7(Rnci#kh zt+hP%$iqDP(EU^@)$AS~JZx=f`@Hyl?6_ikwiRQ$PgQ>?A{U}Qmf6)G;y`|*XhVmC ze+4QR0sqw2-mbG|{+UBZk73lPy-3qqw|f6g_3k9^%pr)un~-WSnD7x&uUQrZ6(#Wr z-gwj~VBTH-?7E^{kHiaZ$!BgUMoqyjT}CLnM?`q~iCG1SfL8+l1C;UvE!zP?xe*^= z$}KQHUc+t$8h~qnccJ>W(9;sgWNT5Y5T!^)ZuJYQOfiZ5e{0vQVba9w`QV2?#)y$4 zQPnvvb>l_kCwPNT4I~b+g2WQgdU7`2`f%s5Y<#6sHa`xahULZA5V#A))iMwLqmr4%_) z9hwx!yJ7M-bOOrgQmMrHb!)l*-f6{x^xt^rCT3-oKIb=nKyE5eZkl>$L#E1RKPzP5 z3%R9`Wp}B=xf;%;YdroEpu1cyvvK1FRxDp$d=dT$2G>%NV1@;X&je;nRiUr00PAfjJ(GqM}Qu_AIAkd#U#)~;F2{CRU@_n`K9@5g(W zE{g(NP-nG}W+}E$-hKb8p5j~ip??CfCD#2Z+(as^4S zGRKO@g(6rQ0F!-|*~+8odW*oR-^2;Ptlcnr8nDKzGGq7!=`>o1(2;nSF$q0AJ=`{F zBCA%e$lSp!=hN?cmlk-3%R}U$TWzE@(TG;YYrW*bd53f9R8_qOLecm7b!%C%Y*_%X zss)(A&`QRrfAia_JnoC2BLvHOFj1N0jxy$uEF43?XcCJP}6$R)t& zBuVJ&>torHC7Iv{sz3A2-xd^N7kZ6wF$8b?jKLmA0WI|cadoJ7k;kO#)%){-eq#*F zmo8=MowrkMZ_lje--yJdXu)bVe#X`u1sC3Mf3)b9E@ty*8h;##kl~UFp_DQB3rRQTs@llV!iJb zzzkKTt*woR9=L~|p6(2AM}k~kyxxJNcWVi-3>7{B`d#3$-B{dfxjdmK{LeYJVW@(w z0x6Aet_WWM`FKcM86JJ;0g@!i9^7noZh}kGTu;gVsarJ}Bp6Y$aVB|zc$&t-_7vyR zsh~d&B3_l*Pt0W9x^=~T(&Zu?JalZWTYc1Bt~RUfQ-B}s=8Ev3Yn>_=hiGck-6btj z3|0cs8)J-N_7gL?ciNP^2TE0c>AjyETuS{WD}@`~5-{~=QzyQmz$$g=TJ`?o5G_?% zwq!9=@4OwCrkR!ddyvmGV52arN4MEg4ZBoW0j1C)*0v0P*~;t_kl;U z5PIQ)`DE!0!ZfT4T30I;%0sr*kxjB|u+&@yOy4s1w%dRPP%Z~%4xN`Gk2sK~erfm| z@S%*~%z65$dTvT}rdR#0_bv_*@~58*ll?D`cbf=cZ2?P(_q>D*$lJwq2bU6!fN0=h92fP)Y~!w_#QAi zDV12VcoBErHYsm76V+?bx+2<&2WzokEAkoVVh?$m;!?Lx)t^tS#B(lX?wmQ4%H_;L zeH!?OBKVX>%T?E=w!x8G<=mRR6s#N@cP3c2M;c+MRo+%s%{j-L-f}X=CU}=-cWl!Y z+r`{cY}!{%@xF$7cTHyYtJNxZ+%}o+p02Et5a2-IyUihP0F-Pu6CGCxn9({~+BrGW zWP6t+3UUa-RY3a|-n1^@TwqyRt1H z=&l76*{Oip0q#l80zOYcAsyI}IfRu_=EPC6uBWGm6W{nIjy>i$dV6~SMv9nA#h87< zv``r^47!p32J)*e{2E2Pt0B}mS1MH&EnL8DlW#$tYwE3*9sZ6hyKe31_OvvI#E#1H za6YC{$Wj2OL6cbOz2|kWe*^D2?R|NILN;Ve6#PP<38X4-K305ZW zEch|-fQV2kS6DD_Erk3>eX4I z^v{ny%$zyT6br=91HM_9t+SiPMlw{1=(mB6UB%J}aIpxlZdvxwT0W`&NBGU}fQjCx zw6(Qy`Wa_((7}h$-`59lqBY5R`COVLOgOI#iH(I-`4Gk2%A}p@+&6&@#u$3KyLsTg zdy1;>+ktJVrNal6KZNP#xVV{ zhl>T`RgkMiXs6f*#cm3kto$Z|Ct_v;a3Nbcc7I!f=1>WmOtq*d3`Tw%8s2f^>*6krv1X?%{b#Gj5(!k(5$Dd_(%5>I!1OGo#wqF@-N{=+f9;REhx3oEzJeJ z3{-o%yE*XqR&~p@^<|qo`e;F;RY6!2-BQ=C0-6H_xe912n)Jrq;+j$g zzF1*LzlQL7VSj%gM<06}M;>)FeSN)|wfYBZlMmaxN84hs;mU1s7mKWPvp(!GbS%{V zq^jQmD3waATep_Qix(BAQk@OFqwba%BEs91Bk4ldM=|Rje)}WB&V0LyP9x_bysl1u zH<21>c2Uappx0@1DTj?2!^n}N=;`SOoPs4fxE=hHa7d|F6@%DITX!xyODE`gTPosh56~P4av36f7bZXq z0vIjqB%9Z~It@du7xwqpc=KCN=9E+4S#(REw>J5B9Elc`U&45hBKgh7f)r-SIG^HN zIz8$URI62@W~a`O9S!j{Q(gEe4wpzKByL>gs+U-L-?V4a^5;ksF&vVa)gk-dM zHDaaSp0#6ins)>L(mE$Ih?U86OA4?Q^cy0$TCJab_T7&I4?2jxzP>QM)tJkSwMS$d zTUb*Mu^XZq4SRQoX1A=2)8U=_88Agv3CeE=Q*N6aC!*_~r@a$Evlnm$!d}JP%#h`q zjp21Ic3dt@)0D#wJDd-E=%e+tC; zlUr`Op7rb24x_MYQwr;@ymtKcZU$)NToE~`LBx~6Yh<*krBqdK0KE>Nx2K!ezWzi; zjT#jqCXrVdV=oH1rI^8vJGwQOJmfsayPWuX@4CJB7o)l?NfJ6bI+%3R^{ijJhVpK@ z+sUrHhRbQ*Ey9-@vcGJC%QUo@vM`3Y{b^d`HLrgIM;?6)y;47dWCNk%Z|QJA zv$uo+J|l|_`kN%b6on?P0OmNCGHS2AIsJ??acP=anZL0%`HV49i%Be&!Z7Hjb2nQV z=iF23{eaROChne+lJM{a#dR$vbXxvKHwa}T%1i>8Uen7a7t@e zG%WIa1L$vvR^WtJzcyM356Dr*+6mdPXp>lMwoiBQ+_4@cw4u1-<`x~^xeI{RrBaDS z3+Hp^t&@w_H5RxcP%kMIBgr1y(c(!X(Qyc%i(@VrY?Y?5%qkz;Xb+=NZcXRS>>os(Z{!X5?9c$)X3m(7t&dhC*89Gy!L1sz^xK>TB3%;)RFpbf^zfYau~pf$~B$;A1=vsHBh zK(*S&L-*ao%*UtKH&BHSi|~&PJVw|9n9cTDdKK{0aK*iI5Z+b4$vP-lw8dOFRc;1; zniW>P@5~Rf|Ni@D;qPdK%SFTvh>(gSTH{o!e7~i->f0X+#9vI361}}WEMK;?SR-=) z$ak9vSZhDN2OUiWm|~ItRNy~{EAG9CFbihj`9%w3U7MQhz0dg5a?p!J#P|00aO6?P z#Eya#LP^2|RE}vbeD>a>qS@wNv1BvuPV2H{WDV!swJ5h3W9aVg;;uVxW#!5hS+kpo zEdQ0QH3%Q;ef9t+rxkg}g65HvDjNvBXMMu~%_!jeQ8{f?d;O{7GS1jVd2Od^zc=Oo~xpAB2F#0u}gX(y|mC9w7E?LaN z7v?i^)Tqpg{WYOTyuotS@hyYy>id_w3O)4o`r>QYL~m3QEr12SN@z>1m&@krZZoTH zG;5Ja-3I(T;}VCBIh?k(cDlQ|0A3N(2;~gr>IE;M5IkrzHa~ZL7Ze}H&O4`su_Mbx zgw?B7a{oP3X|ETEA4up=ZXZPE+(Tz*k}-X4hKFFW#aI^Yhf#z zA;!3>ssUCJMvJ5_O*#0GLwV~-r(}S!z?TRcX{{+*67@c8cp(Nz>r$TAIKm!Oy#W6m zlx0YgglTu)PDjT}#c5Ul2RMPa15AGH4cEN~HG=_16$APbVRZiL;ecjr1kEE9dmr-Y zlABx=!XUxfMa`yA?^}+DxSmh|l@nekp(VF@;)B*LMC`odXASH1^@s6xErd1000U(X+uL$P-t&- zZ*ypGa3D!TLm+T+Z)Rz1WdHz3$DNjUR8-d%htIutdZEoQ0#b(FyTAa_dy`&8VVD_U zC<6{NG_fI~0ue<-nj%P0#DLLIBvwSR5EN9f2P6n6F&ITuEN@2Ei>|D^_ww@lRz|vCuzLs)$;-`! zo*{AqUjza0dRV*yaMRE;fKCVhpQKsoe1Yhg01=zBIT!& zC1$=TK@rP|Ibo3vKKm@PqnO#LJhq6%Ij6Hz*<$V$@wQAMN5qJ)hzm2hoGcOF60t^# zFqJFfH{#e-4l@G)6iI9sa9D{VHW4w29}?su;^hF~NC{tY+*d5%WDCTXa!E_i;d2ub z1#}&jF5T4HnnCyEWTkKf0>c0%E1Ah>(_PY1)0w;+02c53Su*0<(nUqKG_|(0G&D0Z z{i;y^b@OjZ+}lNZ8Th$p5Uu}MTtq^NHl z*T1?CO*}7&0ztZsv2j*bmJyf3G7=Z`5B*PvzoDiKdLpOAxi2$L0#SX*@cY_n(^h55xYX z#km%V()bZjV~l{*bt*u9?FT3d5g^g~#a;iSZ@&02Abxq_DwB(I|L-^bXThc7C4-yr zInE_0gw7K3GZ**7&k~>k0Z0NWkO#^@9q0fwx1%qjZ=)yBuQ3=5 z4Wo^*!gyjLF-e%Um=erBOdIALW)L%unZshS@>qSW9o8Sq#0s#5*edK%>{;v(b^`kb zN5rY%%y90wC>#%$kE_5P!JWYk;U;klcqzOl-UjcFXXA75rT9jCH~u<)0>40zCTJ7v z2qAyk54cquI@7b&LHdZ`+zlTss6bJ7%PQ)z$cROu4wBhpu-r)01)S~6}jY?%U? zgEALn#wiFzo#H}aQ8rT=DHkadR18&{>P1bW7E`~Y4p3)hWn`DhhRJ5j*2tcg9i<^O zEt(fCg;q*CP8+7ZTcWhYX$fb^_9d-LhL+6BEtPYWVlfK zTBusSTASKKb%HuWJzl+By+?gkLq)?+BTu761jmyXF)a;mc z^>(B7bo*HQ1NNg1st!zt28YLv>W*y3CdWx9U8f|cqfXDAO`Q48?auQqHZJR2&bcD4 z9Ip>EY~kKEPV6Wm+eXFV)D)_R=tM0@&p?(!V*Qu1PXHG9o^TY0bZ?)4%0 z1p8F`JoeS|<@=<@RE7GY07EYX@lwd>4oW|Yi!o+Su@M`;WuSK8LKk71XR(_ zRKHM1xJ5XYX`fk>`6eqY>qNG6HZQwBM=xi4&Sb88?zd}EYguc1@>KIS<&CX#T35dw zS|7K*XM_5Nf(;WJJvJWRMA($P>8E^?{IdL4o5MGE7bq2MEEwP7v8AO@qL5!WvekBL z-8R%V?zVyL=G&{be=K4bT`e{#t|)$A!YaA?jp;X)-+bB;zhj`(vULAW%ue3U;av{9 z4wp%n<(7@__S@Z2PA@Mif3+uO&y|X06?J#o zSi8M;ejj_^(0<4Lt#wLu#dYrva1Y$6_o(k^&}yhSh&h;f@JVA>W8b%oZ=0JGnu?n~ z9O4}sJsfnnx7n(>`H13?(iXTy*fM=I`sj`CT)*pTHEgYKqqP+u1IL8No_-(u{qS+0 z<2@%BCt82d{Gqm;(q7a7b>wu+b|!X?c13m#p7cK1({0<`{-e>4hfb-UsyQuty7Ua; zOu?B?XLHZaol8GAb3Wnxcu!2v{R_`T4=x`(GvqLI{-*2AOSimkUAw*F_TX^n z@STz9kDQ$NC=!KfXWC z8h`dn#xL(D3Z9UkR7|Q&Hcy#Notk!^zVUSB(}`#4&lYA1f0h2V_PNgUAAWQEt$#LR zcH#y9#i!p(Udq2b^lI6wp1FXzN3T;~FU%Lck$-deE#qz9yYP3D3t8{6?<+s(e(3(_ z^YOu_)K8!O1p}D#{JO;G(*OVf24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV z0GgZ_00007bV*G`2iyf55g|4u##o*J02)0>L_t(|+U=crm|azs=znYPeQs5yf`mz# zhX4sJ-S%S=5m6gOAFZvzYqbZk+oyiKem?E?Az_dy1Vd!#_R*8WXRA+I`tdOZf&mi- z84V$XAtOUbDszYgQu94$@AdxJ=iGa44M|lcl}e!Z_wA%^4fpPI)?RD>_8Rt)$tQk- z9c^i~$`KQf=F^`!muj__|7=5Vs)}=tBuP-+`S1U&|9&uyBE~v#iHrZ+M69)9 zj1_CGSYt(ufFP)z0L%p59AzQ@nHY1GbBVD@1#2r}ofT`HSnI?XBgPm3BY>b<2>2Vo z!z2Ar5uwu51iMsibaG1g+NDfEP(imD=6H02&SBFob0 zsGbM-Q4;WBBIY{h5@TIbTS|&?0R$fi-Xlg46*P|IR^XRAB480=)oZV^aKSUIU$+(k znsT9!Rx_oqJK|cS?F~W|5MvrTA|MvTAi{e9M(CUOR&~{0r=u5U9*VDG@B$xSeIav1efHWn~%vz6nJfUiERyV zDG)}qHaLffFn``t+;#hHtY5b_pICPE*QNarI=cLN9biP{%qYkRN%adA8nwL8N1Y68 zvd+$LZ{JF_+RLGb9uDx9qMJp`pBQ6}7@OZblWTUq5+E2ch>>FH6V-5%s@^jSX8ViQ zCD%Gv!McRRIh;xIm8lMessRmw5IjE1f`;%-;L+jFD8sncajabNDl_lAjWw&=s8lMo z>mTj*zb-hxL6eL2eN~=_cRTa}>Hhzo(Xj4TiW4GuC3Q!&rCzPBhy_M5fv#ImWsK>nv7m zeF_yfULk0}XP)3Qe3q_8^|Ik?>s*r1)YQzAk3Y(7zx)M@UVNTvZ*QFRW6;bX-=C+d zH=#<=8-!Att#z|*5aF{RU$)lKw(>Pry!sL?EiC{mg7;T?pW%H*@X-`%P--54eEqXx zFwSC(BSMj#1{}5%uJv=qy7R3m;Ll=>ZP+4dP=b1bj{?qoO7Q+F;MF0meb$mB32iH1 z3=lO*T2qDna)Pyl+%p5L#*W_|rC?X#M4zF#P2B7WeJJ9%T1rbgHe&t-UdE

We Made ChatWisely With Haskell

February 17, 2021

GravatarBy Michael Litchard

Three years ago, I met fellow Haskeller Brian Hurt while working with Obsidian Systems on a Reflex project. Not long after, he started telling me his ideas about how to fix social media. These ideas intrigued me, one thing led to another, and we began building ChatWisely. We thought other Haskellers might like to hear about what we’re doing and how we use Haskell to do it.

ChatWisely is a member-supported mini-blogging social network currently in open beta. We envision a place where people connect in a spirit of comradery. We see an elevated discourse and a way to show bullies the door by providing a platform to debate safely. Here’s how we’re doing it.

Safety First

+Brian and I built several mechanisms to filter out people looking for a fight or to harm others. First and foremost will be the monthly subscription fee of one dollar. We think that will discourage a large portion of toxic people. Another is a sharable mute list that we believe will help mitigate the rest. And finally, in the event of a serious Terms of Service violation, we ban payment methods rather than just accounts.

Mini-Blogging

+The big idea here is that sometimes, a short post isn’t enough. Brian and I made a way to link that short post to a longer one. So the timeline looks something like Twitter’s, but with some posts that can expand to something more detailed. These can be connected to other people’s posts to create a continuity in conversation hard to come by on other platforms. So when another member’s post inspires you to write a longer one about your experience with the ghcjs ffi (for example), you can link your post to theirs.

Ownership of Your Timeline

+Members can organize their timeline and choose to what extent they follow other people’s posts. The typical mainstream social network requires that when you follow someone you must follow everything they post, or nothing. Sure, there are filtered word lists in some cases. But none of it seems to work quite right. Instead, we have groups called caboodles that members can use to decide where other people’s posts fit, and how to share their own. So say someone likes their uncle’s cookie recipes but not his political posts. They can follow one but not the other.

Geolocated Messaging

+One day this pandemic will be over and we’ll be there to meet that day. At that time, when a member’s movie caboodle wants to organize local screenings of the latest blockbuster from the House of Mouse they can make posts visible to people in their proximity. Perhaps you want to target local Haskellers to organize a meetup, or leave them a message that pops up when they’ve found the meeting place. Also, I think running a scavenger hunt with geolocated clues sounds like a hoot.

How It’s Built

+Brian and I rely heavily on the Haskell ecosystem to build ChatWisely. Haskell’s type system reduces errors and cognitive load. GHC is our pair programming partner that tightens the delivery cycle and lets us learn how to build ChatWisely while we’re building it. Refactoring is a breeze, and unit testing is constrained to the I/O edges of the system, which means we spend less time on that and more time building the product. Here’s the principal tools we rely on to get the job done. +

+

Ghcjs

The fact is we all hate javascript. The problem is, we can’t build web apps without it. Ghcjs lets us deal with the realities of building a product that uses a web browser for a client. The FFI lets us wrap our hand-written javascript in Haskell which helps to keep that part of the codebase pretty small.. We especially love what is built on top of that, Reflex-Dom.

Reflex-Dom

Reflex helps us build a system that needs to adapt to changes in data, requirements and platform. We’re learning how to build ChatWisely as we build it, and reflex keeps up with our changing ideas on how to do that. Our first app store product will be a PWA, delivered with reflex.

Servant

Servant delivers the API, and requires us to separate definition from implementation. This helps us keep the backend from turning into a big ball of mud. We can auto-generate clients, which we currently use in unit testing. They even have a way to generate a reflex client, and we’ll be adding that in the near future.

Yesod

We use Yesod for marketing tasks, which largely require static pages. Currently it handles our landing page and unsubscribe mechanism for our emails. The Yesod Widget is a monoid, and therefore composable, which makes structuring html simple.

Three Reasons Why People Should Use ChatWisely.

+

We are member supported

We won’t have ads, which means we have no need to manipulate people’s timelines in order to serve those ads. Their timeline should be about conversations of interest.

We solve the tweet thread problem

Brian and I find tweet threads hard to follow. Our mini-blog looks like twitter in the sense that you get a timeline of short posts. However if a post is the beginning of something more developed, that message can open up to access it.

Keep the RealWorld conversation going

We have delayed the development of these features for obvious reasons. But one day we’ll be together again. By then we’ll have useful geo-location tools for conference attendees and speakers to continue the conversation.

What makes a weekend conference fun for me are the conversations in-between formal talks. I get all caught up in compelling conversations, and want to keep that going. We’ll have a way to do that, without having to know or remember anyone’s email address or phone number.

Conference speakers will often want to build on the momentum gathered after a successful talk. Brian and I think Twitter hashtags are the terrible but often only way to do this. We’ll have a way to use proximity and common interests to help build that momentum and keep everyone engaged.


We built ChatWisely as a response to the unpleasantness all too common on mainstream social networks. Depending on our membership for support creates the place we want to meet because ad revenue and data-mining motivates engagement, not conversations and connection. No one is fooled by what mainstream social networks call engagement because that looks to us like derailed conversations, confusing timelines we only have a shallow control over, and unsafe situations.

Brian and I love the daily experience of building ChatWisely, the Haskell ecosystem brings joy to the experience of running our startup. You can support us on patreon and should come by and test the beta. We look forward to hearing from you about any ideas or questions you may have.

Comments

comments powered by Disqus

Archives

+

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 Q@Ezm>kW!MY7B>m`FMRh#e*gdg literal 0 HcmV?d00001 diff --git a/public/assets/skinning-conduits/conduit.png b/public/assets/skinning-conduits/conduit.png new file mode 100644 index 0000000000000000000000000000000000000000..6264be5dd3752de027ad9cd8067372526fb4e1e0 GIT binary patch literal 9019 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 E4f%aXHUIzs literal 0 HcmV?d00001 diff --git a/public/assets/skinning-conduits/source.png b/public/assets/skinning-conduits/source.png new file mode 100644 index 0000000000000000000000000000000000000000..1de794836329f9cc6b3f65c1b89118a9690d5d22 GIT binary patch literal 7785 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 literal 0 HcmV?d00001 diff --git a/public/assets/streaming/pure-exceptions.png b/public/assets/streaming/pure-exceptions.png new file mode 100644 index 0000000000000000000000000000000000000000..6abaa2b856dc72503e132fddaee7f39d69cdc685 GIT binary patch literal 4513 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 J_4rxX{{ix!oy-6L literal 0 HcmV?d00001 diff --git a/public/assets/vegito-benchmark-2016-02-28.html b/public/assets/vegito-benchmark-2016-02-28.html new file mode 100644 index 00000000..01dc296d --- /dev/null +++ b/public/assets/vegito-benchmark-2016-02-28.html @@ -0,0 +1,1016 @@ + + + + + criterion report + + + + + + + +