-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,21 @@ module DictList | |
-- Conversion | ||
, toDict | ||
, fromDict | ||
-- list-extra | ||
, last | ||
, inits | ||
, (!!) | ||
, uncons | ||
, maximumBy | ||
, minimumBy | ||
, andMap | ||
, andThen | ||
, takeWhile | ||
, dropWhile | ||
, unique | ||
, uniqueBy | ||
, allDifferent | ||
, allDifferentBy | ||
) | ||
|
||
{-| Have you ever wanted a `Dict`, but you need to maintain an arbitrary | ||
|
@@ -115,12 +130,19 @@ between an association list and a `DictList` via `toList` and `fromList`. | |
# JSON | ||
|
||
@docs decodeObject, decodeArray, decodeWithKeys, decodeKeysAndValues | ||
|
||
# ListExtra | ||
|
||
@docs last, inits, (!!), uncons, maximumBy, minimumBy, andMap, andThen, takeWhile, dropWhile, unique, uniqueBy, allDifferent, allDifferentBy | ||
|
||
-} | ||
|
||
import Dict exposing (Dict) | ||
|
||
import Dict exposing (Dict, keys) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better not to expose |
||
import DictList.Compat exposing (customDecoder, decodeAndThen, first, maybeAndThen, second) | ||
import Json.Decode exposing (Decoder, keyValuePairs, value, decodeValue) | ||
import List.Extra | ||
import Set | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears to be unused so far? |
||
|
||
|
||
{-| A `Dict` that maintains an arbitrary ordering of keys (rather than sorting | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this needs a run of |
||
(!!) = | ||
flip getAt | ||
|
||
{-| Insert a key-value pair into a `DictList`, replacing an existing value if | ||
the keys collide. The first parameter represents an existing key, while the | ||
|
@@ -938,6 +965,174 @@ fromDict dict = | |
|
||
|
||
|
||
-- LIST EXTRA | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to keep things organized nicely, we could move I actually had considered putting the |
||
|
||
{-| Extract the last element of a list. | ||
last (fromList [(1, 1), (2, 2), (3, 3)]) == Just (3, 3) | ||
last (fromList []) == Nothing | ||
-} | ||
last : DictList comparable v -> Maybe ( comparable, v ) | ||
last xs = | ||
toList xs | ||
|> List.Extra.last | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will work, but doing a complete last (DictList dict list) =
List.Extra.last list
|> Maybe.map (\key -> DictList key (unsafeGet key dict)) That is, we can get the last key from the internal list directly, and then use |
||
|
||
{-| Return all initial segments of a list, from shortest to longest, empty list first, the list itself last. | ||
|
||
inits (fromList [(1, 1),(2,),(3, 3)]) == [fromList [], fromList [(1, 1)], fromList [(1, 1), (2, 2)], fromList [(1, 1), (2, 2), (3, 3)]] | ||
-} | ||
inits : DictList comparable v -> List (DictList comparable v) | ||
-- @FIXME | ||
inits list = | ||
toList list | ||
|> List.Extra.inits | ||
|> List.map fromList | ||
|
||
{-| Returns a list of repeated applications of `f`. | ||
|
||
If `f` returns `Nothing` the iteration will stop. If it returns `Just y` then `y` will be added to the list and the iteration will continue with `f y`. | ||
nextYear : Int -> Maybe Int | ||
nextYear year = | ||
if year >= 2030 then | ||
Nothing | ||
else | ||
Just (year + 1) | ||
-- Will evaluate to [2010, 2011, ..., 2030] | ||
iterate nextYear 2010 | ||
-} | ||
iterate : ((comparable, v) -> Maybe (comparable, v)) -> (comparable, v) -> DictList comparable v | ||
iterate f x = | ||
List.Extra.iterate f x | ||
|> fromList | ||
|
||
{-| Decompose a list into its head and tail. If the list is empty, return `Nothing`. Otherwise, return `Just (x, xs)`, where `x` is head and `xs` is tail. | ||
|
||
uncons (fromList [(1, 1),(2, 2),(3, 3)] == Just ((1, 1), fromList [(2, 2), (3, 3)]) | ||
uncons empty = Nothing | ||
-} | ||
uncons : DictList comparable v -> Maybe ( (comparable, v), DictList comparable v) | ||
uncons xs = | ||
toList xs | ||
|> List.Extra.uncons | ||
|> Maybe.map (\(a, la) -> (a, fromList la)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one can also be optimized by working directly on the |
||
|
||
{-| Find the first maximum element in a list using a comparable transformation | ||
-} | ||
maximumBy : (comparable2 -> a -> comparable1) -> DictList comparable2 a -> Maybe (comparable2, a) | ||
maximumBy f ls = | ||
toList ls | ||
|> List.Extra.maximumBy (uncurry f) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one looks fine to me -- I don't think there's an optimization possible using the internals, since we'll have to get all the values one way or another. I suppose we might be able to avoid constructing the intermediate list by using a |
||
|
||
{-| Find the first minimum element in a list using a comparable transformation | ||
-} | ||
minimumBy : (comparable2 -> a -> comparable1) -> DictList comparable2 a -> Maybe (comparable2, a) | ||
minimumBy f ls = | ||
toList ls | ||
|> List.Extra.minimumBy (uncurry f) | ||
|
||
{-| Take elements in order as long as the predicate evaluates to `True` | ||
-} | ||
takeWhile : ((comparable, a) -> Bool) -> DictList comparable a -> DictList comparable a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the signature of the callback function, I've mostly been preferring |
||
takeWhile predicate xs = | ||
toList xs | ||
|> List.Extra.takeWhile predicate | ||
|> fromList | ||
|
||
{-| Drop elements in order as long as the predicate evaluates to `True` | ||
-} | ||
dropWhile : ((comparable, a) -> Bool) -> DictList comparable a -> DictList comparable a | ||
dropWhile predicate list = | ||
toList list | ||
|> List.Extra.dropWhile predicate | ||
|> fromList | ||
|
||
{-| Remove duplicate values, keeping the first instance of each element which appears more than once. | ||
|
||
unique [0,1,1,0,1] == [0,1] | ||
-} | ||
unique : DictList comparable1 comparable2 -> DictList comparable1 comparable2 | ||
unique list = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ... |
||
toList list | ||
|> List.Extra.unique | ||
|> fromList | ||
|
||
{-| Drop duplicates where what is considered to be a duplicate is the result of first applying the supplied function to the elements of the list. | ||
-} | ||
uniqueBy : (comparable1 -> a -> comparable2) -> DictList comparable1 a -> DictList comparable1 a | ||
uniqueBy f list = | ||
toList list | ||
|> List.Extra.uniqueBy (uncurry f) | ||
|> fromList | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
{-| Indicate if list has duplicate values. | ||
|
||
allDifferent [0,1,1,0,1] == False | ||
-} | ||
allDifferent : DictList comparable1 comparable2 -> Bool | ||
allDifferent list = | ||
-- @TODO Should this method just use the values, or also the keys | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
values list | ||
|> List.Extra.allDifferent | ||
|
||
{-| Indicate if list has duplicate values when supplied function are applyed on each values. | ||
-} | ||
allDifferentBy : (comparable1 -> a -> comparable2) -> DictList comparable1 a -> Bool | ||
allDifferentBy f list = | ||
toList list | ||
|> List.Extra.allDifferentBy (uncurry f) | ||
|
||
{-| Map functions taking multiple arguments over multiple lists. Each list should be of the same length. | ||
|
||
((\a b c -> a + b * c) | ||
|> flip map [1,2,3] | ||
|> andMap [4,5,6] | ||
|> andMap [2,1,1] | ||
) == [9,7,9] | ||
-} | ||
andMap : DictList comparable a -> DictList comparable (a -> b) -> DictList comparable b | ||
andMap l fl = | ||
let | ||
keyList = keys l | ||
lList = values l | ||
flList = values fl | ||
in | ||
List.Extra.andMap lList flList | ||
|> List.Extra.zip keyList | ||
|> fromList | ||
|
||
{-| Equivalent to `concatMap`. For example, suppose you want to have a cartesian product of [1,2] and [3,4]: | ||
|
||
[1,2] |> andThen (\x -> [3,4] | ||
|> andThen (\y -> [(x,y)])) | ||
|
||
will give back the list: | ||
|
||
[(1,3),(1,4),(2,3),(2,4)] | ||
|
||
Now suppose we want to have a cartesian product between the first list and the second list and its doubles: | ||
|
||
[1,2] |> andThen (\x -> [3,4] | ||
|> andThen (\y -> [y,y*2] | ||
|> andThen (\z -> [(x,z)]))) | ||
|
||
will give back the list: | ||
|
||
[(1,3),(1,6),(1,4),(1,8),(2,3),(2,6),(2,4),(2,8)] | ||
|
||
Advanced functional programmers will recognize this as the implementation of bind operator (>>=) for lists from the `Monad` typeclass. | ||
-} | ||
andThen : (comparable -> a -> DictList comparable b) -> DictList comparable a -> DictList comparable b | ||
andThen = | ||
concatMap | ||
|
||
concatMap : (comparable -> a -> DictList comparable b) -> DictList comparable a -> DictList comparable b | ||
concatMap f xs = empty | ||
-- map f xs | ||
-- |> toList | ||
-- |> values | ||
-- |> concat | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yes, it isn't obvious to me either what There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: Note: Dict.Map doesn't implement that in haskell For
What could this function do:
In this world you could reimplement:
Does it even remotely makes sense? TL;DR
|
||
----------- | ||
-- Internal | ||
----------- | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
module ListExtraTests exposing (tests) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
That is, instead of working from Then, in a separate test file, you could test anything else you want to test -- i.e. things that |
||
{-| This is an adaptation of the `List` tests in elm-lang/core, in order | ||
to test whether we are a well-behaved list. | ||
-} | ||
|
||
import Test exposing (..) | ||
import Expect exposing (Expectation) | ||
import Maybe exposing (Maybe(Nothing, Just)) | ||
import DictList exposing (..) | ||
import List.Extra | ||
|
||
|
||
tests : Test | ||
tests = | ||
describe "List Tests" | ||
[ testListOfN 0 | ||
, testListOfN 1 | ||
, testListOfN 2 | ||
, testListOfN 5000 | ||
] | ||
|
||
|
||
toDictList : List comparable -> DictList comparable comparable | ||
toDictList = | ||
List.map (\a -> ( a, a )) >> DictList.fromList | ||
|
||
|
||
testListOfN : Int -> Test | ||
testListOfN n = | ||
let | ||
xs = | ||
List.range 1 n |> toDictList | ||
|
||
xsOpp = | ||
List.range -n -1 |> toDictList | ||
|
||
xsNeg = | ||
foldl cons empty xsOpp | ||
|
||
-- assume foldl and (::) work | ||
zs = | ||
List.range 0 n | ||
|> List.map (\a -> ( a, a )) | ||
|> fromList | ||
|
||
sumSeq k = | ||
k * (k + 1) // 2 | ||
|
||
xsSum = | ||
sumSeq n | ||
|
||
mid = | ||
n // 2 | ||
in | ||
describe (toString n ++ " elements") | ||
[ test "last" <| | ||
\() -> | ||
if n == 0 then | ||
Expect.equal (Nothing) (last xs) | ||
else | ||
Expect.equal (Just ( n, n )) (last xs) | ||
, test "inits" <| | ||
\() -> | ||
if n == 0 then | ||
Expect.equal [empty] (inits empty) | ||
else | ||
Expect.equal [empty, fromList [(1,1)], fromList [(1,1), (2,2)]] (inits (fromList [(1,1), (2,2)])) | ||
, test "(!!)" <| | ||
\() -> | ||
if n == 0 then | ||
Expect.equal Nothing ((!!) empty 0) | ||
else | ||
Expect.equal (Just (n, n)) ((!!) xs (n-1)) | ||
, test "uncons" <| | ||
\() -> | ||
if n == 0 then | ||
Expect.equal Nothing (uncons empty) | ||
else | ||
-- @TODO Generalize | ||
Expect.equal (Just ((1, 1), fromList [(2, 2), (3, 3)])) (uncons (fromList [(1, 1), (2, 2), (3, 3)])) | ||
] |
There was a problem hiding this comment.
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.