Search code examples
haskellhaskell-lenshaskell-optics

Haskell optics: Setter for several lists


I would like to set an item at position x between two lists, as if they were the same list. For example:

data Foo = Foo {
    list1 :: [Char],
    list2 :: [Char]}

foo = Foo ['a', 'b', 'c'] ['d', 'e', 'f']

setItem :: Int -> Char -> Foo -> Foo
setItem i c f = ???

For instance, setting element at position 5 would yield:

setItem 5 'X' foo 
==> Foo ['a', 'b', 'c'] ['d', 'X', 'f']

I thought I could use optics/lens for this. Something like (using Optics and Labels):

setItem :: Int -> Char -> Foo -> Foo
setItem i c f = set ((#list1 <> #list2) % at i) c f

But this doesn't work:

No instance for (Semigroup (Optic k1 NoIx s0 t0 v0 v0))

Solution

  • I've looked into how to do this with optics, and you need to combine the two lenses using adjoin, you can then get a traversal over the element with a particular index using elementOf.

    These can be combined as follows:

    I'm going to start by writing out the imports and data declarations in full:

    {-# Language TemplateHaskell #-}
    {-# Language OverloadedLabels #-}
    {-# LANGUAGE DataKinds #-}
    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE MultiParamTypeClasses #-}
    {-# LANGUAGE TypeFamilies #-}
    {-# LANGUAGE UndecidableInstances #-}
    
    import Optics
    import Optics.Label
    import Optics.IxTraversal
    
    data Foo = Foo {
        list1 :: [Char],
        list2 :: [Char]
      } deriving Show
    
    makeFieldLabelsNoPrefix ''Foo
    foo = Foo ['a', 'b', 'c'] ['d', 'e', 'f']
    

    Then the actual function looks like:

    setItem :: Int -> Char -> Foo -> Foo
    setItem i c f = set (((#list1 `adjoin` #list2) % traversed) `elementOf` i) c f
    

    Two things to be aware of is that correct usage of adjoin relies on the targets of both lenses being disjoint; you shouldn't use it with the same lens (or lenses with overlapping targets) as different arguments.

    And elementOf traverses the nth element of the traversal; it doesn't respect the indices of an IxTraversal, it takes any old Traversal and uses the position of each element within that traversal as the index.