Why Python Is Better Than Haskell



Also read Hillel Wayne: Why Python Is My Favorite Language.

So: I like python a lot. On the other hand, I also really like haskell. I like functional programming. I like putting information into types, and having the types checked at compile-time. I like higher-order function like map. By god, I even like monads. I think monadic parser-combinators are just about the coolest thing in the world - especially the fact that you can implement them as a library!

Python is very antithetical to that. It obviously doesn’t have static types. It has higher-order functions, but their use isn’t super idiomatic. It’s not pure. But… I still really like coding in it. In a lot of cases I strongly prefer writing Python to Haskell. Probably the strongest reason for that is the build situation in Haskell, which is extremely stupid. But actually even allowing that, python seems to have a certain ease of use that Haskell doesn’t. Why is that? I think the key thing is that Haskell makes it easy to create new abstractions and regards it as a normal part of coding.

Wait, what? Weren’t we talking about things that make python easier to use? Yes. That’s what I mean. The fact that Haskell makes it easy and expected to introduce new abstractions means that, to pick up a new library, I need to understand its abstractions, and probably massage an interface between them and my program. That’s super annoying.

When writing in python, there are essentially no abstractions, beyond the bare minimum of procedural “each command modifies variables and does IO and returns a value”, along with a very small set of types of data: numbers, strings, maps, lists. It’s not that this makes libraries easy to integrate, it’s that it makes them easy to understand. To use requests to talk to mailgun, here’s what I did:

# the variables are populated further up...
r = requests.post(
    mgurl,
    auth=("api",MAILGUN_APIKEY),
        data={"from": sender,
              "to": [TARGET_MAIL],
              "subject": subject,
              "text": text})
return(r.text)

I want to make a post request with some arguments, and get a response back (actually the response doesn’t matter that much in this application, but whatever). I build the arguments (strings) and the url (a string), then shove all this into post function.

Here’s the top google result for “haskell make post request”, which I just googled now while writing this post. Okay.. so I need to find a RequestBody (what’s that?) and use parseRequest to turn my url into a request, where I can then insert my body..?

I go to the http-conduit docs, where I see this:

Okay, so after staring at this for a bit, it seems like you just put in a bytestring? That’s slightly more low-level than I was hoping - to be honest, I don’t even know how to format a list of arguments like the above into a http request!

After digging around the docs page for a little while, I find this: Okay.. that doesn’t look too bad. Now we can implement the fragment above in Haskell like this:

do
  manager <- newManager defaultManagerSettings
  request <- urlEncodeBody [("from", sender),("to",targetMail),("subject",subject),("text",text)] <$> parseRequest mgurl
  response <- httpLbs request manager
  return $ body response

(I actually haven’t checked whether this works like I expect it to, so don’t be shocked if this code doesn’t work as written).

Okay, so in this very simple case, we needed to understand two new types: Manager and Request. RequestBody turned out to be a red herring, although I also looked that one up. I’m going to allow ByteString as part of the “standard library”, but if I was really writing this code I probably would’ve had to look up/remember how to convert between string types.

The Manager object keeps track of open connections, which is probably not needed for my application. Do I need to close my connections after I’m done or something? I actually make some other http requests earlier in this application - does it matter if I use the same manager?

Why does parseRequest use the IO monad? (Looks at the docs) okay, it actually uses MonadThrow, so I guess I should make a decision about how to handle a parse error - should I just crash with an exception or handle the error more gracefully (the Python version of this script has logging - that’s probably what “should” be done).

Is this all very complicated? No. Is it bad design? Also no. http-conduit provides stuff like simpleHttp :: MonadIO m => String -> m ByteString which lets you do simple requests. I’m gonna assume giving people the option of passing a manager object around is a good idea. Making people be explicit about what request they want instead of just making a best guess based on the arguments passed to requests.post.

Once your application gets more complicated, the extra structure imposed by Haskell starts paying dividends - the abstractions begin to simplify things, justifying the upfront cost of understanding them. And the abstractions built by the user - not imported from libraries but custom-built for this application - start becoming worthwhile.

But while you’re still at the “fucking around to do something simple” stage, it’s all very overkill.