To make sure we're taking advantage of a solid structure to develop a more complicated application on, you'll create a project with Stack. You'll see how to structure your main module, library functions, and tests and look at how to develop, compile, and test the project.
- In this section, we're going to look at developing a word game. So in this section, we're going to look at creating a new project with dependencies and libraries using stack. We'll see how to print out data structures, we'll look at searching lists and transforming them with tools like map, and we'll have a look at unit testing with Hspec. What do I mean by creating a word game? Well, let's have a look at this grid, which might seem, on first sight, to be a jumble of random letters.
But if you look more closely at it, you'll see that some of these letters do, in fact, form words. And you might spot the word "Haskell." So, in fact, this is a grid full of the names of programming languages, and you'll see other ones. For example, there's "Perl," but written backwards. There isn't a programming language called "Lrep." We've also got words written vertically, like "Python," and just to make things a little harder for the person solving, and for us writing the code, we'll also have words written diagonally, like "Ruby." And we'll see how to both generate the grid and solve the game using Haskell.
In this first video, we'll look at creating a project with stack. We'll start off by setting up the project using stack new, and we'll see how we organize code, libraries, and tests. We'll see how to run the code in the interactive interpreter, and we'll see how to compile it using stack build. So as always, we're using the command stack. Now, stack has a sub-command, new, which allows us to create a project. We have to give it the name of a project.
We'll call it words. Now when we run that, we'll see that although it creates a new directory, it does complain that we didn't provide it a whole lot of metadata about our name and the license we want to put it under. And stack helpfully gives us a set of configuration that we can use and we can copy and add into our config.yaml into the .stack directory in our home directory.
So if we edit that file, then we can simply paste that set of configuration into here. We can remove the curly braces that were auto-generated, and then simply add our own details. For the copyright, I'm going to write the letters BSD, which is the open-source license that I will license this code under. So now, if we remove the words directory that was created the last time we ran stack, then we can re-run stack new words, and let's have a look inside that new directory.
Now we can run stack ghci, and that will compile the code stack created for us, including a Lib and a Main, and you can see that it's auto-generated a function main, which simply outputs the string someFunc, for some function. So let's have a look at the files that stack has created for us. It's created a Main.hs, a Lib.hs for library functions, and a Spec.hs for tests, and also a words.cabal, which contains information about how to build the project.
So if we look at the main.hs, we can see that this simply imports the library and then calls the someFunc function from it. So let's go ahead and look at that library. And this is how you declare a library module in Haskell, with the module keyword and the name, and then, within parentheses, a list of the exported functions and symbols. And then the where keyword, after which follow all of the symbols and functions that it defined in this module.
And someFunc itself is an IO function, it does input and output, and it returns no value. And all it does is puts out the string someFunc. Now we can also build this project. But, of course, the first time we run this nothing happens, and that's because by running stack ghsi, stack had already built a code and doesn't think there's anything else to do. If we make a small change, adding an exclamation mark, now we can see that stack does, in fact, build the code.
And it generates an executable called words-exe. If we run that path, we can see that it outputs the string someFunc. That's quite unwieldy, though, so again, if we see that the executable is called words-exe, we can execute that using stack exec, which runs a command in the context of the project. So let's look at the words.cabal, and in here you can see that all of the metadata that we added to our config.yaml has resulted in this information, and for example, it's generated a homepage which it is assuming our place in a github directory with my username and the name of the project.
Obviously you can change that to whatever you like. And also, if we scroll down, we can see that it's defined an executable called words-exe, so if we didn't like that name, we could rename that. So I'm going to simply rename it to words, and now, if I rebuild, you can see that it's generated an exe:words. If we look at the code that it's generated, we can see that it's a file of 1.4 megabytes.
When we previously used stack ghc to compile, we added the dash dynamic flag, and we can do exactly the same here by editing the words.cabal and updating the line that says ghc-options, so here we can add -dynamic, save, and build again, and at this point we'll see that it's generated a much smaller file. Again, this isn't very important while you're just beginning with Haskell, but it's important that you know how to do it.
So let's see how to test code. If we run stack test, we get the message "Test suite not yet implemented," which makes sense. So let's look at the test suite that Haskell generated for us. And you'll see, in fact, that the code here simply outputs that message. So a common way of creating tests in Haskell is to use the hspec library, Test.Hspec, so here I'm simply copying out from the manual page of the hspec library an example with the hspec function, the describe function, and "it," and these are basically from the BDD principles of testing, where we describe what we're testing and get particular statements about our code with "it," and then we give a set of assertions.
In this case, I'm asserting that someFunc "shouldBe" literal string someFunc. So what do all these dollar signs and "do" functions mean? Well, $ is a piece of essentially syntactic sugar. It's an operator which is the equivalent of placing the code to the right of it in parentheses, and if we substitute all the $ with parentheses, you'll see that the code begins to look a little bit more like Lisp.
We have a whole set of closed parentheses at the end. I'll revert back to that style just for now. We'll see many more examples of that in upcoming chapters. Now the do, on the other hand, indicates we're going to sequence things, so after this first "do," we will have one or more descriptions of things, we'll have one or more statements of "it," and we'll have one or more assertions about the output of a function and "it result".
So right now, we'll start off with a simple function. So let's look again at how someFunc is actually defined, and you'll see there that, in fact, someFunc has no return value, so it's impossible for its result to be that string. All it does is output the string to standard out. So instead, we'll create a string called someString, and we can do a little refactor to make the function return that string. And, of course, because we've created a new symbol, if we want our test to know about it, we'll have to add it to the exports.
So now we can modify our test here to assert the value of someString. So let's try to run that test. And, of course, we get a failure here, and the reason is that stack couldn't find the module Test.Hspec. Just because we imported it doesn't mean it knew where to get it. So we need to tell stack about it, which we do in the words.cabal file. We're going to add the package name, which is hspec, to the section about the test-suite.
And now, if we run stack test again, we'll see that stack downloads not just the hspec library, but also a whole bunch of other libraries. It resolved all of the dependencies: async, primitive, and so on, and is downloading and compiling each of those within the context of this stack project. And once that's done, it will then go onwards to try to run the tests using these newly installed libraries.
And unfortunately, that still didn't quite work because the test can't find the symbol someString. We've made a mistake. Let's have a look again at the test code. And it looks like we forgot to import Lib, so let's add that import statement and rerun stack test. And this time, we can see that we got a test pass, and here we have diagnostics based on the "describe" and "it" statements, and all of our examples succeeded.
And I mentioned before that we could sequence multiple tests. Let's try doing that. As we only have one symbol, we'll do two assertions, one of which we know will fail. If we rerun that test, we'll see that we do, in fact, get a failure and here we get some useful diagnostics about what stack was expecting and what it actually got. In the next video, we'll look at how to set up the grid for this word game.
Note: This course was created by Packt Publishing. We are pleased to host this training in our library.
- Discovering Haskell with GHCI
- Haskell datatypes and functions
- Using higher order functions for data manipulation and code reuse
- Editing Haskell source code
- Creating a project with Stack
- Writing and conducting tests