Haskell vs. Clojure (2014)
gist.github.com
gist.github.com
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.An example of "Lets take 6 devs and have them build something" would probably create a more obvious use for Haskell over Clojure, as the biggest problem (to me) with Dynamic Typing is its scalability under development. As in, alone I (and most devs) could write dynamically typed code with little issue, as all decisions are made internally and guard cases can be written correctly based on those internal decisions. But throwing a second dev (or n devs) into the mix makes those internal decisions much more difficult, making the guard clauses much more difficult to write correctly. Static typing takes a lot of the guard-clause-writing out of the picture, as the compiler creates them for you.
In the untyped code you must validate everywhere you access the data or trust that someone above you already validated it (which can break silently with any future modification).
#!/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))Clojure is one of my favorite languages. If they get the stack traces/better errors figured out, it will be damn near perfect IMO.
Highly recommended.
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.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.
Care to elaborate why you think any other Lisp is better than Clojure? I would like to hear your arguments.
* 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.
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)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")1. You could add "using (hash-value v)" in the iteration clause to directly have the value (no gethash).
2. There is maphash too.
As far as collections go, there's filter, remove, reduce, and various take functions. I don't see the deficiency.
Debugging is still an issue, but I personally don't find it too difficult... probably because I'm used to it.
The whole point of benefiting from immutable data structures is their ubiquity. If they're something you reach for only when you personally decide you need them, they're unlikely to work well with 3rd-party code; you will have to do a lot more digging to understand how the code you're writing will behave in a concurrent context vs having it be obvious. Making it opt-in is almost making it pointless, unless you never use 3rd-party code.
(0) The compiler can automatically perform optimizations like fusing small nodes and hash consing.
(1) The garbage collector can take advantage of the fact that traversing (the immutable portion of) the heap sequentially produces a topological sorting of the object graph.
I actually really like recur because it signifies intent. It tells the future-me that I intended for this function to be tail recursive (ie, it not being tail-recursive should be considered an error).
Dont Clojure's immutable types handle the side-effect issues well? Without the contortions that Haskell puts you through.
But there are many more side effects than changing variables. Clojure makes no guarantees that a given function is referentially transparent, nor does it promise that a given function doesn't write to stdout/stderr, read from stdin, change the file system, make a network connection, send missiles to foreign countries, etcetera.
https://clojuredocs.org/clojure.core/io℅21
The 'io!' macro marks a code block to have side effects which prevents using it in a STM transaction as 'dosync' might need to execute the functions multiple times in case of conflicts during the optimistic locking.
In the sense C programmers use the term? Why should humans be trusted with identifying all operations that can't be safely performed within a transaction?
http://jsbin.com/vikesu/edit?html,js,console #!/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.
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"'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)))