[personal profile] horrorcheck
Today, I worked with Steve McCarthy on my Haskell game. We decided to implement a really simple feature: listing all the game files (files ending in "exp") in the current directory. I want to share how we tackled this problem because it shows a lot of the steps in learning how to reason about Haskell when you're a beginner.

I'm using test-driven development as much as I can in this game, so we started with a test. Since I can't figure out how to test modules in Main (it imports the Main module in the test-suite instead), I pulled the function into its own module, Init.

This file was created in the test suite:

module InitSpec where

import Test.Hspec
import Init

main = hspec $
    describe "listGames" $
        it "lists the game files in the given directory" $
            listGames "games" `shouldBe` ["demo.exp", "example.exp"]
 
And this file in the library:

module Init where

listGames :: String -> [String]
listGames = undefined

As expected, this compiled but the test failed. "Yay, failure!"

So the next step is to figure out to make the test pass. Off to Hoogle! Hoogle says we can use getDirectoryContents, which has the type FilePath -> IO [FilePath]. FilePath to list of FilePaths! That's a way better description of what we need to do. We're stealing it.

import System.Directory

listGames :: FilePath -> IO [FilePath]
listGames dir = getDirectoryContents dir

So, we have a list of files, now we filter them by extension:

import Data.List (isSuffixOf)

listGames :: FilePath -> [FilePath]
listGames dir = filter ("exp" `isSuffixOf`) (getDirectoryContents dir)
```

Wow, it was so easy!

...or not.

Init.hs:7:17:
    Couldn't match expected type `IO [FilePath]'
                with actual type `[[Char]]'
    In the return type of a call of `filter'
    In the expression:
      filter ("exp" `isSuffixOf`) (getDirectoryContents dir)
    In an equation for `listGames':
        listGames dir
          = filter ("exp" `isSuffixOf`) (getDirectoryContents dir)

Init.hs:7:46:
    Couldn't match expected type `[[Char]]'
                with actual type `IO [FilePath]'
    In the return type of a call of `getDirectoryContents'
    In the second argument of `filter', namely
      `(getDirectoryContents dir)'
    In the expression:
      filter ("exp" `isSuffixOf`) (getDirectoryContents dir)

TWO type errors. Shoot. Okay, let's look at the first one. The type we said we expected in the type signature was IO [FilePath], but the return type of "filter" here is [[Char]] (aka [String]). The whole type of filter is (a -> Bool) -> [a] -> [a]. All those "a"'s have to be the same, so since we gave filter a function of type String -> Bool, it wants to give back a [String].

What about the second error? It seems to be the first in reverse. It's complaining that since the first argument of filter is a String, the second should be a list of Strings, and instead it's an IO list of filepaths.

How do we fix this?

Steve and I were pretty blank, but since I've seen stuff like this before I had a vague idea I needed fmap? or mapM? or something? And my usual technique is to throw a bunch of ideas around in ghci and figure out later why one works and another fails.

So I tried mapM_, and it gave me a crazy error, so I tried fmap, and then it compiled... yay... But WHY did it compile?

Let's look at the types of the parts of the filter that we are absolutely sure of and that make sense:

The directory listing:
getDirectoryContents "." :: IO [FilePath]
FilePath is a type synonym for String, so this is the same as IO [String]

and the filter:
filter (isSuffixOf "exp") :: [String] -> [String]`.

One way to think of the problem were were having is that we needed to "get out of IO". But another way to think of it is that the filter needs to get IN. We need to match up the types. fmap did that somehow. What is fmap's type?

fmap :: Functor f => (a -> b) -> f a -> f b

What's a Functor? Who cares? I don't want to get sucked into category theory when I'm trying to program! Instead, let's look at the rest of it. The first part, in parentheses, is a function from one type to another type. The next type is f a, so the first type inside some context. The final type is type b in the same context.

So in our function, this is what fmap is doing:

([String] -> [String]) -> IO [String] -> IO [String]

Exactly what we needed!

So, the function compiles in ghci, and now we understand why, but does our test pass?

NO. The test doesn't even compile! What?

InitSpec.hs:9:39:
    Couldn't match expected type `IO [FilePath]'
                with actual type `[[Char]]'
    In the second argument of `shouldBe', namely
      `["demo.exp", "example.exp"]'
    In the second argument of `($)', namely
      `listGames "./" `shouldBe` ["demo.exp", "example.exp"]'
    In the second argument of `($)', namely
      `it "lists the games in the directory"
       $ listGames "./" `shouldBe` ["demo.exp", "example.exp"]'
 
That's because we wrote this expecting to get a list of strings, not IO. We need to change the t a bit.

main = hspec $
    describe "listGames" $
        it "lists the games in the directory" $
            listGames "./" >>= (`shouldBe` ["demo.exp", "example.exp"])

This is how you test IO in Hspec, you stick a ">>=" in front of the shouldBe and wrap the rest in parens. Dunno exactly why, but it works. Now, let's try again!

$ runhaskell InitSpec.hs      

listGames
  lists the games in the directory

Finished in 0.0006 seconds
1 example, 0 failures
 
Hooray!

Tomorrow I'll add more about actually slotting this into the game... Hint: there were more type errors. :)
From:
Anonymous( )Anonymous This account has disabled anonymous posting.
OpenID( )OpenID You can comment on this post while signed in with an account from many other sites, once you have confirmed your email address. Sign in using OpenID.
User
Account name:
Password:
If you don't have an account you can create one now.
Subject:
HTML doesn't work in the subject.

Message:

 
Notice: This account is set to log the IP addresses of everyone who comments.
Links will be displayed as unclickable URLs to help prevent spam.

Profile

horrorcheck

August 2015

S M T W T F S
      1
2345678
9101112131415
16171819202122
23242526272829
3031     

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Oct. 23rd, 2017 04:23 am
Powered by Dreamwidth Studios