[HN Gopher] Comparison of Haskell and Julia List Comprehensions
       ___________________________________________________________________
        
       Comparison of Haskell and Julia List Comprehensions
        
       Author : todsacerdoti
       Score  : 56 points
       Date   : 2021-07-07 15:41 UTC (7 hours ago)
        
 (HTM) web link (blog.lojic.com)
 (TXT) w3m dump (blog.lojic.com)
        
       | moomin wrote:
       | I mean, the irony here for me is that I regard Haskell's list
       | comprehensions as a bit of a bag on the side. There's not much
       | that couldn't be equally readably implemented using other Haskell
       | syntax.
        
       | tikhonj wrote:
       | One feature I rather like in Haskell is being able to _zip_ with
       | list comprehension syntax[1]:                   l> [(x, y) | x <-
       | [1..] | y <- [1..3]]         [(1,1),(2,2),(3,3)]
       | 
       | This example is a bit silly--I would just write zip [1..]
       | [1..3]--but it makes more complex expressions involving zips much
       | easier to read at a glance. I looked through some real code I've
       | written, and being able to do things like this is definitely
       | nice:                   toHashMap values = HashMap.fromList
       | [ convert name value           | name  <- Schema.fldName <$>
       | Schema.fields t           | value <- Vector.toList values
       | ]
       | 
       | Of course, this particular example would be nicer if Haskell had
       | something like dictionary comprehensions for building hash maps,
       | but this isn't an awful substitute, and zipping multiple
       | sequences together is useful in a lot of other configurations as
       | well!
       | 
       | [1]: I should add that this requires enabling the somewhat
       | misleadingly named `ParallelListComp` extension. Frankly, I think
       | extensions like this should just be added to the language--this
       | introduces new syntax, so it never changes previously valid code
       | --but the Haskell community is very conservative about making
       | changes to the core, standardized part of the language :/.
        
         | ssivark wrote:
         | That actually seems confusing. I would normally expect such
         | code to perform a Cartesian product i.e. _all_ pairs of (x,y).
        
           | dwohnitmok wrote:
           | It would be a Cartesian product if the second `|` was changed
           | to `,` (as is the case in the article and most examples in
           | this comment section).
        
       | DNF2 wrote:
       | Are there multidimensional comprehensions in Haskell? In Julia
       | you have                 jl> [ (x,y) for x in 1:3, y in 2:5 ]
       | 3x4 Matrix{Tuple{Int64, Int64}}:          (1, 2)  (1, 3)  (1, 4)
       | (1, 5)          (2, 2)  (2, 3)  (2, 4)  (2, 5)          (3, 2)
       | (3, 3)  (3, 4)  (3, 5)
        
         | jolmg wrote:
         | You can have them if you can define Matrix as a data structure
         | with a Monad instance. I don't know if there are any packages
         | that do something like that.
        
       | codesections wrote:
       | The Raku syntax for list comprehension[0] is very similar as
       | well.
       | 
       | Julia:                   [ x^2 for x = 1:5 ]
       | 
       | Raku:                   do $_2 for 1..5;
       | 
       | [0]: https://docs.raku.org/language/py-
       | nutshell#List_comprehensio...
        
       | maweki wrote:
       | Is Julia eager? In Haskell you can also do infinite lists
       | [1..]
       | 
       | This generates all the integers. The first example
       | [ x^2 | x <- [1..5] ]
       | 
       | Is basically already list comprehension within list
       | comprehension. Are infinite lists possible?
        
         | bobbylarrybobby wrote:
         | Julia's Iterators package has infinite iterators but I don't
         | think there's syntax for them
        
           | oscardssmith wrote:
           | If you use parens instead of brackets, you get lazy
           | comprehensions in Julia.
        
       | dwohnitmok wrote:
       | Haskell's comprehension syntax is very powerful and quite a bit
       | more general than what is described here.
       | 
       | It can both be generalized to arbitrary monads through
       | `MonadComprehension` and has extended syntax that brings in basic
       | SQL operators (such as grouping and sorting) through
       | `TransformListComp` as well as zipping through
       | `ParallelListComp`.
       | 
       | The links are respectively
       | https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/mona...,
       | https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/gene...,
       | and
       | https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/para....
       | 
       | And what's more is that all three of these play very nicely with
       | each other, so that you can write SQL-like syntax to deal with
       | say an arbitrary, side-effectful stream rather than a finite data
       | structure.
       | 
       | That means you can write something like (to take the example from
       | the `TransformListComp` docs)                 output = [ (the
       | dept, sum salary)                | (name, dept, salary) <-
       | employees                , then group by dept using groupWith
       | , then sortWith by (sum salary)                , then take 5 ]
       | 
       | where `employees` is e.g. a paginated HTTP endpoint that you're
       | hitting.
       | 
       | It can make for some very nice code.
        
         | mapgrep wrote:
         | This sounds something like transducers in Clojure, I'd be
         | curious to see how they compare! Fascinating to see similar
         | ideas emerging in various places. (It's possible Clojure
         | borrowed some concepts here as I know Rich Hickey often
         | discusses how Haskell has solved a particular problem when
         | discussing a Clojure approach.)
        
           | dwohnitmok wrote:
           | This is different from transducers in Clojure (note how
           | `dept` and `salary` show up in multiple levels despite not
           | being manually threaded through). This is much more similar
           | to the way SQL syntax works.
           | 
           | Transducers in Haskell (and really in any language, including
           | in Clojure) are functions of the form `a -> List b` (yes this
           | is a fully polymorphic transducer despite the use of `List`;
           | it can be applied to concrete data structures as well as e.g.
           | streams of the form found in core.async, I can explain more
           | if you're interested).
           | 
           | I actually think the design of transducers is a bit of a wart
           | in Clojure. Clojure usually goes for the least powerful
           | abstraction (so e.g. data < higher-order functions < macros),
           | but for some reason when designing transducers decided to
           | move away from the simple `List` based approach and instead
           | do a Church encoding of the list to end up with a higher-
           | order function. I wonder if it was performance related
           | (although I'd be slightly surprised to see a higher-order
           | function performing better)?
           | 
           | One of these days I hope to see a library expressing the
           | alternative form of transducers since I find those much much
           | easier to understand and write from scratch without pre-
           | existing transducer generating functions.
        
         | mbauman wrote:
         | Conversely, Julia's comprehension syntax is very powerful and
         | quite a bit more general than what is described here -- but in
         | a different way.
         | 
         | - They're not _list_ comprehensions, they 're one-dimensional
         | _vector_ comprehensions. And they seamlessly extend to
         | multidimensional _array_ comprehensions.
         | 
         | - Just drop the square brackets and instead of a dense array
         | you get a lazy generator!
        
           | anyfoo wrote:
           | Correct me if I'm wrong, but doesn't them being Monad
           | comprehensions in Haskell automatically mean that Haskell's
           | "list" comprehensions can be all of those things as well, and
           | more? As well as the language's laziness by default that
           | pretty much encompasses the generator aspect in general
           | anyways.
           | 
           | (Although I'm not really trying to pit anything against each
           | other here, they are different languages with different
           | requirements.)
        
       | moonchild wrote:
       | APL has no list comprehensions, but can generally be as clear and
       | concise (if not more so) using ordinary array operations. Re-
       | implemented:                 2*[?][?]5       1 2 3 [?]., 4 5
       | ([?],"[?]+[?][?]1+3-[?])"1 2 3   [?]ok, this one is not so hot,
       | but apl has no primitive for x..y       concat - [?]
       | [?]primitive       firsts - [?][?]-1          [?]leading-axis
       | version       firsts - [?][?]1           [?]trailing-axis version
       | firsts - [?]"            [?]nested version       length - +/1[?]"
       | [?]this counts!       factors - [?]0=[?]|[?]       [?]doesn't
       | include the number itself       prime  - (,1) [?] factors
       | primes - (prime[?]0{[?]/[?]}[?])[?]  [?]{[?]/[?]} is a historical
       | artefact, please ignore it       find   -
       | (1[?][?]){[?]/[?]}[?]9=0[?][?]  [?]{[?]/[?]} is very sorry for
       | its repeat appearance       pairs  - 2 ,/ [?]       sorted - [?]/
       | 2<=/[?]       positions - [?]=       is_lower - {sorted [?]ucs
       | 'a'[?]'z'}       lowers - +/(is_lower[?]0)       count  - +/=
       | 
       | Note many of these are polymorphic wrt rank (or can trivially be
       | made so), where the haskell and julia implementations only work
       | on rank-1 ([a]) or rank-2 ([[a]]) arrays.
        
         | dwohnitmok wrote:
         | The Haskell one is polymorphic with respect to rank (and is
         | indeed polymorphic even with respect to whether it's backed by
         | a data structure or not).
         | 
         | Haskell's infelicities for numeric code come from other places.
        
           | moonchild wrote:
           | For those haskell functions which operate on lists of
           | arbitrary items, the items can indeed be list. However, the
           | code is still fundamentally item-oriented. Those haskell
           | functions which operate on lists of specific types (such as
           | 'lowers', which operates on lists of characters) cannot
           | readily be applied to higher-ranked lists of the same.
        
       | occamrazor wrote:
       | Julia's syntax is the same as Python's. It's very natural for
       | short expressions, a bit cumbersome for multiline comprehension,
       | because there is no obvious way to format the code.
       | 
       | Another gripe I have is the order of the for clauses:
       | [x for xs in xss for x in xs]
       | 
       | should, in my opinion, be instead written as                   [x
       | for x in xs for xs in xss]
       | 
       | Also Haskell gets the order of '<-' clauses wrong, so probably
       | mine is a minority opinion.
        
         | berlinquin wrote:
         | I can make sense of nested Python list comprehensions if I read
         | them as shorthand for nested for loops, in which case you need
         | to define variables in the outer loop before you can define
         | dependent variables in the inner loop.
         | 
         | e.g. with your example above,                 for xs in xss:
         | for x in xs:               x
         | 
         | What's confusing about the syntax is that `x` in your example
         | above comes first, whereas in actual nested loops it comes
         | last. So maybe:                 [for xs in xss for x in xs : x]
         | 
         | Could be alternative syntax that moves `x` to the end?
        
         | BadInformatics wrote:
         | Note that because blocks in Julia are expressions, you can do
         | multi-line expressions in comprehensions that you wouldn't be
         | able to do in Python:                   [            let y = x
         | + 1              z = 2y              -z            end
         | for x in 1:5         ]
        
           | eesmith wrote:
           | This specific case could be approximated with something like:
           | >>> [(y:=x+1, z:= 2*y, -z)[-1] for x in range(1, 6)]
           | [-4, -6, -8, -10, -12]
        
         | romwell wrote:
         | I'm with you on that minority opinion.
         | 
         | The "for" side mimics nested for loops structure, but then the
         | output statement goes first (?!).
         | 
         | It's the same feel as MM/DD/YYYY date format: very widespread,
         | yet absolutely asinine to anyone seeing it for the first time
         | :)
        
         | jolmg wrote:
         | > Also Haskell gets the order of '<-' clauses wrong, so
         | probably mine is a minority opinion.
         | 
         | That's probably so it's more in-line with do-notation:
         | [x | xs <- xss, x <- xs]
         | 
         | is the same as:                 do { xs <- xss; x <- xs; return
         | x }
         | 
         | Maybe writing statements backwards was deemed more confusing.
         | 
         | Personally, I don't really use list comprehension syntax, but
         | maybe I would if they were backwards like you describe. It
         | would make more sense with the position of the return value
         | being to the left.
        
         | dmitriid wrote:
         | Haskell and Erlang basically go for mathematical notation.
         | Python and Julia more or less go for how you read those
         | notations.
         | 
         | I personally prefer math-like notation because I find it much
         | more readable and composable.                 x, such as x is
         | in xs, xs is in xss            x | x [?] xs, xs [?] xss # my
         | math is rusty             [x | x <- xs, xs <- xss] # haskell,
         | erlang is isimilar            [ x for xs in xss for x in xs ] #
         | python, julia, order might be different
         | 
         | As soon as you add conditions/filters on data, math-like
         | notation wins hands down
         | 
         | Edit:
         | 
         | Since I don't know Haskell, apparently in Haskell the order is
         | wrong. So, only Erlang gets it right (variables are upper case
         | in Erlang):                 [ X || X <- Xs, Xs <- Xss]
        
           | jolmg wrote:
           | > only Erlang gets it right... [ X || X <- Xs, Xs <- Xss]
           | 
           | I don't have much experience with Erlang, but that also seems
           | wrong:                 1> Xss = [[1]].       [[1]]       2> [
           | X || X <- Xs, Xs <- Xss].         * 1:13: variable 'Xs' is
           | unbound       3> [ X || Xs <- Xss, X <- Xs].       [1]
        
             | dmitriid wrote:
             | Damnit. I haven't used Erlang in a long time, but I
             | should've known that.
             | 
             | So, no language does it "right".
        
       | montalbano wrote:
       | Note that in Julia these are often referred to as array
       | comprehensions, as the data structure involved is a (very
       | efficient) array. C.f. Python
        
         | kryptiskt wrote:
         | And in Haskell they are monad comprehensions these days, so
         | they can build more than lists. They actually used to be so
         | back in the 90s, but Haskell 98 was conservative and restricted
         | them to lists.
        
           | exdsq wrote:
           | What do monad comprehensions offer?
        
             | thewakalix wrote:
             | List comprehensions are a concise way of writing do-
             | expressions in the list monad, so monad comprehensions just
             | extend that to all monads.
        
             | jolmg wrote:
             | Being very generic. One can define a very efficient array
             | structure, define its Monad instance and be able to use it
             | in comprehensions. The point is that comprehensions are not
             | fixed to a particular data structure by the language, and
             | can be used with any user-definable Monad.
        
       ___________________________________________________________________
       (page generated 2021-07-07 23:01 UTC)