Unable to understand the strange "where" syntax in Haskell / Clash

162 views Asked by At

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.

2

There are 2 answers

0
K. A. Buhr On BEST ANSWER

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.

0
Code-Apprentice On
board rx = tx

This defines a function named board with a single parameter named rx. The result of calling this function is the value of tx which is defined in the where clause.