Tuesday, November 28, 2017

Yesod tutorial for beginners, haskell web programming

Well hello

So, I started to learn web programming with haskell and yesod. Yesod book was too hard for me to grasp and I couldn't find a plausible entry-level tutorial, that would not be written 5 years ago and could compile. So I took an article by  yannesposito and fixed it.

I saw some effort to fix the tutorial on the school of haskell here, but its formatting gave me an impression, that it is not maintained anymore.

Prerequisites: a basic understanding of haskell. If you lack it, I recommend you to read this.

1. Work environment setup

I use stack instead of cabal. I, together with yesod manual, really recommend you to use it. Not to mention, that using yesod you don't really have a choice. The only way to create a new project is to use stack  ¯\_(ツ)_/¯.

1.1 So, let's get us some stack!

It's as easy as running either one of the two.
curl -sSL https://get.haskellstack.org/ | sh
wget -qO- https://get.haskellstack.org/ | sh

I strongly recommend using the latest version of stack instead of just apt-getting it. Ubuntu repos often contain older and buggier versions of our favorite software.

[optional] Check what templates are available
$ stack templates

1.2 Generate a template project.

You can generate a yesod project only using stack. The init command has been removed from yesod.  Use yesod-sqlite template to store you blog entries (see "Blog" chapter). Of course, if you don't intend to go that far with this tutorial, you can use yesod-simple. So, let's create a new project called "yolo" with type yesod-sqlite.
stack new yolo yesod-sqlite

1.3 Install yesod

You should be able to run your  project, for this you have to install yesod. This takes about 20 min.
stack install yesod-bin --install-ghc

1.4 Build and launch

Warning, first build will take a looong time
stack build && stack exec -- yesod devel

And check your new website at http://localhost:3000/
(3000 is the default port for yesod).
For more detailed reference about setting up yesod look here.

My versions of stuff:
stack:  Version 1.5.1, Git revision 600c1f01435a10d127938709556c1682ecfd694e
yesod-bin version:
The Glorious Glasgow Haskell Compilation System, version 8.0.2

2. Git setup

You know it's easier to live with a version control system.
git init .
git add .
git commit -m 'Initial commit'

3. Echo

Goal: going to localhost:3000/echo/word should generate a page with the same word.

Don't add the handler with `yesod add-handler`, instead, do it manually.

Add this to config/routes, thus adding a new page to the website.
/echo/#String EchoR GET
#String is the type of the input after slash and haskell's strict types prevent us from getting SQL injections, for example.
EchoR is the name of the GET request handler, GET is the type of supported requests.

And this is the handler, add it to src/Handler/Home.hs.
getEchoR :: String -> Handler Html
getEchoR theText = do
    defaultLayout $ do
        setTitle "My brilliant echo page!"
        $(widgetFile "echo")

This tiny piece of code accomplishes a very simple task:
  • theText is the argument, that we passed through /echo/<theText is here>
  • for it we return a defaultLayout (that is specified in templates/defaultLayout.hamlet and is just a standart blank html page)
  • set page's title "My brilliant echo page!"
  • set main widget according to templates/echo.hamlet
 Also, remember that RepHtml is deprecated.

So, let's add this echo.hamlet to the <projectroot>/templates! As you can see it's just a header with the text that we passed after slash of echo/<word here>.
<h1> #{theText}

Now run and check localhost:3000/ :)

If you're getting an error like this
 Illegal view pattern:  fromPathPiece -> Just dyn_apb9
    Use ViewPatterns to enable view patterns
 Illegal view pattern:  fromPathPiece -> Just dyn_aFon 

then just open your package.yaml file, that stack has automatically created for you and add the following lines just after `dependencies:` section:
default-extensions: ViewPatterns

Else if you're getting something like this
yesod: devel port unavailable
CallStack (from HasCallStack):
  error, called at ./Devel.hs:270:44 in main:Devel
that most probably you have another instance of site running and thus port 3000 is unavailable.

If you see this warning
Warning: Instead of 'ghc-options: -XViewPatterns -XViewPatterns' use
'extensions: ViewPatterns ViewPatterns'

It's okay, so far stack does not support 'extensions' section in .cabal file. Catch up with this topic in this thread.

If you see this warning
Foundation.hs:150:5: warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘isAuthorized’:
        Patterns not matched: (EchoR _) _

That means, that you need to add this line to Foundation.hs
isAuthorized (EchoR _) _ = return Authorized 
All it does is grants permissions to access localhost/echo to everybody.

 4. Mirror

Goal: create a page /mirror with an input field, which will post the actual word and its palindrome glued together, as in book -> bookkoob or bo -> boob.

Add the following to config/routes to create a new route (i. e. a page in our case).
/mirror MirrorR GET POST

Now we just need to add a handler to src/Handler/Mirror.hs
    import Import
    import qualified Data.Text as T

    getMirrorR :: Handler RepHtml
    getMirrorR = do
        defaultLayout $ do
            setTitle "You kek"
            $(widgetFile "mirror")

    postMirrorR :: Handler RepHtml
    postMirrorR = do
        postedText <- runInputPost $ ireq textField "content"
        defaultLayout $ ($(widgetFile "posted"))

 Don't be overwhelmed! It's quite easy to understand.

And add the handler import to src/Application.hs, you will see a section, where all other handlers are imported

import Handler.Mirror

Mirror.hs mentions two widget files: 'mirror' and 'posted', here's their contents


<h1> Enter your text
<form method=post action=@{MirrorR}>
    <input type=text name=content>
    <input type=submit>

<h1>You've just posted
<p>#{postedText}#{T.reverse postedText}
<p><a href=@{MirrorR}>Get back

There is no need to add anything to .cabal or .yaml files, because stack magically deducts everything on its own :)

Don't forget to add the new route to isAuthorized like in the previous example!

Now build, launch and check out your localhost:3000, you must see something similar to my pics

stack build && stack exec -- yesod devel

And after you entered some text in the form, you should get something like this

5. Blog

Again, add Handler.Article and Handler.Blog to Application.hs imports.
This is contents of Blog.hs

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

module Handler.Blog
    ( getBlogR
    , postBlogR
    , YesodNic
    , nicHtmlField

import Import
import Data.Monoid

import Yesod.Form.Nic (YesodNic, nicHtmlField)
instance YesodNic App

entryForm :: Form Article
entryForm = renderDivs $ Article
    <$> areq textField "Title" Nothing
    <*> areq nicHtmlField "Content" Nothing

getBlogR :: Handler RepHtml
getBlogR = do
    articles <- runDB $ selectList [] [Desc ArticleTitle]
    (articleWidget, enctype) <- generateFormPost entryForm
    defaultLayout $ do
        setTitle "kek"
        $(widgetFile "articles")

postBlogR :: Handler Html
postBlogR = do
    ((res, articleWidget), enctype) <- runFormPost entryForm
    case res of
        FormSuccess article -> do
            articleId <- runDB $ insert article
            setMessage $ toHtml $ (articleTitle article) Import.<> " created"
            redirect $ ArticleR articleId
        _ -> defaultLayout $ do
                setTitle "You loose, sucker!"
                $(widgetFile "articleAddError")

Article.hs contents

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

module Handler.Article

import Import

getArticleR :: ArticleId -> Handler Html
getArticleR articleId = do
    article <- runDB $ get404 articleId
    defaultLayout $ do
        setTitle $ toHtml $ articleTitle article
        $(widgetFile "article")

Add this to conf/models

    title   Text
    content Html

This to conf/routes

/blog               BlogR       GET POST
/blog/#ArticleId    ArticleR    GET

And add these lines to src/Foundation.hs. This is a hack, but you cannot view the contents unauthorized, right? :) Drawback: all users on the internets will be able to see your post.

    isAuthorized BlogR _ = return Authorized
    isAuthorized (ArticleR _) _ = return Authorized

All done! What will you see: