zwilias / elm-holey-zipper / List.Holey.Zipper

Like a regular old list-zipper, except it can also focus on the holes between elements.

This means you can represent an empty list, or point between two items and plug that hole with a value.

Types


type Zipper t a

Represents Zipper over a list with items of type a. The type t will, in practice, always be either Full or Hole. When it is Full, the zipper is focused on an item. When it is Hole, you're looking at a hole between elements.


type Full

A Zipper Full a is pointing at an element of type a.


type Hole

A Zipper Hole a is pointing at a hole between as.

... Heh.

Creation

empty : Zipper Hole a

Create an empty zipper. It's pointing at nothing, there's nothing before it and nothing after it. It's the loneliest of all zippers.

import List.Holey.Zipper as Zipper


Zipper.toList Zipper.empty
--> []

singleton : a -> Zipper Full a

A zipper with a single thing in it. Singleton is just fancy-speak for single thing.

import List.Holey.Zipper as Zipper


Zipper.singleton "wat"
    |> Zipper.toList
--> [ "wat" ]

zipper : a -> List a -> Zipper Full a

Construct a zipper from the head of a list and some elements to come after it.

import List.Holey.Zipper as Zipper


Zipper.zipper "foo" []
--> Zipper.singleton "foo"


Zipper.zipper 0 [ 1, 2, 3 ]
    |> Zipper.toList
--> [ 0, 1, 2, 3 ]

Extraction

current : Zipper Full a -> a

Get the value the Zipper is currently pointing at.

Only applicable to zippers pointing at a value.

import List.Holey.Zipper as Zipper


Zipper.singleton "hi there"
    |> Zipper.current
--> "hi there"


Zipper.zipper 1 [ 2, 3, 4 ]
    |> Zipper.last
    |> Zipper.current
--> 4

before : Zipper t a -> List a

List the things that come before the current location in the zipper.

import List.Holey.Zipper as Zipper


Zipper.zipper 0 [ 1, 2, 3 ]
    |> Zipper.next
    |> Maybe.andThen Zipper.next
    |> Maybe.map Zipper.before
--> Just [ 0, 1 ]

after : Zipper t a -> List a

Conversely, list the things that come after the current location.

import List.Holey.Zipper as Zipper


Zipper.zipper 0 [ 1, 2, 3 ]
    |> Zipper.next
    |> Maybe.map Zipper.after
--> Just [ 2, 3 ]

toList : Zipper t a -> List a

Flattens the zipper (back) into a list.

import List.Holey.Zipper as Zipper


Zipper.toList Zipper.empty
--> []


Zipper.zipper 123 [ 789 ]
    |> Zipper.nextHole
    |> Zipper.plug 456
    |> Zipper.toList
--> [ 123, 456, 789 ]

Navigation

next : Zipper t a -> Maybe (Zipper Full a)

Move the zipper to the next item, if there is one.

import List.Holey.Zipper as Zipper


Zipper.zipper 0 [ 1, 2, 3 ]
    |> Zipper.next
    |> Maybe.map Zipper.current
--> Just 1

This also works from within holes:

Zipper.empty
    |> Zipper.insertAfter "foo"
    |> Zipper.next
--> Just <| Zipper.singleton "foo"

If there is no next thing, next is Nothing.

Zipper.empty
    |> Zipper.next
--> Nothing


Zipper.zipper 0 [ 1, 2, 3 ]
    |> Zipper.last
    |> Zipper.next
--> Nothing

previous : Zipper t a -> Maybe (Zipper Full a)

Move the zipper to the previous item, if there is one.

import List.Holey.Zipper as Zipper


Zipper.previous Zipper.empty
--> Nothing


Zipper.zipper "hello" [ "holey", "world" ]
    |> Zipper.last
    |> Zipper.previous
    |> Maybe.map Zipper.current
--> Just "holey"

nextHole : Zipper Full a -> Zipper Hole a

Move the zipper to the hole right after the current item. A hole is a whole lot of nothingness, so it's always there.

import List.Holey.Zipper as Zipper


Zipper.zipper "hello" [ "world" ]
    |> Zipper.nextHole
    |> Zipper.plug "holey"
    |> Zipper.toList
--> [ "hello", "holey", "world" ]

previousHole : Zipper Full a -> Zipper Hole a

Move the zipper to the hole right before the current item. Feel free to plug that hole right up!

import List.Holey.Zipper as Zipper


Zipper.singleton "world"
    |> Zipper.previousHole
    |> Zipper.plug "hello"
    |> Zipper.toList
--> [ "hello", "world" ]

first : Zipper Full a -> Zipper Full a

Go to the first element in the Zipper. Note that this only accepts a zipper that points at a thing, since it would have to return a Maybe otherwise.

If you want to zip back to the beginning of a zipper pointing at a hole, you can still use zipper |> previous |> Maybe.map first

import List.Holey.Zipper as Zipper


Zipper.zipper 1 [ 2, 3, 4 ]
    |> Zipper.prepend [ 4, 3, 2 ]
    |> Zipper.first
    |> Zipper.current
--> 4

last : Zipper Full a -> Zipper Full a

Go to the last element of a zipper. Same warnings as for first apply.

import List.Holey.Zipper as Zipper


Zipper.zipper 1 [ 2, 3, 4 ]
    |> Zipper.last
    |> Zipper.current
--> 4

beforeFirst : Zipper t a -> Zipper Hole a

Go to the hole before the first element. Remember that holes surround everything! They are everywhere.

import List.Holey.Zipper as Zipper


Zipper.zipper 1 [ 3, 4 ]
    -- now we're after 1
    |> Zipper.nextHole
    -- plug that hole
    |> Zipper.plug 2
    -- back to _before_ the first element
    |> Zipper.beforeFirst
    -- put something in that hole
    |> Zipper.plug 0
    -- and check the result
    |> Zipper.toList
--> [ 0, 1, 2, 3, 4 ]

afterLast : Zipper t a -> Zipper Hole a

Go to the hole after the end of the zipper. Into the nothingness.

findForward : (a -> Basics.Bool) -> Zipper t a -> Maybe (Zipper Full a)

Find the first element in the zipper the matches a predicate, returning a zipper pointing at that thing if it was found. When provided with a zipper pointing at a thing, that thing is also checked.

This start from the current location, and searches towards the end.

findBackward : (a -> Basics.Bool) -> Zipper t a -> Maybe (Zipper Full a)

Find the first element in the zipper matching a predicate, moving backwards from the current position.

Modification

map : (a -> b) -> Zipper t a -> Zipper t b

Execute a function on every item in the zipper.

import List.Holey.Zipper as Zipper


Zipper.zipper "first" [ "second", "third" ]
    |> Zipper.map String.toUpper
    |> Zipper.toList
--> [ "FIRST", "SECOND", "THIRD" ]

mapCurrent : (a -> a) -> Zipper t a -> Zipper t a

Execute a function on the current item in the zipper, when pointing at an item.

import List.Holey.Zipper as Zipper


Zipper.zipper "first" [ "second", "third" ]
    |> Zipper.mapCurrent String.toUpper
    |> Zipper.toList
--> [ "FIRST", "second", "third" ]

mapBefore : (a -> a) -> Zipper t a -> Zipper t a

Execute a function on all the things that came before the current location.

import List.Holey.Zipper as Zipper


Zipper.zipper "first" [ "second" ]
    |> Zipper.nextHole
    |> Zipper.mapBefore String.toUpper
    |> Zipper.toList
--> [ "FIRST", "second" ]

mapAfter : (a -> a) -> Zipper t a -> Zipper t a

Execute a function on all the things that come after the current location.

mapParts : { before : a -> b, current : a -> b, after : a -> b } -> Zipper t a -> Zipper t b

Execute a triplet of functions on the different parts of a zipper - what came before, what comes after, and the current thing if there is one.

import List.Holey.Zipper as Zipper


Zipper.zipper "first" [ "second" ]
    |> Zipper.nextHole
    |> Zipper.plug "one-and-a-halfth"
    |> Zipper.mapParts
        { before = (++) "before: "
        , current = (++) "current: "
        , after = (++) "after: "
        }
    |> Zipper.toList
--> [ "before: first"
--> , "current: one-and-a-halfth"
--> , "after: second"
--> ]

plug : a -> Zipper Hole a -> Zipper Full a

Plug a hole-y zipper.

import List.Holey.Zipper as Zipper


Zipper.plug "plug" Zipper.empty
--> Zipper.singleton "plug"

remove : Zipper Full a -> Zipper Hole a

Punch a hole into the zipper by removing an element entirely. You can think of this as collapsing the holes around the element into a single hole, but honestly the holes are everywhere.

import List.Holey.Zipper as Zipper


Zipper.zipper "hello" [ "holey", "world" ]
    |> Zipper.next
    |> Maybe.map Zipper.remove
    |> Maybe.map Zipper.toList
--> Just [ "hello", "world" ]

append : List a -> Zipper t a -> Zipper t a

Append a bunch of items after the zipper. This appends all the way at the end.

import List.Holey.Zipper as Zipper


Zipper.zipper 123 [ 456 ]
    |> Zipper.append [ 789, 0 ]
    |> Zipper.toList
--> [ 123, 456, 789, 0 ]

prepend : List a -> Zipper t a -> Zipper t a

Prepend a bunch of things to the zipper. All the way before anything else.

import List.Holey.Zipper as Zipper


Zipper.zipper 1 [ 2, 3, 4 ]
    |> Zipper.last
    |> Zipper.prepend [ 5, 6, 7 ]
    |> Zipper.toList
--> [ 5, 6, 7, 1, 2, 3, 4 ]

insertAfter : a -> Zipper t a -> Zipper t a

Insert something after the current location.

import List.Holey.Zipper as Zipper


Zipper.empty
    |> Zipper.insertAfter "hello"
    |> Zipper.toList
--> [ "hello" ]


Zipper.zipper 123 [ 789 ]
    |> Zipper.insertAfter 456
    |> Zipper.toList
--> [ 123, 456, 789 ]

insertBefore : a -> Zipper t a -> Zipper t a

Insert something before the current location.

import List.Holey.Zipper as Zipper


Zipper.empty
    |> Zipper.insertBefore "hello"
    |> Zipper.toList
--> [ "hello" ]


Zipper.singleton 123
    |> Zipper.insertBefore 456
    |> Zipper.toList
--> [ 456, 123 ]