Static typing all day every day!
I'm working on the server for the game.Previously I have mentioned Google App Engine, and the fact I'm using Python. Now that has changed. I am not using Google App Engine for now. (I'm rather thinking about getting a free plan on OpenShift.)
I could use Frege (Haskell-like language on the JVM) on the JVM with GAE, but why limit myself to GAE's restrictions, when I can have a free OpenShift account that doesn't have them? (I haven't researched the topic extensively, so I might stumble upon some factor that makes me rethink this, but I will eventually need an offline, local server, when I want to support LAN, so either way, I have to write it.)
What was my problem with Python?
When I wrote a few lines of code, then I fired up the client to test it, it caught some error - maybe I wanted to call a method on the None object, because I didn't check for the object being None (this was the most common type of error). I fixed it, ran the client again, and oops, another error. This time I tried to concatenate a string and a number together. Fixed that. Next run, I forgot to write the "else" part of an "if" statement - since that I always write out the "else" part explicitly, and then have a "pass" in it, if nothing else. I have avoided a few problems thanks to this habit. Ran the client, and it went fine.
Three runs for a few lines of code, half an hour wasted, and have no guarantee whatsoever about my code correctness. That would require unit tests, static code-analysis tools, etc.
What about Haskell?
After I switched back to Haskell (which I've been coding in for a few years now), I started by laying out the types of the functions I knew I wanted to have. Most functions' body was empty. It gave me a structure that could be filled with code - most times the solution was straightforward, thanks to the types, I hardly had to do any thinking. Of course, the initial, upfront design had to be thought out carefully, but after the types were in place, it was just compiling for typechecking, and fixing the type errors.
I coded for 4 days without ever running the code - mostly because there were missing body functions that I haven't written yet. But I had confidence that once I'll run the code, it will Just Work(TM). After I wrote every necessary functions, I have started the code, and it ran...
...with hitches. It turns out, that once the main thread finishes execution, all the other threads die instantly. So I just inserted a placeholder thread that just runs without doing anything, and the server is now working correctly (at least the connection handling, but I don't expect any big problems with the other parts). I could quickly add a logging capability as well.
What causes this difference?
Types. Let me explain.
What kind of troubles I ran into with Python?
- - Null types - with Haskell, you can make any type into a nullable type by using Maybe. (Or you can just roll up your own Nullable datatype.) You want a nullable Int? You can have Maybe Int. You want a nullable EmployerStruct? Maybe EmployerStruct. You want a nullable function with side effect giving you back some value? Maybe (IO a). If you are a masochistic mad scientist, you can make nulable types nullable: Maybe (Maybe Int) (though this usually has no usability, so don't bother). The catch is: it is stated explicitly. If you are eypecting a nullable type, the type checker makes sure you are aware of it. If you are not expecting nullable type, the type checker makes sure you can't give that function a nullable type.
- - Missing "else" caluses - in Haskell, an "if" MUST have an "else" clause. You have to return something. If your function returns a number, you have to return a number. (And "undefined" is a number. In fact, "undefined" is kind of a Null pointer. Although you are better off with some other error-handling, if you really need to halt and catch fire. If you just don't always want to return something, make the return type a Maybe, or some other kind of multi-value type.) Most of the time, however, this is not even uncomfortable. Sometimes it is, but you should know that it is for your own good.
- - Concatenate a string with a number - if Python would have a compiler with types, that would've caught this. Without type-checking, that particular code branch has to run to catch this error. There are no tests in the world that can guarantee 100% coverage. (Of course, static typing could not get 100% coverage, either. But silly things like this? It does catch 'em quite well.)
As was written in Why haskell just works, there are three types of mistakes, and a static type system can catch two of them. Dynamic type systems are not so good at it. All in all, types are there to protect the programmer from shooting itself in the foot.
And, as Carmack said: "Anything that is semantically legal will end up in your codebase." - static types will make that "semantically legal" space smaller - which filters out a huge number of bugs.
Of course, the drawback is that it is less comfortable to adhere to the type system. But in the long run, the benefits are so much bigger than this discomfort. Think about it.
No comments:
Post a Comment