1. 11

  2. 12

    This doesn’t even scratch the surface of Haskell comprehensions. They’re a full SQL-like sublanguage and they can operate over non-list Monads.

    Some of their SQL-like abilities https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/generalised_list_comprehensions.html#generalised-list-comprehensions

    output = [ (the dept, sum salary)
             | (name, dept, salary) <- employees
             , then group by dept using groupWith
             , then sortWith by (sum salary)
             , then take 5 ]

    All of the examples above are filters & maps, but with parallel comprehensions they can behave like zips: [ (x+y) | x <- [1..10] | y <- [11..20] ]

    Monad comprehensions are even more powerful. This syntax works for anything, not just a list. It works for optional values, non-deterministic code, stateful code, generating actual database queries, etc.

    1. 4

      See also Wadler 1990, “Comprehending Monads”. I read this when I was first trying to grok monads, and it clarified a lot of things for me. After using Python, seeing that monads are “just” list comprehensions really helped!

      1. 2

        Hah. Yes, the list of things that monads “just” are is endless from “just list comprehensions” to “just burritos”. It’s better to focus on specific monads that are interesting and get a feel for them rather than looking to analogies. No one analogy is really enough. For example, if you think of them as list comprehensions it’s harder to see what transformers or comonads would be about.

      2. 1

        I mean, if you turn on more extensions they get more features, but naked Haskell they don’t do quite so many things :)

        Conversely, in naked Haskell you have do which works for any Monad, including list, and has almost-identical syntax to the comprehensions

        1. 4

          Just because your compiler might, without any flags, default to “naked C++” meaning C++98 doesn’t mean that this is what C++ is today :)

          Extensions are how Haskell evolves and they represent the language as it is today. This is like writing an article comparing C++ to Java but silently using C++98 (pretty much exactly because “naked Haskell” is Haskell 98). Missing out on 20 years of development will make anything seem less interesting.

          It’s really unfortunate that we call them extensions. Maybe language feature flags or something would have been better.

          Actually, you’re more right than you think. Even more modern comprehensions are simple to convert to do notation.

          1. 1

            Naked Haskell is at least Haskell2010, not 98! The fair comparison for new C++ versions is new Haskell versions. The fair comparison for Haskell extensions in C++ land would be things like GNU C++. Haskell does a much better job of course by naming all extensions and making them source-level opt-in.

            And yes, that comprehensions can be written with do was my whole point :)

            1. 3

              Haskell2010 is not an actual language standard. It continues the terrible messaging of the community. Haskell98 describes the language in 1998, it’s a language standard. The community tried to write a new language standard, to describe new Haskell (Haskell Prime). And it was a disaster (repeatedly). Haskell 2010 is a minor update to 98 that doesn’t even describe the language as it was in 2000 never mind 2010. This is all described and explained in the report.

              Even if there is an updated language spec one day, and there may never be, it would not change the situation with extensions. The language will always work by having a ~98 core and then having you enable extensions on top of it. All because this has been such an incredibly successful way of evolving the language.

              Extensions aren’t 2nd tier in any way. Haskell in 2021 is GHC’s implementation plus a bunch of extensions enabled.

        2. 1

          Most of the time I use list comprehensions as a replacement for loops. I was not aware that Haskell has these capabilities. I try to use the subset that works in Python, Erlang and other languages.

        3. 5

          Am I the only person who dislikes list comprehensions in all their forms? Using a list comprehension feels to me like doing recursion by hand: something occasionally useful but best avoided and use the base building blocks (map, filter, etc) directly instead.

          1. 8

            I’m the opposite, it’s the closest I can get to set-builder notation in a programming language.

            1. 2

              Here is one case where I think list comprehensions are the cleanest solution, how would you do this?

              f1 :: a -> Maybe m
              f2 :: a -> Maybe n
              aList :: [a]
              myMap :: Map.Map m n
              myMap = Map.fromList $ extractTwoJustsFromTuple $ map (f1 &&& f2) aList

              How would you implement extractTwoJusts...? With list comprehensions, you hardly even need the helper function:

              Map.fromList $ [ (x, y) | (Just x, Just y) <- map (f1 &&& f2) aList ]

              The gist is that it provides a very concise ability to skip failed pattern matches.

              1. 2

                Here is what springs to mind:

                Map.fromList $ mapMaybe (\a -> (,) <$> f1 a <*> f2 b) aList
              2. 2

                In Haskell at least, I think they have a useful niche (I was just this morning talking with a colleague about [ foo | bar ] as a nice shorthand for if bar then [foo] else []), but beginner material has an inexplicable fascination with them. They’re much better taught as an occasionally-useful niche feature once the student groks the Monad typeclass, as understanding them thoroughly requires desugaring into calls to (>>=) or do-notation.

              3. 1

                Yeah Julia list comprehensions are nice.

                From what I’ve heard, Haskell syntax is designed to look like set builder notation, I guess that’s a plus if you have a math background?

                1. 1

                  In my programming language (Curv), list comprehensions reuse the syntax for the imperative control structures (for, if, etc).

                  Haskell               Julia               Curv
                  [x^2 | x <- [1..5]]   [x^2 for x = 1:5]   [for (x in 1..5) x^2]
                  H: [ x | x <- [1..n], n `mod` x == 0 ]
                  J: [ x for x = 1:n if n % x == 0 ]
                  C: [for (x in 1..n) if (n `mod` x == 0) x]

                  F# syntax is similar to Curv, in that the ‘generator expression’ is at the end, not at the beginning, and it looks more imperative.

                  In Curv, reusing imperative syntax in list comprehensions is a feature. There’s less overall syntax to learn (compared to other imperative languages like Python, for example).

                  1. 1
                    primes n = [ x | x <- [2..n], prime x ]
                    primes(n) = [ x for x = 2:n if prime(x) ]

                    This recurses forever and busts the call stack.

                    1. 2

                      No it doesn’t. prime != primes.

                      1. 1

                        You’re right, my bad.

                      2. 1

                        I don’t currently have a Haskell compiler on my machine, but I just tested the Julia code with Julia 1.6.1, and it works fine. Note - primes depends on prime, and prime depends on factors, so you need to define all three of those functions.