Good question, thanks for asking.
Quoting relevant part from post:
The library also comes with a zipper module. Unlike the NonEmpty
, the Zipper
type is opaque. Zipper type contains private data field users are not supposed to be able to mess with – therefore the opaque type is the right choice in this case.
More specifically in case of Zipper
the list of previous values is stored in reverse order. This is to make movements of zipper O(1):
next : Zipper a -> Maybe (Zipper a)
next (Zipper p f n) =
case n of
[] ->
Nothing
h :: t ->
Just <| Zipper (f :: p) h t
as you can see it just cons
currently focus value on one hand and takes head of the other side as a new focus.
On the other hand, the cost of reversing the list is then payed in places where conversion between and from other data structure happens. for instance:
custom constructor
custom : List a -> a -> List a -> Zipper a
custom p f n =
Zipper (List.reverse p) f n
and when converting back to non empty.
toNonEmpty : Zipper a -> NonEmpty a
toNonEmpty (Zipper p f n) =
case List.reverse p of
[] ->
( f, n )
h :: t ->
( h, t ++ f :: n )
It didn’t seem like a good idea to expose constructor which is hiding this implementation detail to the users. The reason why I included the Zipper to the library was to make it clear that I’m not against usage of opaque types in general.
- I don’t think it would be good idea to expose implementation detail like this
- I didn’t feel that it’s worth doing linear append while moving the focus in zipper is worth it. Folks are expecting this operation to be constant time.
So this was sort of my reasoning. That being said I can imagine Zipper represented exactly as you describe provided the list of previous values won’t be reversed or it would be even using Array in its place. Anyway I usually don’t need to construct Zipper within some other library but I would often like to return non empty list (Like Err errors
where I know the list of errors can’t be empty).
For example this was the issue in elm-street which finaly motivated me to implement the idea, that might provide some context to where might be NonEmpty
defined as this useful.
EDIT:
Maybe additional clarification. Even though it might not seem like a big deal to make movement of focus more expensive, you need to consider that this function is reused in other functions. The simple example might be function for moving to the end or start of a Zipper. In app I’m working on we’re also using comonadic operations a lot (extend) which also call next
for every position within the zipper. This is especially why I felt it’s important to keep these operations constant.