Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-Empty Discussion #105

Open
Chadtech opened this issue Jul 25, 2018 · 7 comments
Open

Non-Empty Discussion #105

Chadtech opened this issue Jul 25, 2018 · 7 comments

Comments

@Chadtech
Copy link
Collaborator

We recently merged a change to groupWhile to make it deal with non-empty lists (a, List a). Until now, we have kind of steered clear of non-empty lists.

So now we have a little bit of non-empty lists in List.Extra. Is that a good move? A bad move? Where do we draw the line? Do we need to draw any lines? Bottom line question is, should we have non-empty list support in List Extra?

My opinion is that theres no problem with a some non-empty list functions. We dont have to support every possible operation, but a few helpers that can make it easy to transform non-empty lists to lists and vice versa would be nice.

@robinheghan
Copy link
Contributor

I'm not a huge fan myself, as I usually pass the result of list-extra fns to other functions which deal with a List. If list-extra return (a, List a) everywhere, I'll often have to convert to from that to a regular List, which already composes well.

It's not a big deal, but personally I don't often have use of non-empty lists.

@pzp1997
Copy link
Contributor

pzp1997 commented Oct 4, 2018

In my own code, I often will use uncurry (::) to convert ( a, List a ) -> List a. I think it works pretty well when pipelined. @Skinney, do you think adding a function to that effect to list-extra would be helpful?

fromNonempty : ( a, List a ) -> List a
fromNonempty ( x, xs ) =
    x :: xs

@robinheghan
Copy link
Contributor

I think that would be a good idea. I also think it would be good to use a type alias in those functions which return a nonempty list, to make it more clear :)

type alias Nonempty = (a, List a)

@Chadtech
Copy link
Collaborator Author

Chadtech commented Oct 4, 2018

I also like fromNonempty.

The alias, I am not so sure. I see Nonempty as adding information about the use of the data, but also removing information about the underlying data structure. For example, you cant immediately tell that Tuple.first can be used on Nonempty a but you could from seeing (a, List a). This might disadvantage people newly approaching codebases that use the Nonempty alias.

I could be wrong. Maybe this is a worthy trade off. This is the kind of thing I wish we could obtain empirical data on, but I suppose we never will.

@janwirth
Copy link

janwirth commented Sep 27, 2019

I once wrote this piece of code where the non-empty actually came in quite useful!
I found it often improves the way the my code evolves - similar to the constraits of elm-lang put upon you: When it starts feeling clunky you are doing something wrong.

-- GROUPING


type alias Groups a =
    List (Group a)


type alias Group a =
    { name : String, items : List a }


type alias Config a =
    { outerOrder : Comparison a
    , innerOrder : Comparison a
    , makeName : a -> String
    }


type alias Comparison a =
    a -> a -> Order


{-| preprocess elements before passing them to a comparison function
-}
pipeComparison : (a -> b) -> Comparison b -> Comparison a
pipeComparison fn comparison firstOperand secondOperand =
    comparison (fn firstOperand) (fn secondOperand)


{-| Turn a list into a list of groups with a title

    type alias Pizza = {cat : String, price : Int}

    items : List Pizza
    items = [ {cat = "Veggie", price = 7}
        , {cat = "Veggie", price = 10}
        , {cat = "Beef", price = 8}
        , {cat = "Beef", price = 9}
        ]

    cfg : Config Pizza
    cfg =
        { outerOrder = pipeComparison .cat compare
        , innerOrder = pipeComparison .price compare
        , makeName = .cat
        }

    make cfg items
    --> [ { items = [{ cat = "Beef", price = 8} , { cat = "Beef", price = 9 }], name = "Beef"}
    --> , { items = [{ cat = "Veggie", price = 7} , { cat = "Veggie", price = 10 }], name = "Veggie" }
    --> ]

-}
sortedGroups : Config a -> List a -> Groups a
sortedGroups { outerOrder, innerOrder, makeName } =
    -- sort and make groups for outer
    List.sortWith outerOrder
        >> List.Extra.groupWhile (\a b -> outerOrder a b == EQ)
        >> List.map (\( first, rest ) -> { name = makeName first, items = first :: rest })
        -- sort inner
        >> List.map (\group -> { group | items = List.sortWith innerOrder group.items })

@Chadtech
Copy link
Collaborator Author

Chadtech commented Sep 29, 2019

I have had a few use cases recently where I tried out a non-empty list. They didnt pan out too well I think, but that could have been just due to my use cases.

ButtonRow.view : (Button msg, List (Button msg)) -> Html msg
I made a view function that took a list of buttons, and then rendered them horizontally. I realized it doesnt make sense to try and render a button row in the first place if you dont have any buttons to render, so I decided to make the api take a non empty list (Button msg, List (Button msg)) instead of a regular List (Button msg)

I dont think a non empty list was really worthwhile for this use case, and I have since refactored back to a regular List. All my ButtonRow.view were taking static inline lists of Button msg. So the only source of error is a developer manually typing ButtonRow.view [], which seems like a remote possibility. Since the risk isnt there, the benefit isnt either, and the costs are having to type a tuple, which I find to be just a little bit tedious.

An admin panel
I am working on an admin panel, where you administer Groups. Just in terms of UX, loading the page and having no groups at all, is possible, but unusual. Starting with no groups is a different kind of UX entirely, so I forked on that possibility at the top of the model:

type Model
    = NoGroups
    | SomeGroups (Group, List Group) GroupsLoadedModel

A non-empty list was handy here, not because the state of no groups to administer isnt possible, but because when there are some groups there necessarily should be a bunch of other values as well.

But then, for my project, it turned out to be quite a lot more possible to have no groups later on in the UX than I expected, so I reverted to a regular list. So the actual non-empty list type wasnt used, but, it has been useful to fork on list emptiness at the "top" of the applicatio.

@Erudition
Copy link

My vote is to use the Nonempty Library, and then have a Nonempty.Extra library mirroring these functions. Keeps them separate, and gives first-class support for nonempty lists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants