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

Request: add list comprehensions #147

Closed
deadfoxygrandpa opened this issue May 4, 2013 · 19 comments
Closed

Request: add list comprehensions #147

deadfoxygrandpa opened this issue May 4, 2013 · 19 comments
Labels

Comments

@deadfoxygrandpa
Copy link
Contributor

I suggest that list comprehensions be added to Elm. They see heavy use in languages that implement them. Considering the existing similarities between Elm and Haskell syntax, I propose using Haskell style list comprehensions, like this:

[(x, y) | x <- [1..10], y <- [1..20], x /= y]
@evancz
Copy link
Member

evancz commented May 4, 2013

Haskell actually discourages the use of list comprehensions in their style guide. I think they are cool and exciting, but I have never found a use for them in real Haskell code. It is much nicer in Python, but I think that's because you can't easily do the functional version in the first place.

This is not on the roadmap for now.

@deadfoxygrandpa
Copy link
Contributor Author

Ok, that makes sense.

@ghost
Copy link

ghost commented Sep 8, 2013

Doesn't Haskell discourage them because there are a bunch of other, better ways to use them using Monads?

@evancz
Copy link
Member

evancz commented Sep 8, 2013

See the reasoning in the style guide I linked. It does not mention using Monads instead. I cannot speak for the larger Haskell community, which perhaps tells people to favor do notation.

Overall I feel that both list comprehensions and monads are overkill for the handfull of cases where they really do make things slightly shorter.

@JeffreyVest
Copy link

I'm trying to understand then how to do something simple like the code below. It uses the applicative style. It creates a cartesian style join between the years and the months. Lacking this applicative style, I was thinking ok I'll do it as a list comprehension. Then I see that's not supported because a Haskell style guide frowns on it. I'm sure I'm lacking something conceptual here, but how do I do something as simple as a cartesian join between lists?

monthlyDates dom = take 12 $ fromGregorian <$> [2015..] <> [1..12] <> pure dom

@Apanatshka
Copy link
Contributor

Rewrite from Haskell to Elm:

monthlyDates dom = take 12 $ fromGregorian <$> [2015..] <*> [1..12] <*> pure dom

--remove infinite list-->

monthlyDates dom = fromGregorian <$> [2015] <*> [1..12] <*> pure dom

--apply fmap (<$>)-->

monthlyDates dom = [fromGregorian 2015] <*> [1..12] <*> pure dom

--singleton list = pure, so use fmap f x = pure f <*> x-->

monthlyDates dom = fromGregorian 2015 <$> [1..12] <*> pure dom

--applicative interchange law-->

monthlyDates dom = ($ dom) <$> (fromGregorian 2015 <$> [1..12])

--I'm doing this step on intuition-->

monthlyDates dom = (\m -> fromGregorian 2015 m dom) <$> [1..12]

--replacing the general fmap operator with the map from Data.List-->

monthlyDates dom = map (\m -> fromGregorian 2015 m dom) [1..12]

--valid Elm, and much more readable in my opinion-->

monthlyDates dom = List.map (\m -> fromGregorian 2015 m dom) [1..12]

Final note, if you want a cartesian join between lists, you can use List.concatMap:

andMap : List (a -> b) -> List a -> List b -- like (<*>) in Haskell, specialised to lists
andMap listOfFuncs listOfAs = List.concatMap (\f -> List.map f listOfAs) listOfFuncs

gridOf12x24 = List.map (,) [1..12] `andMap` [1..24]

@JeffreyVest
Copy link

You committed a fairly major cheat in your steps. You turned my list of years into a single value. Given the day of month happens to also be a single value, the whole step by step section completely side stepped my question by focusing on producing a solution to the one example without actually attending to the conceptual question I asked. Now extend your example to do multiple years or start at a month other than January. I would like a solution that does so as an easy conceptual and syntactic step, as it would be in the Applicative style.

I won't respond to readability since you didn't produce an equivalent solution. I understand you happen to have produced the equivalent list, but the construction methods leave me quite hamstrung to take next steps, like apply it starting at the current month and go for 12 months from there, for instance. Again, the conceptual question I was asking is how to do something as simple as combining two lists in a cartesian style. The standard answer I found for Haskell was list comprehensions. So now apparently lacking both list comprehensions and <*>, I was stuck as to how to do something like that, which to me is extremely basic.

andMap though looks exactly like what I'm looking for. So thank you for that. That uses an applicative style. Without giving it much thought this late at night, I'm assuming this would leave it partially applied if instead of (,) you had used a ternary function for instance. I assume then you can just pass another list with another andMap. It's true, in my particular case, I can use a straight value for the third argument, but I find having it just be another value in the list of values in the Applicative style is more readable and easier to understand how to change it for something like, say, wanting to apply multiple days at once.

@JeffreyVest
Copy link

andMap is for Tasks, not for lists as you indicated. So I'm back to not knowing how to do join two list in a cartesian style, lacking both list comprehensions and Applicative List.

@Apanatshka
Copy link
Contributor

andMap is not in the core libraries, that's why I gave the implementation for it. It's the usual name for this function, it's defined for Tasks because that type is an applicative functor too.

@JeffreyVest
Copy link

Ah ok. My apologies. So concatMap is like SelectMany in Linq (I have a primarily C# background). That's really good to know too. And you used that to build an Applicative List implementation. Very nice. Thanks for your help with this. I really want to use Elm, but I'm getting hung up on some things I've learned in Haskell that seemed very nice and I'm trying to apply it in Elm.

@JeffreyVest
Copy link

Using your andMap (calling it 'ap' for brevity, inspired by Ramda), I can now do the following, which is some very recognizable Applicative style. Thanks again.

monthlyBills = List.map (,,) [2015..2020] ap [1..12] ap [5]

@joneshf
Copy link

joneshf commented May 5, 2015

Warning, this is kind of long.

Another approach to arrive at a similar solution is to notice:

fromGregorian <$> [2015..] <*> [1..12] <*> pure dom 
    == liftA3 fromGregorian [2015..] [1..12] (pure dom)

And since liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d, you just want something monomorphized for elm.
So let's write lift3 : (a -> b -> c -> d) -> List a -> List b -> List c -> List d.

N.B. This is not map3 in the List module. In haskell map3 is liftA3 for the ZipList instance, which is not the same as liftA3 for the [] instance.

I feel this is instructive to help solidify the relationship between Functor and Applicative. @Apanatshka's explanation helps show the relationship between Applicative and Monad--since List.concatMap == (=<<).

It's instructive to look at simpler examples first, and build up to lift3.

lift0 : a -> List a

This odd little function should be trivial to implement.
Surprisingly, it doesn't exist in the List module.

lift0 : a -> List a
lift0 x = [x]

This is pure for the Applicative instance of [] in Haskell.

lift1 : (a -> b) -> List a -> List b

This also looks pretty simple to implement. And it is the same as map in the List module, though we take a less efficient approach to it.

lift1 : (a -> b) -> List a -> List b
lift1 f xs = case xs of
  []       -> []
  (x::xs') -> lift0 (f x) ++ lift1 f xs'

This is fmap for the Functor instance of [] in Haskell.
There's more than one way to define this, you can use a fold, or eschew the lift0 and cons straight on, but I think it makes for clearer understanding this way as to how to go to the next one.

lift2 : (a -> b -> c) -> List a -> List b -> List c

This looks a bit more complex, but still doable. And with this definition, we can start to see a pattern forming.

lift2 : (a -> b -> c) -> List a -> List b -> List c
lift2 f xs ys = case xs of
  []       -> []
  (x::xs') -> lift1 (f x) ys ++ lift2 f xs' ys 

The basic pattern is to case on the first List a. If it's [] then we're done. If it's (x::xs') then we partially apply the x to f, and "lift" one level lower, then recurse on the rest of the list and concatenate them together.

lift3 : (a -> b -> c -> d) -> List a -> List b -> List c -> List d

Let's see if we can apply this pattern to lift3.

lift3 : (a -> b -> c -> d) -> List a -> List b -> List c -> List d
lift3 f xs ys zs = case xs of
  []       -> []
  (x::xs') -> lift2 (f x) ys zs ++ lift3 f xs' ys zs

Looks like we can! So now your function could be:

monthlyDates dom = lift3 fromGregorian [2015..2020] [1..12] [dom]
  |> List.take 12

Using more elmish idioms.

What's interesting to note here is that you can sort of apply the basics of recursion at the design level, not just with the function implementations.
Here, the base case is lift0 and each case above that builds up on that. Though I guess that's more inductive, but whatever.

The important part is that there's no magic here! It's just pattern matching and recursive functions. There's not much special about List a except that is is polymorphic. If you want to define similar functions for some other data type like Maybe a or Result a b (though the Result module already has these), you can do that as well. You don't have to rely on List.concatMap or Maybe.andThen or Result.andThen existing. And you can extend it further with lift4, lift5, etc. Those are instructive to try and ensure you understand what's going on.

@JeffreyVest
Copy link

Thank you for taking the time to put that together @joneshf. Amazingly, I understood all that perfectly, with the only exception being the use of monomorphism. I've read the concepts before but it didn't quite connect there for me.

@joneshf
Copy link

joneshf commented May 9, 2015

Great! Glad it wasn't all mumbo. :)

Sorry, that was supposed to read, "...slightly monomorphized...". Though maybe it would have been better to say "less polymorphic". I just meant that instead of:

liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d

Which works for any Applicative instance (regardless of the implementation). You choose the Applicative instance to be List, so you end up with:

lift3 : (a -> b -> c -> d) -> List a -> List b -> List c -> List d

@boxed
Copy link

boxed commented Jun 25, 2017

The example at https://github.com/danfinch/fmarkup shows how much nicer Elm could be with list comprehensions imo.

@robinheghan
Copy link

Nice is subjective. Personally I found that much less readable than what we have today.

@boxed
Copy link

boxed commented Jun 26, 2017

Maybe I'm doing elm wrong. I recently had do do this:

ingredientView : Model -> Ingredient -> Html Msg
ingredientView model ingredient =
    div []
        [ text (prettyRound (model * ingredient.quantity))
        , text " "
        , text ingredient.unit
        ]


view : Model -> Html Msg
view model =
    div []
        [ div [] (List.map (ingredientView model) ingredients)
        , button [ onClick Decrement ] [ text "-" ]
        , button [ onClick Increment ] [ text "+" ]
        ]

where really I wanted to do

view : Model -> Html Msg
view model =
    div []
        [ div []
            [ for ingredient in ingredients
              div []
                  [ text (prettyRound (model * ingredient.quantity))
                  , text " "
                  , text ingredient.unit
                  ]]
        , button [ onClick Decrement ] [ text "-" ]
        , button [ onClick Increment ] [ text "+" ]
        ]

The currying is pretty ugly I think. In this case it's not horrible because I've written the function to be curried so I can make sure the parameters are in the right order, but if they needed to be the reverse order, I'd have to put in an anonymous function there. Compared to a list comprehension it's quite verbose, but it can easily become even worse.

I'm open to suggestions for how to improve my thinking!

@mgold
Copy link
Contributor

mgold commented Jun 26, 2017

@boxed You will get better results if you ask about this problem independent of new language features on elm-discuss, r/elm, or slack.

@boxed
Copy link

boxed commented Jun 27, 2017 via email

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

No branches or pull requests

8 participants