shouya
shouya

Reputation: 3083

How to combine the results of two simple lenses into one

Suppose I have a data type defined as below:

data Register = Register { _reg_h :: Word8
                         , _reg_l :: Word8
                         }
makeLenses ''Register

Now if I want to define a lens that focus from a Register to a Word16. This function should be some like below:

refer :: Lens' Register Word16
refer = do h <- reg_h
           l <- reg_l
           return $ (fromEnum h `shiftL` 8) .&. fromEnum l

(Word8, Word16, shiftL, and .&. are from Data.Word and Data.Bits, and I have enabled RankNTypes in the beginning of my source code.)

However this code simply doesn't work. I guess it might because the full Lens type definition has four type parameters and that would be more complex than a simple Lens' to act as a simple Monad.

So in what way can I achieve the effect described above?

Thanks.

Upvotes: 0

Views: 344

Answers (2)

Reite
Reite

Reputation: 1667

You cannot define a lens with do notation like that. A lens needs to contain both the notion about how to get a Word16 from a Register, and how to set a Word16 back inn. Your code only says how to get a Word16 from a Register. So it should rather be written as a straight function:

registerToWord16 :: Register -> Word16
registerToWord16 r = (fromEnum (view reg_h r) `shiftL` 8) .&. fromEnum (view reg_l r)

If you just want to get a word16 from a register, you can use this function with to from Control.Lens.Getter, to get a Getter you can compose with your lenses.

If you want to be able to go the other way too so that you can set a Register from a Word16, you probably want to write an Iso. To write an Iso you need to define a function that goes the other way:

word16ToRegister :: Word16 -> Register
word16ToRegister = ...

You can now use the iso :: (a -> b) -> (b -> a) -> Iso' a b function to create your reference.

refer :: Iso' Register Word16
refer = iso registerToWord16 word16ToRegister

An Iso is higher in the lens hierarchy than Lens, so if you compose it with a Lens you will get a Lens.

Edit:

So you clarified in the comment that you want a register to contain more fields. In that case you want a Lens. You can write it manually like András showed in his post, or you can use the lens :: (s -> a) -> (s -> a -> s) -> Lens' s a function to construct it from a getter and setter like this:

refer :: Lens' Register Word16
refer = lens get set
    where
        get :: Register -> Word16
        get = registerToWord16

        set :: Register -> Word16 -> Register
        set reg w16 = reg & reg_h .~ word16ToRegH w16
                          & reg_l .~ word16ToRegL w16

        word16ToRegH :: Word16 -> Word8

        word16ToRegL :: Word16 -> Word8

Upvotes: 3

Andr&#225;s Kov&#225;cs
Andr&#225;s Kov&#225;cs

Reputation: 30113

For an idiomatic solution using the lens library you should look at Reite's answer. Here I give more of a direct answer and show how to implement the lens you intended.

The definition of the Lens type synonym is Lens s t a b = Functor f => (a -> f b) -> s -> f t. Making a custom lens can be achieved by implementing a function with this type (specialized to some s t a b).

Generally, you implement a lens by finding some value of type a inside the context of type s, applying the a -> f b function to it, then wrapping the rest of the context around the resulting f b using fmap. A simple example:

-- we'd like to focus on the first element of a pair.
-- we change the type of  the first elem from a to a'.
-- thus the type of the whole pair changes from (a, b) to (a', b)

_1 :: Lens (a, b) (a', b) a a'
_1 f (a, b) = fmap (\a' -> (a', b)) (f a)
                     |              |
                     |              ---> apply f to the focus
                     |
                     --> wrap the result back

We can implement the lens for Register analogously:

refer :: Lens' Register Word16
refer f (Register h l) = fmap wrap (f val) where
    val    = fromIntegral (shiftL h 8) .&. fromIntegral l
    wrap n = Register (fromIntegral $ shiftR n 8) (fromIntegral $ 0xffff .&. n)

Again, we apply f to the value we want to focus on, then wrap the result back. The definition is only complicated slightly by the need to merge and split the numbers.

Upvotes: 3

Related Questions