
Haskell vs. Clojure (2014) - kafkaesq
https://gist.github.com/honza/5897359
======
andolanra

        flatten :: ByteString -> [ (String, Int, Int, String) ]
        flatten json = case decode json of
          Nothing -> []
            -- Type inference here can infer that we're using `val` as
            -- :: HashMap String (HashMap String (HashMap String String))
          Just val ->
            let books = ["Genesis", "Exodus" {- ...and so forth -} ]
            in [ (book, chNum, vsNum, verse)
                  -- for every book in the list of books...
               | book <- books
                   -- find the corresponding map
               , Just bookMap <- [HM.lookup book val]
                   -- for every number in the range [1..max] for chapters
               , chNum <- [1..HM.size bookMap]
                   -- pull that map out of the submap
               , Just chMap <- [HM.lookup (show chNum) bookMap]
                   -- ...and for every number in the range [1..max] for verses
               , vsNum <- [1..HM.size chMap]
                   -- ...pull the verse out of the sub-sub map
               , Just verse <- [HM.lookup (show vsNum) chMap]
               ]
    

Okay, this isn't as type-safe or signed-in-triplicate as the supplied Haskell
solution, but if we're comparing size-to-size, then this is pretty terse while
also avoiding partial functions (the supplied Haskell solution uses _read_ ,
which can throw an error if we happen to have a non-numeric key in the wrong
place) and still working within the restrictions of what was given.

~~~
jeletonskelly
One thing true of all bugs, it passed the type checker.

------
joobus
That haskell implementation is less than ideal. If he uses Data.Aeson.Lens,
most of that instance declaration business is not necessary. It would just be
mapping over the chapter keys to create the list of lists, then sorting it.

------
sdegutis
The Clojure code is shorter, but that's almost entirely because it's
dynamically typed. And that's not necessarily a benefit, either. Cheshire just
takes JSON and turns it into plain old Clojure data (maps, vectors, strings or
keywords, etc.) whereas Aeson requires the programmer to manually unroll the
whole structure from top to bottom into user-defined types. But by doing so it
guarantees that if something is missing or unexpected, the code fails early
and fails fast. Plus the static typing enables compile-time checking of all
your code which uses it, rather than how Clojure accesses JSON which is so
often stringly-typed because that's easiest/shortest/fastest.

~~~
fbreduc
hmm still unsure what the types win me then

~~~
pseudonom-
It's pretty handy to only have to be vigilant at the boundary layer. With the
untyped approach, all consumers of the deserialized map have to guard against
data which doesn't conform to expectations. With the typed approach, all
subsequent functions only have to work with data conforming to the
specification as laid out by types.

------
asa400
scala?

    
    
      #!/usr/bin/env amm
      
      import $ivy.`io.circe::circe-core:0.5.1`
      import $ivy.`io.circe::circe-generic:0.5.1`
      import $ivy.`io.circe::circe-parser:0.5.1`
      
      import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._
      
      val bookOrder = Array("Genesis", "Exodus", /* and so forth */).zipWithIndex.toMap
      
      type Bible = Map[String, Map[Int, Map[Int, String]]]
    
      val bible = /* load the json file */
    
      decode[Bible](bible).map { parse =>
        (
          for {
            (bookName, book) <- parse
            (chapterNo, chapter) <- book
            (verseNo, verse) <- chapter
          } yield (bookName, chapterNo, verseNo, verse)
        )
          .toList
          .sortBy { b =>
            (bookOrder.get(b._1), b._2, b._3, b._4)
          }
      }.foreach(_.foreach(println))

------
elliotec
Clojure is beautiful and elegant. If not for it's lack of popularity and
necessity of keeping up with JS constantly, it would be my most used language
especially on side projects.

Clojure is one of my favorite languages. If they get the stack traces/better
errors figured out, it will be damn near perfect IMO.

~~~
hardwaresofton
Keeping up with JS? Maybe you're referring to clojurescript, for that
criticism?

~~~
nafarlee
I think he meant he has to spend so much time keeping up with JS, that he does
not have the chance to use Clojure more.

~~~
elliotec
Yes thank you.

------
WilliamDhalgren
I know barely enough haskell, passively, to dislike quite a lot of this code
(like really, 3 lines on 3 places, just to case on a maybe to throw an error?
w/a just calling fromMaybe or even fromJust? and manual instances??) - but,
could someone more experienced suggest a saner one?

------
codygman
That Haskell example needs some love. There should also be a Haskell lens
example. Perhaps I can help out when I get home.

------
elliotec
One of the best Code Koans is for Clojure:
[http://clojurekoans.com/](http://clojurekoans.com/)

Highly recommended.

------
piaste
In F# you'd normally use a type provider for this kind of work, but for the
sake of exercise:

    
    
        type Chapter = Map<int, string>
        type Book    = Map<int, Chapter>
        type Bible   = Map<string, Book>
        
        let bookOrder = [|"Genesis"; "Exodus"; "Leviticus"; (* etc.. *) |]
    
        let sortChapter = Map.toSeq >> Seq.sortBy fst
                          >> Seq.toArray
    
        let sortBook = Map.map (fun _ -> sortChapter)
                       >> Map.toSeq >> Seq.sortBy fst
                       >> Seq.toArray
    
        let sortBible = Map.map (fun _ -> sortBook)
                        >> Map.toSeq >> Seq.sortBy (fun (bookname, _)-> Array.findIndex ((=) bookname) bookOrder)
                        >> Seq.toArray
    
        BIBLE_TEXT
        |> JsonConvert.DeserializeObject<Bible>
        |> sortBible
        |> printfn "%A"
    

FP 101 suggests you could extract the Map.toSeq >> Seq.sortby x >> Seq.toArray
function, but I don't think it gains any readability, and in any case it's
just an artifact of the problem requiring an array output even though a Map
would make more sense.

------
tombert
I'm reasonably certain I've written JSON-parsing logic in Haskell that's
substantially smaller than this.

------
wyager
This seems like a contrived task constructed to be convenient for Clojure.

------
bitwize
If you just want to get shit done, Clojure is fine (though any other Lisp
would arguably be better than Clojure).

If you want to get shit done, and make sure the types are correct, and make
sure all side effects are properly managed and accounted for, that's Haskell's
niche.

~~~
Zolomon
> ... Clojure is fine (though any other Lisp would arguably be better than
> Clojure).

Care to elaborate why you think any other Lisp is better than Clojure? I would
like to hear your arguments.

~~~
tsm
Not the GP, but for quick things I prefer CL because:

* TIMTOWTDI. Doing everything with map/reduce/filter is great, but sometimes it's more direct to just use the LOOP macro. Immutability is great, but sometimes it's way faster to just SETF something (and not have to worry about atoms).

* Batteries (more) included. IME, I find myself needing to use third-party libraries sooner with Clojure than with CL. Also, QuickLisp is arguably faster to get rolling with than Leiningen (I've found it easier to get the new library into my existing lisp image). And CL tends to have clusters of functions that do similar-but-different things, whereas Clojure puts the cognitive burden on you to do things right (example off the top of my head: CL has REMOVE, REMOVE-IF, and REMOVE-IF-NOT, Clojure has ????? (like ten different ways, I'm serious), `(filter pred list)`, and `(filter (complement pred) list)`).

* The debugging situation is fantastic. Both because CL deals with errors a lot more gracefully than Clojure (are we allowed to blame the JVM?) and because SLIME >>> CIDER, at least for now.

~~~
gyim
I haven't used the LOOP macro in CL, but the for macro in Clojure is also very
powerful and (imho) very readable.

My take on the 'versify' example in 4 lines:

    
    
        (for [book book-order
              chapter (range 1 1000) :when (get-in bible [book (str chapter)])
              verse (range 1 1000) :when (get-in bible [book (str chapter) (str verse)])]
          [book chapter verse (get-in bible [book (str chapter) (str verse)])])
    

Notice that a single "for" can iterate in a multi-level structure. You can
also use :let if you don't want to call (get-in) multiple times (which makes
the code shorter, but more redundant and less efficient)

~~~
shiro
I use CL/Scheme/Clojure at work. I generally prefer comprehension style ('for'
in Clojure, srfi-42 in Scheme) but sometimes CL's loop let me save a few
nestings.

One of such patterns is when I have to accumulate multiple kind of things
while I zip through the input. Somewhat contrived example: You have a
hashtable that maps integer key to a list of strings. You want to scan it just
once and build two lists, strings associated with odd keys and strings
associated with even keys.

    
    
        ;; populate input 
        (defvar input (make-hash-table :test 'eql))
        (setf (gethash 1 input) '("ichi" "hi"))
        (setf (gethash 2 input) '("ni" "fu"))
        (setf (gethash 3 input) '("san" "mi"))
        (setf (gethash 4 input) '("yon" "shi" "yo"))
        (setf (gethash 5 input) '("go" "itsu"))
    
        ;; loop
        (loop for k being each hash-key in input
           when (oddp k) append (gethash k input) into odds 
           when (evenp k) append (gethash k input) into evens 
           finally (return (values odds evens)))
        ; => ("go" "itsu" "san" "mi" "ichi" "hi") and ("yon" "shi" "yo" "ni" "fu")

~~~
junke
You surely know this but for other people interested:

1\. You could add "using (hash-value v)" in the iteration clause to directly
have the value (no gethash).

2\. There is maphash too.

~~~
shiro
Right! I tried to construct a terse example in hurry but apparently missed the
mark. Usually loop comes handy when conditions and the way to extract values
gets more complicated.

------
javajosh
Well, here's a partial JavaScript solution, which doesn't use new ES2015
features and doesn't actually implement the sort. Feel free to riff:

    
    
       http://jsbin.com/vikesu/edit?html,js,console

------
dmead
one time i made a haskell to clojure translator

[https://github.com/dmead/Clojure-translate](https://github.com/dmead/Clojure-
translate)

------
asciihacker
Mirror of the github link please? I'm FW'ed...

------
paulddraper
Obviously, Scala is better than either of those languages.

    
    
        #!/usr/bin/env amm
        import $ivy.`com.typesafe.play::play-json:2.5.8`
        import java.nio.file._, play.api.libs.json._
    
        val bookOrder = Seq(
            "Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy",
            "Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings",
            "1 Chronicles", "2 Chronicles", "Ezra", "Nehemiah", "Esther", "Job",
            "Psalms", "Proverbs", "Ecclesiastes", "Song of Solomon", "Isaiah",
            "Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos",
            "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai",
            "Zechariah", "Malachi", "Matthew", "Mark", "Luke", "John", "Acts",
            "Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians",
            "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians",
            "1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James",
            "1 Peter", "2 Peter", "1 John", "2 John", "3 John", "Jude", "Revelation"
          ).zipWithIndex.toMap
    
        val bible = Json.parse(Files.readAllBytes(Paths.get("ESV.json")))
          .as[Map[String,Map[String,Map[String,String]]]]
    
        val flattened = for((book, x) <- bible; (chapter, x) <- x; (verse, x) <- x)
          yield (book, chapter.toInt, verse.toInt, x)
        val result = flattened.toSeq.sortBy { case (b, c, v, _) => (bookOrder(b), c, v) }
        println(result)
    

\---

Also, Python

    
    
        #!/usr/bin/env python3
        import json
    
        books = [
            "Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy",
            "Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings",
            "1 Chronicles", "2 Chronicles", "Ezra", "Nehemiah", "Esther", "Job",
            "Psalms", "Proverbs", "Ecclesiastes", "Song of Solomon", "Isaiah",
            "Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos",
            "Obadiah", "Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai",
            "Zechariah", "Malachi", "Matthew", "Mark", "Luke", "John", "Acts",
            "Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians",
            "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians",
            "1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James",
            "1 Peter", "2 Peter", "1 John", "2 John", "3 John", "Jude", "Revelation"
        ]
        books = {book:i for i, book in enumerate(books)}
    
        with open('ESV.json') as f:
            result = sorted(
                ((b, int(c), int(v), x) for b, x in json.load(f).items() for c, x in x.items() for v, x in x.items()),
                key=lambda part: (books[part[0]], part[1:])
            )
        print(result)
    

\---

Glad we've put that to rest.

~~~
piaste
Code golf? Code golf!

    
    
        let o = [|"Genesis"; "Exodus"; "Leviticus"; "Numbers"; "Deuteronomy";
                  "Joshua"; "Judges"; "Ruth"; "1 Samuel"; "2 Samuel"; "1 Kings"; "2 Kings";
                  "1 Chronicles"; "2 Chronicles"; "Ezra"; "Nehemiah"; "Esther"; "Job";
                  "Psalms"; "Proverbs"; "Ecclesiastes"; "Song of Solomon"; "Isaiah";
                  "Jeremiah"; "Lamentations"; "Ezekiel"; "Daniel"; "Hosea"; "Joel"; "Amos";
                  "Obadiah"; "Jonah"; "Micah"; "Nahum"; "Habakkuk"; "Zephaniah"; "Haggai";
                  "Zechariah"; "Malachi"; "Matthew"; "Mark"; "Luke"; "John"; "Acts";
                  "Romans"; "1 Corinthians"; "2 Corinthians"; "Galatians"; "Ephesians";
                  "Philippians"; "Colossians"; "1 Thessalonians"; "2 Thessalonians";
                  "1 Timothy"; "2 Timothy"; "Titus"; "Philemon"; "Hebrews"; "James";
                  "1 Peter"; "2 Peter"; "1 John"; "2 John"; "3 John"; "Jude"; "Revelation"|]
    
        let a s = Map.toSeq >> Seq.sortBy s >> Seq.toArray
    
        JsonConvert.DeserializeObject<_> BIBLE_TEXT    
        |> Map.map (fun _ -> Map.map (fun _ -> a fst) >> a fst)
        |> a (fun (n, _) -> Array.findIndex ((=) n) o)
        |> printfn "%A"

~~~
paulddraper
F#, right?

~~~
piaste
Yup.

'Minified', for fun:

    
    
        let a s=Map.toSeq>>Seq.sortBy s>>Seq.toArray
        let m f=Map.map(fun _->f)  
        printfn"%A"(a(fun(n, _)->Array.findIndex((=)n)o)(m(m(a fst)>>a fst)(JsonConvert.DeserializeObject<_>BIBLE_TEXT)))

