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.