Search code examples
haskellfunctional-programmingclash

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


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.


Solution

  • 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.