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]
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.
Ok, that makes sense.
Doesn't Haskell discourage them because there are a bunch of other, better ways to use them using Monads?
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.
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
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]
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.
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.
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.
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.
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]
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 aThis 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 bThis 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 cThis 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 dLet'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.
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.
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
The example at https://github.com/danfinch/fmarkup shows how much nicer Elm could be with list comprehensions imo.
Nice is subjective. Personally I found that much less readable than what we have today.
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!
@boxed You will get better results if you ask about this problem independent of new language features on elm-discuss, r/elm, or slack.
Sure, but this is where people end up when googling so this is the closest thing you've got to documentation. So if I get an answer somewhere else, that just gets buried and the next person has to ask again, and the next, ad adfinitum.
Most helpful comment
Sure, but this is where people end up when googling so this is the closest thing you've got to documentation. So if I get an answer somewhere else, that just gets buried and the next person has to ask again, and the next, ad adfinitum.