haskellquickcheckhspec

Why cannot I get `where` to work in Hspec


I'm struggling with the semantics of where within do blocks, specifically with Test.Hspec. The following works:

module ExampleSpec where

import Test.Hspec
import Test.QuickCheck

spec :: Spec
spec = do
    describe "foo" $ do
        let
            f = id
            in
                it "id" $ property $
                    \x -> f x `shouldBe` (x :: Int)
    describe "bar" $ do
        it "id" $ property $
            \x -> x `shouldBe` (x :: Int)

This does not:

module ExampleSpec where

import Test.Hspec
import Test.QuickCheck

spec :: Spec
spec = do
    describe "foo" $ do
        it "id" $ property $
            \x -> f x `shouldBe` (x :: Int)
        where
            f = id
    describe "bar" $ do
        it "id" $ property $
            \x -> x `shouldBe` (x :: Int)

It fails with:

/mnt/c/haskell/chapter15/tests/ExampleSpec.hs:13:5: error: parse error on input ‘describe’
   |
13 |     describe "bar" $ do
   |     ^^^^^^^^

Am I doing something wrong or is this some kind of inherent limitation with where?


Solution

  • A where clause can only be attached to a function or case binding, and must come after the right hand side body.

    When the compiler sees where, it knows that the RHS of your spec = ... equation is over. Then it uses indentation to figure out how far the block of definitions inside the where extends (just the single f = id in this case). Following that the compiler is looking for the start of the next module-scope definition, but an indented describe "bar" $ do is not valid for the start of a definition, which is the error you get.

    You cannot randomly insert a where clause into the middle of a function definition. It only can be used to add auxiliary bindings in scope over the whole RHS of a binding; it cannot be used to add local bindings in scope for an arbitrary sub-expression.

    However there is let ... in ... for exactly that purpose. And since you're using do blocks under each describe, you can also use the let statement (using the remainder of the do block to delimit the scope of the local bindings, instead of the in part of the let ... in ... expression). So you can do this instead:

    spec = do
        describe "foo" $ do
            let f = id
            it "id" $ property $
                \x -> f x `shouldBe` (x :: Int)
        describe "bar" $ do
            it "id" $ property $
                \x -> x `shouldBe` (x :: Int)