3

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?

  • I should add I've tried playing around with `{}` and `;` to no avail – Ben Heilman Nov 30 '21 at 21:39
  • 1
    I love the way you use `where` as an expression. Many of us think `where` should be an expression and usable just like you did above. However, as the answers below explain, `where` is more restricted and can only come in particular places (ex. as part of a declaration). – Thomas M. DuBuisson Nov 30 '21 at 22:21

2 Answers2

3

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)
Ben
  • 68,572
  • 20
  • 126
  • 174
  • I did notice that without the `in` shadowing of assignments occurs for usage in the same indentation level. Makes sense, just a note – Ben Heilman Nov 30 '21 at 22:34
  • @BenHeilman This is a syntactic issue and is not really related to semantics. Semantics describes the meaning of syntactically valid code, and the code in the question is not syntactically valid. – David Young Nov 30 '21 at 23:24
  • @BenHeilman I'm not 100% sure what you mean by that. Bindings introduced in a `let` **statement** (in a do block, no `in` keyword) scopes over the entire rest of the do block they occur in (so everything following that is part of a construct starting on the same indentation, yes). Bindings introduced by the `let ... in ...` **expression** are scoped over the expression in the `in ...` part (but `let ... in ...` expressions only use indentation for the bindings block, not the `in ...` part). Either of them will shadow bindings from enclosing scopes, as will any method of binding a name. – Ben Dec 01 '21 at 03:03
2

This is a syntactic restriction in service of the scoping rules of a where block. Within a where block, values bound in a pattern match are in scope, and values defined in the where block are in scope for guards within that pattern match. As such, a where block must be attached to locations where a pattern match and guards at the very least could exist. This ends up being value declarations and branches of case expressions. In your second example you are trying to attach a where block to an arbitrary expression, which is just not what they're intended to do.

Carl
  • 26,500
  • 4
  • 65
  • 86