4

Is there a way in Haskell to add an element to the end of a list without having to go through the whole list?

Example if I have the list [1,2,3,4] and want to add 5 to the end like: [1,2,3,4,5]

As I understand, the ++ operator will have to unravel through the whole list, which is quite inefficient.

Thanks!

Sequinex
  • 641
  • 1
  • 9
  • 17
  • 6
    So.. maybe you will tell us about the *real* problem? Perhaps it can be solved without appending an element to the end of the list? – Eugene Sh. Jul 31 '17 at 17:14
  • 3
    "without appending an element" often means using a different data structure or algorithm. Lists are easy but commonly misused when other structures would be more fitting. – Thomas M. DuBuisson Jul 31 '17 at 17:49
  • 2
    Sometimes it can be useful to build a list in reverse (with prepending), and then return the `reverse`. – Willem Van Onsem Jul 31 '17 at 18:32

1 Answers1

11

As @Jubobs already said in the comments - no you cannot, but this is not necessarily (always) inefficient.

Before starting with some explanation I'd like to remind you of Donald Knuth and his saying that the root of all evil is premature optimization. So try to write a correct program and worry about performance later - when you run into bottlenecks (and be assured haskell has good tooling to detect bottlenecks!).

Example

Say you have an infinite list [1..] and you want to append 5 you can still do that in haskell. Let us fire up ghci and observe some things.

> ghci
GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
Prelude> let a = [1..] ++[5] :: [Int]

first of all we notice - that let-bindings work lazily and nothing is printed (yet). We can even go further and inspect how far a is evaluated.

Prelude> :sprint a
a = _

while an expression like filter (== 5) a will never terminate, we still can do useful stuff with our list a.

Prelude> let b = map (+2) a
Prelude> take 10 b
[3,4,5,6,7,8,9,10,11,12]

and inspecting a again.

Prelude> :sprint a
a = 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : _

we have only evaluated the first ten elements.

Concluding this example we see - even if you "append" 5 it might never be used - and thus the "appending" will have never happened, which is more efficient than if we had a cheap "appending" that had happened.

(Note that this lazyness, introduces a bit of a memory overhead that can lead into troubles see Lazy Evaluation - Space Leak for more on that.)

Alternatives

As others have already said - there are alternatives to using good old lists.

  1. If you append often but use the list as an "accumulator" - it might be a good idea to just pre-pend and reverse the list before returning. which is O(1) for all (:) operations and O(n) for the reverse - compared to O(n) for each (++) step.
  2. Use different data structures:

    • difference list: which you can read up on Learn you a haskell for great good quoting from there:

      The difference list equivalent of a list like [1,2,3] would be the function \xs -> [1,2,3] ++ xs. A normal empty list is [], whereas an empty difference list is the function \xs -> [] ++ xs.

    • Sequence: from Data.Sequence which supports efficient prepend/append-operations and lookup for elements at the edges.

      $ stack ghci --package containers
      Prelude> import Data.Sequence
      Prelude Data.Sequence> let a = fromList [1..4] :: Seq Int
      Prelude Data.Sequence> (a |> 5)
      fromList [1,2,3,4,5]
      Prelude Data.Sequence> 0 <| (a |> 5)
      fromList [0,1,2,3,4,5]
      
    • Set - which is not ordered like lists, but nevertheless supports efficient (O(log n)) inserts, but only allows for unique inserts.

      $ stack ghci --package containers
      Prelude> import Data.Set
      Prelude Data.Set> let a = fromList [1..4] :: Set Int
      Prelude Data.Set> insert 5 a
      fromList [1,2,3,4,5]
      Prelude Data.Set> insert 4 a
      fromList [1,2,3,4]
      
epsilonhalbe
  • 15,637
  • 5
  • 46
  • 74
  • 1
    Nice answer, but `Set Int` contains only unique elements, so not a good list alternative. – trevor cook Aug 01 '17 at 12:26
  • 2
    Please be judicious with that Knuth quote. Since appending to the end of a list is not just overhead-slow but _complexity_-slow, avoiding this is something I'd never call _premature optimisation_. I'd call it perhaps YAGNI, and indeed I often use `++` myself, but this might be considered _always a hack_. – leftaroundabout Aug 01 '17 at 15:46