41

I have a list of elements and I wish to update them:

from this: ["Off","Off","Off","Off"]

to this: ["Off","Off","On","Off"]

As I am somewhat new to Haskell, I have been using (x:xs)!!y to extract and update individual components using the function:

replace y z [] = []
replace y z (x:xs)
  | x==y           = z:replace y z xs
  | otherwise      = x:replace y z xs

and then entering the following in ghci: (replace "Off" "On" ["Off",'Off","Off","Off"]) !! 2

I get the following: "On"

I seem to be able to extract and convert elements of a list but I can't seem to get a list up with the single element converted.

Any help regarding this matter would be appreciated.

HaskellElephant
  • 9,819
  • 4
  • 38
  • 67
maclunian
  • 7,893
  • 10
  • 37
  • 45

9 Answers9

64

Typically, you modify elements of a list by splitting the list, replacing an element, and joining it back together.

To split a list at an index, we have:

 splitAt :: Int -> [a] -> ([a], [a]) 

which you can use to break up a list, like so:

 > splitAt 2 ["Off","Off","Off","Off"] 
 (["Off","Off"],["Off","Off"])

now you just need to pop the head element of the snd component of the list. This is easily done with pattern matching:

 > let (x,_:ys) = splitAt 2 ["Off","Off","Off","Off"]
 > x
 ["Off","Off"]
 > ys
 ["Off"]

you can now join the list back together, with an "On":

 > x ++ "On" : ys
 ["Off","Off","On","Off"]

I'll leave it to you to put those pieces together into a single function.


As a style note, I'd suggest using a new custom data type, instead of String for your toggles:

 data Toggle = On | Off deriving Show
Don Stewart
  • 137,316
  • 36
  • 365
  • 468
42

Changing the nth element

A common operation in many languages is to assign to an indexed position in an array. In python you might:

>>> a = [1,2,3,4,5]
>>> a[3] = 9
>>> a
[1, 2, 3, 9, 5]

The lens package gives this functionality with the (.~) operator. Though unlike in python the original list is not mutated, rather a new list is returned.

> let a = [1,2,3,4,5]
> a & element 3 .~ 9
[1,2,3,9,5]
> a
[1,2,3,4,5]

element 3 .~ 9 is just a function and the (&) operator, part of the lens package, is just reverse function application. Here it is with more common function application.

> (element 3 .~ 9) [1,2,3,4,5]
[1,2,3,9,5]

Assignment again works perfectly fine with arbitrary nesting of Traversables.

> [[1,2,3],[4,5,6]] & element 0 . element 1 .~ 9
[[1,9,3],[4,5,6]]

or

> set (element 3) 9 [1,2,3,4,5,6,7]

Or if you want to effect multiple elements you can use:

> over (elements (>3)) (const 99) [1,2,3,4,5,6,7]
> [1,2,3,4,99,99,99]

Working with types other than lists

This is not just limited to lists however, it will work with any datatype that is an instance of the Traversable typeclass.

Take for example the same technique works on trees form the standard containers package.

 > import Data.Tree
 > :{
 let
  tree = Node 1 [
       Node 2 [Node 4[], Node 5 []]
     , Node 3 [Node 6 [], Node 7 []]
     ]
 :}
> putStrLn . drawTree . fmap show $ tree
1
|
+- 2
|  |
|  +- 4
|  |
|  `- 5
|
`- 3
   |
   +- 6
   |
   `- 7
> putStrLn . drawTree . fmap show $ tree & element 1 .~ 99
1
|
+- 99
|  |
|  +- 4
|  |
|  `- 5
|
`- 3
   |
   +- 6
   |
   `- 7
> putStrLn . drawTree . fmap show $ tree & element 3 .~ 99
1
|
+- 2
|  |
|  +- 4
|  |
|  `- 99
|
`- 3
   |
   +- 6
   |
   `- 7
> putStrLn . drawTree . fmap show $ over (elements (>3)) (const 99) tree
1
|
+- 2
|  |
|  +- 4
|  |
|  `- 5
|
`- 99
   |
   +- 99
   |
   `- 99
Electric Coffee
  • 11,733
  • 9
  • 70
  • 131
Davorak
  • 7,362
  • 1
  • 38
  • 48
23

I'm not sure what you are trying to do. If you only need to generate ["Off","Off","On","Off"] you can do it explicitly. Generally speaking, one should avoid modifying state in haskell.

Perhaps what you want is a function to "modify" (generate a new element with a different value) the nth element of a list? Don gives a very general approach to this kind of problem. You can also use explicit recursion:

 replaceNth :: Int -> a -> [a] -> [a]
 replaceNth _ _ [] = []
 replaceNth n newVal (x:xs)
   | n == 0 = newVal:xs
   | otherwise = x:replaceNth (n-1) newVal xs

Haskell provides excellent features for list manipulation. If you dont know them already filter, map, and foldr/foldl are all worth looking at, as are list comprehensions.

adius
  • 13,685
  • 7
  • 45
  • 46
Philip JF
  • 28,199
  • 5
  • 70
  • 77
  • 6
    Consider the more general `modifyNth :: Int -> (a -> a) -> [a] -> [a]`, which takes a function to modify the element, instead of a hard-coded new value. This function can now be implemented as `replaceNth n newVal = modifyNth n (const newVal)` – jpaugh Mar 15 '16 at 22:31
  • This is why Haskell is not used. It's really a joke to do all these just to modify one element – McBear Holden Jan 03 '23 at 14:19
11

Here is a one liner that works perfectly

replace pos newVal list = take pos list ++ newVal : drop (pos+1) list

I doesn't seem efficient to do this kind of things in haskell.

Cristian Garcia
  • 9,630
  • 6
  • 54
  • 75
  • `++` has poor performance whenever its second argument is long. That's the reason for `ShowS`; a recursive approach would probably perform better. – jpaugh Mar 15 '16 at 22:29
6

Here's some code that I've been using:

-- | Replaces an element in a list with a new element, if that element exists.
safeReplaceElement
  -- | The list
  :: [a]
  -- | Index of the element to replace.
  -> Int
  -- | The new element.
  -> a
  -- | The updated list.
  -> [a]
safeReplaceElement xs i x =
  if i >= 0 && i < length xs
    then replaceElement xs i x
    else xs


-- | Replaces an element in a list with a new element.
replaceElement
  -- | The list
  :: [a]
  -- | Index of the element to replace.
  -> Int
  -- | The new element.
  -> a
  -- | The updated list.
  -> [a]
replaceElement xs i x = fore ++ (x : aft)
  where fore = take i xs
        aft = drop (i+1) xs
mhwombat
  • 8,026
  • 28
  • 53
2

Actually, for many cases (not always) where you'd use a List, a Data.Vector is a better choice.

It comes with an update function, see Hackage, that does exactly what you need.

Zsolt Szatmari
  • 1,219
  • 1
  • 12
  • 29
1

I think you should consider using a data structure other than List. For example, if you just want to have a state of four on/off switches then:

data State = St { sw1, sw2, sw3, sw4 :: Bool }

For a dynamic number of switches then consider a mapping from switch name to Bool.

Thomas M. DuBuisson
  • 64,245
  • 7
  • 109
  • 166
  • For a dynamic number of switches I would suggest a `Vector`, or possibly an `IntMap`. – John L May 02 '11 at 10:47
  • @John Right, depending on your exact needs the mapping could be a Map, IntMap, or HashMap (or even something else). Why would you suggest a Vector though? If it's static for each run, sure, but if the number of switches actually needs to grow or shrink that's something Vector can't do well. – Thomas M. DuBuisson May 02 '11 at 15:04
0

I believe this is more elegant way of replacing an individual element:

setelt:: Int -> [a] -> a -> [a]

setelt i list newValue = 
  let (ys,zs) = splitAt i-1 list in  ys ++ newValue ++ tail zs

There is input error handling. So if index i is out of boundaries haskell will show wrong output. (Note: in Haskell indexing starts from 1 onwards)

Hugs would behave as follows:

Main> setelt 1 [1,2,3] 9
[9,2,3]
Main> setelt 3 [1,2,3] 9
[1,2,9]
Main> setelt 0 [1,2,3] 9
[9,2,3]
Main> setelt 4 [1,2,3] 9
[1,2,3,9]
Program error: pattern match failure: tail []

Error handling at your service:

setelt i y newValue = 
    if and [i>0, i<= length y]
    then let (ys,zs) = splitAt (i-1) y in  ys ++ [newValue] ++ tail zs
    else y

if index you give is wrong it returns original list.

Ziai
  • 41
  • 4
0

This answer arrives quite late, but I thought I'd share what I think is an efficient way of replacing the nth element in a list in Haskell. I'm new to Haskell and thought I'd pitch in.

The set function sets the nth element in a list to a given value:

set' :: Int -> Int -> a -> [a] -> [a]
set' _ _ _ [] = []
set' ind curr new arr@(x:xs)
    | curr > ind = arr
    | ind == curr = new:(set' ind (curr+1) new xs)
    | otherwise = x:(set' ind (curr+1) new xs)

set :: Int -> a -> [a] -> [a]
set ind new arr = (set' ind 0 new arr)

As set traverses a list, it breaks the list apart and if the current index is the given n it combines the previous element with the given new value, otherwise, it combines the previous element with the old value in the list for that index.

theideasmith
  • 2,835
  • 2
  • 13
  • 20