Array indexing lens out of array and index lenses

487 Views Asked by At

This is a simpler version of Using lens for array indexing if both array and index are in State as I have resolved some issues. I'm not sure if I should delete the original or edit it in place.

Given

{-# Language TemplateHaskell #-}
{-# Language Rank2Types #-}

import Control.Lens
import Control.Lens.TH
import Data.Array

data M = M { _arr :: Array Int Int, _idx :: Int } deriving (Show)

$(makeLenses ''M)

I want to write a function

combine :: Lens' M (Array Int Int) -> Lens' M Int -> Lens' M Int

That takes arr and idx lenses and constructs a combined lens that can be used to read and write an element pointed to by idx. The lens I want exists:

comboGet :: M -> Int
comboGet m = _arr m ! _idx m

comboSet :: M -> Int -> M
comboSet m v = m { _arr = _arr m // [(_idx m, v)] }

combo1 :: Simple Lens M Int
combo1 = lens comboGet comboSet

I understand that comboGet and comboSet can in principle be rewritten to use solely arr and idx lenses.

My question is: what is the most idiomatic way to construct combo1 out of arr and idx?

2

There are 2 best solutions below

1
On BEST ANSWER

You could construct it like this:

combo :: Lens' M Int -- alias for 'Functor f => (Int -> f Int) -> (M -> f M)
combo f m = arr (\xs -> fmap (\r -> xs // [(i, r)]) $ f $ xs ! i) m
  where i = m^.idx

EDIT

The better Traversal version would look like this:

betterCombo :: Traversal' M Int
betterCombo f m = arr (\xs -> maybe (pure xs) 
                                    ((\r -> xs // [(i, r)]) <$> f)
                                    xs^? ix i) m
  where i = m^.idx

Which is equivalent to:

betterCombo f m = (arr . ix i) f m
  where i = m^.idx
1
On

It turns out that as ! is partial, Lens' is not the way to go and I should use Traversal' instead:

combine :: Lens' M (Array Int Int) -> Lens' M Int -> Traversal' M Int

comboGet and comboSet can be rewritten using lens combinators:

comboGet' :: M -> Int
comboGet' m = m ^?! almostCombo m

comboSet' :: M -> Int -> M
comboSet' m v = m & almostCombo m .~ v where

almostCombo :: M -> Traversal' M Int
almostCombo m = arr . ix (m ^. idx)

combo of @chaosmasttter can be written as:

combo2 :: Functor f => (Int -> f Int) -> M -> f M
combo2 f m = arr (\xs -> fmap (\r -> (ix i .~ r) xs) . f $ xs ^?! ix i) m  
    where i = m^.idx

It's Traversal signature will only differ by Applicative:

combo :: Applicative f => (Int -> f Int) -> M -> f M

Will Applicative help us make the result of getter optional?

As @chaosmasttter pointed out in his betterCombo, the key was to eta-expand lens composition to get access to m.