Then you’ve never seen tricky/advanced Haskell code that takes hours to grasp.
It’s not a language issue. In most sane languages you can write readable code. And most languages also give you enough freedom to write unreadable code.
Here are some reasons why advanced Haskell code might not be readable if you aren’t already experienced with this style of coding:
- uses the C preprocessor extensively (usually to workaround API changes in upstream libraries, but sometimes used for actual code generation, like in a particular streaming library)
- uses long TH declarations that give no insight into what they do (you only know they have to return a `Q [Dec]` but it’s not obvious what, and dumping splices only goes so far; it can even download things from the Internet to generate code!)
- uses advanced concepts like Church/Scott encodings of data types (though with some comments this really shouldn’t be hard)
- excessive type level meta programming
- uses concepts like Bazaar, Bizarre, Conjoined, Strong, Yoneda, etc
By the way for some laughs you can read about how to write to write a factorial program in Haskell from simple straightforward styles to convoluted ones that use many advanced techniques: https://www.cs.utexas.edu/~cannata/cs345/Class%20Notes/10%20... I hope no one actually writes production code like this but it does show you how much of a steep learning curve could be needed before you understand certain kinds of Haskell code.
True but Haskell's type declarations let you skip reading the actual implementations. You can skim through the type declarations only and know what it does, and that it won't compile if the implementations don't do exactly what the type declarations do. Saves a few brain cycles.
This is turning a bunch of buts, but...that statement is true for any statically typed language.
Fluency is underrated in general and is a big part of the disagreements over languages. If you're really fluent in something, it's amazing how even languages with flaws are less problematic than people think because once you get used to idioms you learn to avoid those flaws and work in the context of the language. I can skim over a JS file and have a similar experience of understanding as the OP describes, given that JS is reasonable.
> that statement is true for any statically typed language.
It's a spectrum!
The awesome part about Haskell is that you can assume functions are deterministic and don't do IO unless they declare otherwise in the type signature. This is really a huge deal. I like statically typed imperative languages, but there's a big difference in expressiveness between a crude type signature like `String -> String` and something more fine grained like `URL -> IO ResponseBody`.
That said, Haskell has lots of downsides (many of which have been addressed in the last few years, some of which have not) so it's not a perfect language by any means. But it is very readable and refactorable.
Glad you liked it! I'm using this library to do my own analysis and visualization of the value of different coins to inform my own investing. My goal there is that some more tools will fall out of that endeavor, which I would also release publicly. We'll see how that goes!
Submit it to https://www.reddit.com/r/haskell/ if you want feedback to your code. I doubt most people on HN are fluent enough in Haskell to give you meaningful feedback.
* Consider using `Text` instead of `String`, as `String` is a linked-list of `Char` and quite inefficient
* Consider refining some of your fields into more constrained types. Sort orders probably only have a couple of valid values, and might reduce to an enumeration instead of `String`.
* In `instance ToQueryString PriceRequest`, I'd be surprised if those type annotations are necessary. The inferencer should be able to work out that `req` is a `PriceRequest`.
* In `instance FromJSON AggregatedSnapshot`, I think the `read`s should go. I think you want to use the `Float` instance of `FromJSON` instead. (What the `read <$> ...` is doing is parsing to a string, and then using the partial (!) `read` function on the result. JSON responses that have a value that fails to read for that key will crash the program.)
* In `instance FromJSON AggregatedSnapshot`, consider `withObject` here too, which encapsulates the pattern you're using.
I don't have it in front of me but I would assume the FromJSON instance for Float expects a JSON number, not a string. If the remote API makes the (puzzling) decision to put numeric values in strings, then parsing those will be necessary. Although, I agree that the 'read' function is the wrong one to use here, due to it making the function partial (consider 'readMaybe' instead?)
Thank you for all of these suggestions, very appreciated! I'll work through implementing these. Also, please see my explanation of the `read` calls below, but I plan to change those to `readMaybe`s, since there is a need to parse numbers from strings ( for whatever reason :/ )
What a bizarre API. You might get some mileage out of writing a helper function like `readParser :: Read a => String -> Parser a` which can call `readMaybe` and inspect the result.
Bunch of datatypes, some instances, some helper code, some top-level IO code tying everything upstream together. Done.