Reputation: 351
I was thinking of putting helper functions that are only accessed by one function into a where
statement. It would look something like this.
f :: Int -> Int
f a = g a + 1
where
g :: Int -> Int
g a = 2 * a
(This example is just to illustrate the idea.)
To me the benefit of that would be that these helper functions can access all the arguments of the top-level function and that it is clear what purpose the helper functions serve.
Are there any problems or drawbacks with this approach?
Upvotes: 2
Views: 1298
Reputation: 8467
There are no drawbacks whatsoever to this approach — it is incredibly common, and precisely what where
blocks were designed for.
EDIT: See @luqui’s answer for a more detailed overview of some common ‘gotchas’ you need to watch out for when using functions in where
blocks.
Upvotes: 2
Reputation: 60463
I agree with @bradrn that this is common and idiomatic usage and you should not hesitate to use it. However there are some gotchas that you should know about.
When the type is polymorphic, you can run into issues with type variable scope.
f :: Num a => a -> a
f x = g x
where
g :: a -> a
g y = x + y
This will, surprisingly, result in a type error. If you remove g
's type signature it will work fine. That is because the a
in g
's type signature is a different a
than the one in f
's, so Haskell expects that g :: a -> a
should hold no matter what a
is, regardless of f
's signature. The workaround is either to remove the signature, or use the ScopedTypeVariables
extension to explicitly scope the outer type variable, using the following (imo un-intuitive) syntax:
f :: forall a. Num a => a -> a
f x = g x
where
g :: a -> a -- a now refers to the same as as in f's signature
g y = x + y
The other issue has already been pointed out by @amalloy in a comment, that the helper function can re-bind names that the outer function already bound. For example:
f x y = g y x
where
g x y = x ++ y
Calling f "foo" "bar"
will result in "barfoo"
because g
has rebound the variables x
and y
.
The final gotcha is that it's kind of awkward when mixing with do
notation. The following is a common pattern I come across:
main = do
[n, s] <- getArgs
go (read n)
where
go 0 = pure ()
go n = do
putStrLn $ show n ++ s
go (n-1)
In hopes that I could call ./Main 99 ' bottles of beer on the wall'
. Sadly, the scope of s
does not extend into the where
block, and we are forced to either make s
an extra argument of go
(which, in ugly real-world code, can lead to go
having quite a few arguments), or to use let
inside the do
block instead:
main = do
[n, s] <- getArgs
let go 0 = pure ()
go n = do
putStrLn $ show n ++ s
go (n-1)
go (read n)
But, despite its few gotchas, using where
blocks for a helper function that has access to some of the wrapping functions arguments is 100% Kosher.
Upvotes: 5