wernerdegroot / listzipper

MIT License
39 stars 8 forks source link

Feature Request: Additional list modifiers #15

Open trotha01 opened 7 years ago

trotha01 commented 7 years ago

I am really enjoying this zipper library. I was wondering about adding in functionality such as append and cons.

I'm not entirely sure what the api design would look like, I am open to discussion on this.

My use case is for a static list. For example, if I want to make a Zipper that is not empty, and will never be empty, I would have to construct it like:

        Maybe.withDefault (Zipper.singleton item1) <|
            Zipper.fromList
                [ item1
                , item2
                , item3
                ]

Instead of something maybe like

        (Zipper.singleton item1) |>
            Zipper.appendList
                [ item2
                , item3
                ]

Or maybe there's a different better way than this?

wernerdegroot commented 7 years ago

I was wondering about adding in functionality such as append and cons.

That would definitely be a good idea! As a Zipper always "points" to an element in a list, we could add

insertBefore : a -> Zipper a -> Zipper a
insertAfter : a -> Zipper a -> Zipper a
insertAllBefore : List a -> Zipper a -> Zipper a
insertAllAfter : List a -> Zipper a -> Zipper a

Would that solve your use-case?

I think these newly produced Zippers should keep pointing at the same element, or would you rather have them point at the newly inserted element (in case of insertBefore and insertAfter)?

My use case is for a static list. For examp...

Is this a separate request? If so, we would do best to open a separate issue for this. If I understand you correctly, you are interested in a Zipper for something like List.NonEmpty (http://package.elm-lang.org/packages/mgold/elm-nonempty-list/3.0.0/List-Nonempty)?

trotha01 commented 7 years ago

I didn't know about non-empty list, that is neat to see.

I like the idea of insertBefore and insertAfter. Maybe we should wait on the insertAll functions unless there is more demand for them. They would be easier to add later than to remove. I was trying to find a similar case in the core libs, and it looks like Dict has some similarities. You can convert from list, and add a single value, but not add a list. What do you think?

wernerdegroot commented 7 years ago

I didn't know about non-empty list, that is neat to see.

I think you'll need something like List.NonEmpty to prove to the compiler that the List is not empty, otherwise we have no choice but to return an Maybe (Zipper a) from fromList. I would rather not add a dependency to List.NonEmpty, but I would be happy to create another library List.NonEmpty.Zipper (or something similar).

I like the idea of insertBefore and insertAfter. Maybe we should wait on the insertAll functions unless there is more demand for them. They would be easier to add later than to remove. I was trying to find a similar case in the core libs, and it looks like Dict has some similarities. You can convert from list, and add a single value, but not add a list. What do you think?

I agree! Do you think you would rather have the newly produced Zippers point to the newly inserted elements, or rather at the "old" focus point?

trotha01 commented 7 years ago

I thought it would be good to look around to see what other zipper libraries do.

Looking at a Haskell lib: https://hackage.haskell.org/package/ListZipper-1.2.0.2/docs/Data-List-Zipper.html It has "insert" to insert before the cursor, and moves the cursor to the new item It has "push" to insert before the cursor, and keeps the cursor the same

clojure looks like it never moves the cursor: https://clojure.github.io/clojure/clojure.zip-api.html in "insert-child", "insert-left", "insert-right"

So it looks like there are different ways of doing insert. I would be happy if the library just did whatever was easiest to implement and documented the behavior.

Also, if we are going to add some way to insert, we should probably have some way to delete, right? In the case of delete, I assume it would delete at the cursor. Delete seems tricky. We can go to the right, but what if there's nothing to the right? Go left if there's nothing to the right? We would have to return a Maybe zipper. Thoughts on this?

wernerdegroot commented 7 years ago

I think letting insertBefore behave like :: is most intuitive (see insert in Haskell). That means that insertBefore moves the focus to the newly inserted element.

What would also be expected is:

delete (insertBefore 1 someZipper) == someZipper

This means that delete should move the focus to the right. But what do we do when there is no element to the right?

I'm not sure yet as to the direction we should take...

Thinking outside the box, maybe we could distinguish between a Zipper pointing at an element and a Zipper pointing between elements. Only in the latter case would inserting make sense. Deleting the current focus would then result in a Zipper pointing between the previous element and the next element.

trotha01 commented 7 years ago

I like the insertBefore, that makes sense to me.

delete would have to return a Maybe, since there is no such thing as an empty Zipper here, right? If a list has one item, and we call delete, we would have to return Nothing.

So we would have to use the signature delete : Zipper a -> Maybe (Zipper a)

Also, it seems to makes most intuitive sense to delete the element the pointer is at.

With this in mind, the cases seem to be:

  1. delete moves to the right, returns Nothing if there's nothing to the right. Pro's: makes intuitive sense when there is an element to the right. Con's: it can be confusing that calling delete on a Zipper that still has elements returns Nothing.

  2. delete moves to the right, if there's nothing to the right, moves left, if there's nothing left, returns Nothing. Pro's: delete only returns Nothing when the list is empty. Con's the behavior is inconsistent (sometimes moving left, sometimes right) which can be confusing.

  3. delete points at an element between the previous elements (your new proposal). Pro's: something like insert a (delete zipper) == zipper would make sense. Con's: what would current (delete zipper) return if it is pointing between two items? Or between an item and nothing if it's at the end?

Personally, I like # 1 since it is simple and easy to grasp. But, since I currently only have the need for appending to a zipper, I think it would be the elm way to wait on implementing the delete function. Elm seems to stay away from adding anything until the need arises in real-life cases.

simonh1000 commented 5 years ago

I need exactly this feature for a 0.18 project

wernerdegroot commented 5 years ago

Personally, I like # 1 since it is simple and easy to grasp. But, since I currently only have the need for appending to a zipper, I think it would be the elm way to wait on implementing the delete function. Elm seems to stay away from adding anything until the need arises in real-life cases.

There's one thing we should keep in mind when making our decision: how safe is it to change this behavior later. If we switch from #1 to #2 later, the type signatures stay the same but the behavior changes which might cause bugs for projects where the upgrade is done "sloppily". We can solve this by creating a second version of delete which takes a second argument that determines what to do when there's no element to focus on.

I need exactly this feature...

Would you be willing to make a PR? I haven't done any work in Elm for quite some time, so my Elm is a little rusty.

...for a 0.18 project

If you want, we could create a 0.18 fork (like listzipper-compat or something) that supports 0.18 for a while longer. Or is there a better approach to supporting multiple versions of Elm?