3

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))
Joe
  • 1,479
  • 13
  • 22
cdupont
  • 1,138
  • 10
  • 17
  • 2
    This isn't a full answer, and it's using `lens` rather than `optics`, but you can do `set (indexing (each . traverse) . index 4) 10 ([1, 2, 3], [4, 5, 6])` which demonstrates the principle – Joe Sep 06 '22 at 00:03

1 Answers1

3

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.

Joe
  • 1,479
  • 13
  • 22
  • 1
    This is using `optics-0.4.2` and `optics-core-0.4.1`, which just happens to be the latest version on stackage right now – Joe Sep 06 '22 at 10:33
  • Thanks a lot! It works well. I tried with `(#list1 'adjoin' #list2) % ix i` but that didn't work: it was modifying both lists at position `i`. The role of the `traversed` is not clear for me. – cdupont Sep 06 '22 at 15:09
  • I wondered what if list2 is like this: `list2 :: [(Char, Char)]`. Can we do `(#list1 'adjoin' (#list2 % traversed % _1))` ? – cdupont Sep 06 '22 at 15:15
  • `ix 5` is a Traversal from a List, to the 6th element of that list. Whereas `elementOf` takes a traversal as an argument, and returns a new traversal, which only targets the 6th item in the original traversal. The traversed is needed because we want our traversal to target each value in the list, not just each list. This is confusing, it helps me to think of each optic as a "domino", and optics can only be combined if the target of one matches the source of the other. – Joe Sep 06 '22 at 19:36
  • If `list2` has type `[(Char, Char)]` we can traverse over `list1` and the first `Char` in the tuple with `(#list1 % traversed) \`adjoin\` (#list2 % traversed % _1)`. (the `traversed` comes before the `_1`, because it's a list of tuples, not a tuple with a list) – Joe Sep 06 '22 at 19:37