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

WIP: List extra basic #8

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Conversation

dawehner
Copy link
Contributor

This adds the list extra basic stuff, as described in #4 (comment)

last : List a -> Maybe a
init : List a -> Maybe (List a)
getAt : Int -> List a -> Maybe a
(!!) : List a -> Int -> Maybe a
uncons : List a -> Maybe ( a, List a )
maximumBy : (a -> comparable) -> List a -> Maybe a
minimumBy : (a -> comparable) -> List a -> Maybe a
andMap : List a -> List (a -> b) -> List b
andThen : (a -> List b) -> List a -> List b
takeWhile : (a -> Bool) -> List a -> List a
dropWhile : (a -> Bool) -> List a -> List a
unique : List comparable -> List comparable
uniqueBy : (a -> comparable) -> List a -> List a
allDifferent : List comparable -> Bool
allDifferentBy : (a -> comparable) -> List a -> Bool

This is WIP for example because of the missing test coverage

Copy link
Collaborator

@rgrempel rgrempel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is super!

I've made a bunch of suggestions -- see what you think. One theme is that you could take more advantage of your access here to the internals of DictList -- sometimes it would be more efficient to operate on the dict and the list separately, and then reconstruct a DictList to return.

# ListExtra

@docs last, inits, (!!), uncons, maximumBy, minimumBy, andMap, andThen, takeWhile, dropWhile, unique, uniqueBy, allDifferent, allDifferentBy

-}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, I think it probably makes sense to try to classify these under topical headings, thus splitting them up throughout the documentation -- it's nice to keep track of their origin in List.Extra in the source, but I think it's better in the documentation if they are divided topically.

-}

import Dict exposing (Dict)

import Dict exposing (Dict, keys)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better not to expose keys, since we have our own DictList.keys as well. So, we can refer to Dict.keys explicitly.

import DictList.Compat exposing (customDecoder, decodeAndThen, first, maybeAndThen, second)
import Json.Decode exposing (Decoder, keyValuePairs, value, decodeValue)
import List.Extra
import Set
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be unused so far?

@@ -535,6 +557,11 @@ getAt index (DictList dict list) =
|> Maybe.map (\value -> ( key, value ))
)

{-| Alias for getAt, but with the parameters flipped.
-}
(!!) : DictList comparable value -> Int -> Maybe ( comparable, value)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this needs a run of elm-format ...

@@ -938,6 +965,174 @@ fromDict dict =



-- LIST EXTRA

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to keep things organized nicely, we could move getAt and (!!) and any other things actually inspired by List.Extra down here -- it will help a bit when changes are made to List.Extra and we want to track them. (So, I'd suggest organizing the source to match where we got things from, whereas re-arranging the documentation topically).

I actually had considered putting the List.Extra stuff in a separate module, but I can see now that that won't work, because we'll want to make use of the DictList constructors which we don't expose (so aren't accessible from another module).

unique [0,1,1,0,1] == [0,1]
-}
unique : DictList comparable1 comparable2 -> DictList comparable1 comparable2
unique list =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one doesn't really make sense in this form, because the keys will necessarily be unique -- so this will always be an identity function as implemented here -- it will never remove anything.

I suppose we could change it so that it only considers the values -- the puzzle then would be which key should be chosen where two keys have equal values ...

uniqueBy f list =
toList list
|> List.Extra.uniqueBy (uncurry f)
|> fromList
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one, on the other hand, could remove some values, so it could do something. I wonder which value the implementation retains (since the "duplicate" values are not, themselves, necessarily equal)? That would be worth documenting (I suppose List.Extra might not document it).

-}
allDifferent : DictList comparable1 comparable2 -> Bool
allDifferent list =
-- @TODO Should this method just use the values, or also the keys
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very good question!

In some cases, it makes sense to have two versions of a function -- for instance, I ended up with both getAt and getKeyAt. So, you might want to have an allDifferent which considers both keys and values, and then also an allDifferentValues that considers only values, or something like that.

-- |> toList
-- |> values
-- |> concat

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, it isn't obvious to me either what andMap and andThen ought to do in the context of DictList -- that would take some thinking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such an interesting question! I've been thinking about that and talking with a bunch of people.

One sensefull type would look like the following: (DictList comparable (a -> b)) -> (DictList comparable a) -> (DictList comparable b). For the concrete implementation you would probably match the keys and throw away all (a->b)'s / a's when there are no matching keys. This requires for example no instance of pure (the other function which makes sense in the context). To implement pure you would need some mempty for the keys.

Note: Dict.Map doesn't implement that in haskell
Note2: https://hackage.haskell.org/package/total-map-0.0.6/docs/Data-TotalMap.html does implement the above idea

For andThen the signature could look like the following:

andThen : (comparable -> a -> DictList comparable b) -> DictList comparable a -> DictList comparable b

What could this function do:

  • This function maps a key/value pair to a new DictList, so it kinda generates a new dict list.
  • The result could then have the specific key removed and inserted all keys/values of the result in b. This smashing together could be some kind of merge operation.

In this world you could reimplement:

  • remove k dictList = andThen (\k2 v -> if k == k2 then empty else fromList [(k2, v)]) dictList
  • update k v dictList = andThen (\k2 v2 -> fromList [(k2, if k == k2 then v else v2)]) dictList

Does it even remotely makes sense?

TL;DR

  • Don't expect to have some form of product/cartesian behaviour for andMap
  • I doubt there is too much value in return / pure implementations

@@ -0,0 +1,82 @@
module ListExtraTests exposing (tests)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I would suggest here is looking at https://github.com/elm-community/list-extra/blob/6.0.0/tests/Tests.elm and:

  • copy the relevant tests
  • make whatever the smallest changes are to fit the function signatures here

That is, instead of working from ListTests.elm as a base, work from the actual tests for List.Extra -- the idea is to prove that we can pass the List.Extra tests (modified as little as needed to fit our context).

Then, in a separate test file, you could test anything else you want to test -- i.e. things that List.Extra doesn't test, or behaviours that are unique to DictList.

@dawehner
Copy link
Contributor Author

I've made a bunch of suggestions -- see what you think.

I'm attending a conference this week, so I'm not sure I'll find time to respond to your long and detailed review. I'm super happy to retrieve that insightful comments that quickly, so happy to learn new things.

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

Successfully merging this pull request may close these issues.

2 participants