K. A. Buhr's answer is entirely correct. I'm adding this answer as a complementary description of what's going on in terms of Haskell's layout rules, which may help reason it out when a similar problem happens in a different context.
Almost all of Haskell's syntax is completely insensitive to indentation, but there are a few constructs that use alignment to indicate the entries within blocks of statements or definitions. do
blocks are one such context. The rules are the same for all of them, though.
- Each block has an alignment position for its entries. The position is set by the first non-whitespace character following the keyword introducing the block (regardless of whether it's own the same line or a following line).
- A line indented to exactly the same alignment is the beginning of an "entry" in the block.
- A line indented more than the alignment has no particular meaning; it's just part of an entry that started on an earlier line.
- A line indented less than the alignment position indicates the end of the block (and is not itself part of the block, so it must be part of some enclosing syntax, which may or may not be an aligned block)
A consequence of this that may not be obvious is that when an entry in a block spans more than one line itself, every one of the continuation lines must be further indented than the first. If a continuation line was indented the same as the first, it would be taken as starting a new entry in the block instead of continuing an existing one, and if it was indented less it would be taken as indicating the end of the entire block.
How this applies to the OP's example is pretty straightforward.
spec :: Spec
spec = do
describe "productOneLine" $ do
let
inVector = Data.Vector.replicate 0 0
inInteger = 3
outVector = Data.Vector.replicate 1 0
in
it "must manage empty vector" $ productOneLine inVector inInteger `shouldBe` outVector
let
inVector = Data.Vector.fromList [2, 4, 5]
inInteger = 4
outVector = Data.Vector.fromList [9, 6, 1, 2]
in
it "must multiply a vector by an integer" $ productOneLine inVector inInteger `shouldBe` outVector
The first character following the do
keyword is the l in let
(on the next line). So we can immediately see that this do
block has 4 entries, starting with let
, in
, let
, and in
.
That's not what the OP intended; they wanted there to be 2 entries, each of which is a complete let ... in ...
expression. To make that happen, the in
keywords need to be further indented.
It does not matter that let ... in ...
itself is a construct that uses aligned indentation. There's nothing in the above rules that would require in
to be aligned with the let
. The alignment position is the first character of the first declaration inside them (the i in inVector
in each case), not the position of the l
and it applies only to the block of declarations, not to the in
part of the overall let ... in ...
expression.
And in fact the in
keyword of let ... in ...
is totally insensitive to indentation. It can go anywhere (so long as it doesn't fall foul of an alignment set by an enclosing block, as happened in the OP's example). You can do this1:
three = let x = 1
y = 2
in x + y
Or this2:
five = let x = 2
y = 3 in x + y
Either way layout rules aren't necessary to tell that in
keyword isn't part of the declaration block, only to tell where the beginning of each declaration in the block is.
1 There is only one constraint on the placement of the in
in this example. It can't go right at the beginning of the line, which is only because the global definitions in the module (spanning the entire file) actually are an aligned block! You just usually don't notice this, because it's conventional to use an alignment of zero indentation for this block.
2 My usual style is this:
seven
= let x = 3
y = 4
in x + y
which happens to work inside a do
block without adjustment, a bonus I had never noticed before.