Rage-learning Haskell

The hardest language I've ever tried to learn. A rant being written as I try to understand it.

by Robert May on Afternoon Robot

I like learning programming languages. Ruby is my day-job, but I've also learnt and used a whole bunch of other languages. Although I've been programming most of my life, my experience is not in computer science; I just like building things. Off the top of my head I can say with reasonable confidence that I could build something in Ruby, Elixir, Rust, Go, Dart, Elm, JavaScript, or PHP right now and not find it particularly difficult. Out of all of those, Elm is the most similar to Haskell. It helped me massively with getting comfortable with reading Haskell (the first, and biggest barrier for me). I love Elm, and though it has some issues with documentation missing certain steps etc, it is a fucking breeze compared to learning Haskell.

The rant will now commence.

What's a Monad?

Don't know. I also don't really care, and it probably doesn't matter that much. If you search for explanations you will turn up a ton of highly-upvoted (on StackOverflow) answers that all comprehensively fail to explain why you should care what a monad is. I've gone my whole life without caring or even knowing such a thing existed, and I've built a lot of things without knowing what a monad is. This should, in theory, be a simple problem to solve for experienced Haskellers if they really want people to understand: write out an example showing how a monad is used, and why it's better than not using it, maybe even in a couple of different languages (to alleviate the Haskell syntax barrier), and compare it to what the code would look like without it. Do this using a real-world example of something that people care about, say, in the context of a web request. Don't use maths as an example.

Stack/Cabal is a bit pants

I don't even really understand why, and it's hard to give examples. It will break illogically or throw oddly useless errors. I can run my app with stack runhaskell ./src/Main.hs but if you try to load a console with stack ghci it will throw a dependency error like:

    Could not load module ‘Network.Wai’
    It is a member of the hidden package ‘wai-3.2.2.1’.
    You can run ‘:set -package wai’ to expose it.
    (Note: this unloads all the modules in the current scope.)
    Use -v to see a list of the files searched for.
   |
21 | import qualified Network.Wai                          as Wai
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.

What does this mean? Why does this not happen when you run the actual web app? How do I now make it reload the modules?
Moreover, how the fuck do I get out of GHCI? (it's :quit if you're also stuck. This is never mentioned when starting it up, and it ignores Ctrl+C)

Most people write Haskell in a confusing way

A lot of examples import tons of modules without namespacing them. This makes for an extremely perplexing time reading code and trying to figure out where the hell a function has come from. To import in a clearer way you import modules as qualified, e.g.

import qualified Data.Text as Text

-- Then use functions like
Text.pack

-- Rather than just
pack

Do this for basically everything and your code will be far easier to understand for other people. I feel like the one exception to this would be for the "most important" import, e.g. a web framework, which introduces a lot of functions that build up the structure of your module.

There's also a lot of confusion caused by Haskell's weird syntax (if you're coming from e.g. C, Ruby etc). This could nearly universally be resolved by people using more brackets in examples. I also found Elm extremely useful in learning how the syntax works, as it has far fewer oddities than Haskell.

Finding examples of what you want to do is a challenge

Apparently lots of people use Haskell. For what, exactly, I struggle to say. If you search for a subject you will find very few blog articles, and just as few StackOverflow questions/answers, which is quite frustrating because...

The documentation is absolutely shockingly bad, for nearly all libraries

You can just straight-up exclude haskell.org from your Google search results. You will find pretty much nothing of use in any of the package documentation. Most examples will use placeholder values like x or y. What are those values likely to be in real usage? Dunno. Very few provide a good end-to-end example using a case that most people would find relevant. The biggest issue I've faced so far is figuring out how to write a multi-line text parser, which going by the search results and documentation, literally nobody has ever built in Haskell.

An example of this is that I have been writing a parser that can handle the Heroku logplex format, which looks like this:

83 <40>1 2012-11-30T06:45:29+00:00 host app web.3 - State changed from starting to up
119 <40>1 2012-11-30T06:45:26+00:00 host app web.3 - Starting process with command `bundle exec rackup config.ru -p 24405`

This is pretty trivial in say, Elixir, and a primitive bit of regex will get us there:

defmodule Parsers.Logplex do
  # Accepts multiple lines of strings, returns parsed lines
  def parse(string) when is_binary(string) do
    # The `m` here turns it into a multi-line regexp
    lines = Regex.scan(~r/^\d+ <\d+>\d+ (.{25}) (.*)$/im, string)

    # Convert the lists into maps
    Enum.map(lines, fn line ->
      [_original, datetime_str, msg_str] = line
      {:ok, datetime, _} = DateTime.from_iso8601(datetime_str)
      %{time: datetime, message: msg_str}
    end)
  end
end

I think that took me less than an hour to figure out. I realise now how spoilt I have been by Elixir's excellent examples and fantastic inline documentation (in which the examples can be run in your test suite, ensuring they work).

In Haskell you don't really use regex for this, you build a parser. Parsers are quite cool, quite verbose, and there's like 4 very similar competing parser libraries you can try. I settled on megaparsec because it's the only one that had a vaguely-useful tutorial around. Doesn't seem to mention multi-line parsers at all, however. Megaparsec supposedly has useful errors, although I think this may be subjective:

Left (ParseErrorBundle {bundleErrors = TrivialError 13 (Just (Tokens ('-' :| ""))) (fromList [Tokens (' ' :| ""),Label ('a' :| "lphanumeric character")]) :| [], bundlePosState = PosState {pstateInput = "83 <40>1 2012-11-30T06:45:29+00:00 host app web.3 - State changed from starting to up\n119 <40>1 2012-11-30T06:45:26+00:00 host app web.3 - Starting process with command `bundle exec rackup config.ru -p 24405`", pstateOffset = 0, pstateSourcePos = SourcePos {sourceName = "logs", sourceLine = Pos 1, sourceColumn = Pos 1}, pstateTabWidth = Pos 8, pstateLinePrefix = ""}})

This error basically means "unexpected '-' character". It is not a useful error. Haskell developers: go play with Elm and then rethink how you write errors. I'm not picking on Megaparsec in particular here, as a note; this is pretty much the state throughout everything I've tried in Haskell.

Currently I am not sold on the concept of a parser being better than regular expressions for this task. One is a (vaguely) standardised, concise way of parsing strings with a myriad of tools, and the other is library-specific and requires significantly more effort and code, as well as having worse documentation.

Summary, so far

Haskell looks really cool. I think I could really enjoy it, but it is genuinely hard to learn. I'll keep persevering.