I am quite new to Haskell, and I am currently doing a Clash project. I have struggled for few days to understand what these code means in Haskell:
One example is from the retro-clash book https://github.com/gergoerdi/retroclash-book-code/blob/master/src/serial/echo/src/Serial.hs
topEntity
:: "CLK" ::: Clock System
-> "RX" ::: Signal System Bit
-> "TX" ::: Signal System Bit
topEntity = withResetEnableGen board
where
board rx = tx
where
input = serialRx @8 (SNat @9600) rx
buf = fifo input txReady
(tx, txReady) = serialTx @8 (SNat @9600) buf
makeTopEntity 'topEntity
The first thing seems confusing to me is, why we can have board rx = tx
?
like if there is board = some-expression
it is more understandable to me, because the identification 'board' has just been mentioned in the previous context. But how is rx
also appear in the left of the equal sign? Is it related to pattern matching? (if is, how can rx
be matched? like in the definition of topEnity
there is no rx
mentioned?)
Also, I can not understand these three line of the inner 'where', the relation between different function names seems really complicated.
It might help to consider a simpler example. Assuming you've done enough Haskell programming to run into the map
function, you probably understand the following program to double the elements of a list:
doubleList :: [Int] -> [Int]
doubleList xs = map double xs
double :: Int -> Int
double x = 2*x
You may also know that doubleList
can be defined without explicitly referencing the argument list xs
, and it will still behave the same as the original:
doubleList :: [Int] -> [Int]
doubleList = map double
If double
is only referenced by doubleList
, it's possible to move its definition into a where
clause:
doubleList :: [Int] -> [Int]
doubleList = map double
where
double x = 2*x
On the other hand, if we thought that the definition of double
was too complicated, we might rewrite it in terms of some subexpressions defined in its own where
clause:
double x = two * input_number
where two = 2
input_number = x
or even:
double x = output_number
where two = 2
input_number = x
output_number = two * input_number
Combining these changes would give:
doubleList :: [Int] -> [Int]
doubleList = map double
where
double x = output_number
where
input_number = x
two = 2
output_number = two * input_number
which is similar, in structure, to the Clash example:
topEntity :: ...
topEntity = withResetEnableGen board
where
board rx = tx
where
input = serialRx @8 (SNat @9600) rx
buf = fifo input txReady
(tx, txReady) = serialTx @8 (SNat @9600) buf
So, in the Clash example, topEntity
is defined by applying withResetEnableGen
to a one-argument function board
:
board rx = ...
which is, itself, defined in terms of some subexpressions given in a where
clause.
The only additional wrinkle is that the subexpressions include some mutual recursion, so the result of board rx
is defined as tx
, which is part of the output of serialTx
applied to buf
, but buf
is defined in terms of both input
(which depends on the actual argument rx
passed to board
) and txReady
, which is another part of the output of serialTx
.
So, buf
is calculated based on txReady
, and txReady
is calculated based on buf
.
This is not something you typically see in conventional languages, where this kind of mutual dependence would most likely result in an infinite loop, but it's pretty common in Haskell.
For a non-Clash example, consider this function, which determines which elements of a list are even:
isEvenList :: [Int] -> [Bool]
isEvenList = map isEven
where
isEven x = check_x
where
check_x = e x
e = check o (True, False)
o = check e (False, True)
using the following helper function:
check :: (Int -> a) -> (a, a) -> Int -> a
check f (ifZero, ifOne) y = case y of
0 -> ifZero
1 -> ifOne
_ -> f (y-1)
Note that e
depends on o
and o
depends on e
, just like buf
and txReady
in the Clash example, but the program still works fine:
> isEvenList [1..10]
[False,True,False,True,False,True,False,True,False,True]
That's because e
and o
are functions, and check
ensures that they're called in a way that doesn't result in an infinite loop (i.e., because e
only calls o
under certain conditions and vice versa).
Something similar is going on with the Clash example.