I have a strange crush on Haskell. I first dabbled with the language back in, oh, 2007 or something, and it was enlightening. I thought I was the sort of person who obeyed the Don't-Repeat-Yourself principle, who didn't duplicate data, who didn't use global variables, who in general preferred "pull" techniques to "push" techniques because I don't prematurely optimize, god damn it, but just playing with Haskell and Scheme for a few days made me realize that I was one stateful MF. This eye-opening experience turned me into a new coder. Between that and automated testing the way I made programs changed, and that strongly influenced the game engines in Schizoid, Sixty Second Shooter, and other engines and games. I would much more often catch myself about to manipulate a state variable and stop and ask - "hey, can I do this another way, one that's less likely to break months down the road?"
For various reasons, these past few weeks I've decided to take another look at Haskell. For one thing, I'm finding I'm having trouble sharing these insights with co-workers. Their eyes glaze over when I preach the zen of not-mutating-in-place. As this is a team full of mostly imperative C++ coders, I suppose this is not a surprise. I probably would have been the same way back when I was a technical director at Treyarch, with no FP experience, just bent on getting the next feature done and not wanting to hear about some better way that would require a do-over.
But another thing was running into this old John Carmack video clip where he talked about the Quake engine and how maybe Haskell could solve their problems and it was like he was reading my mind. That sounded just like how I felt about the Treyarch engine circa Spider-Man 2: http://functionaltalks.org/2013/08/26/john-carmack-thoughts-on-haskell/
So I start looking back into it again. Turns out nowadays people have been making actual games in Haskell:
https://github.com/jxv/dino-rush
https://github.com/mewhhaha/apecs-unity-tutorial-haskell
https://lambdahack.github.io/ - this one is particularly interesting because there is a lot of game state that has to be managed.
So, how do these things even work? You can't change state in Haskell, I knew that, though they have these inscrutable "monad" things that somehow supposedly make that sort of possible.
So I bear down and try to get these games to compile and ask their creators questions about them on a games-in-Haskell discord server. I think I've come to understand what a state monad is. It is hard to explain; my favorite article on the topic says that you have to experience them to really understand them.
But the upshot is these monad things are a pattern that make it so you can make your functional, declarative program act or seem like an imperative program. You have a state structure that you can pass to a function invisibly that will return the modified state which you can then pass to the next function. It doesn't modify the state in-place, but at some level it may as well have, because the modified state is what invisibly gets passed to the next function using this special "do notation" that hides the functional stuff behind the scenes and looks a whole lot like imperative programming. It seems almost as if Haskell was insecure about being functional and so it provided this illusory mechanism to make imperative coders feel at home. If that was the case, it didn't work, because the imperative coders tend to run screaming when the Haskellers are like, "Let me tell you about monads. You need to know this." This monad stuff becomes a rabbit hole and it seems like the bulk of Haskell literature is about working with them, making stacks of them, improving on them, using different kinds of them, using other patterns and APIs like "lenses" and "transformers" with them and this increasingly convoluted and self-referential web overloaded my poor brain and fried it.
But I digress. The state that's being passed around in the monad is probably not being modified in place. So that sounds great for concurrency, at least: lockless reads are still in-bounds.
But concurrency, while important, isn't why I came to Haskell's door. I was looking for an end to spending so much time in the debugger, trying to figure out why a variable got bashed, setting hardware and conditional breakpoints, only to find out that another bashed variable was the culprit, and that it was bashed variables all the way down.
And these state transformers that Haskell gives us do not eliminate that. They reduce it some, sure: they strictly control the order in which state changes happen, and the state changes are deterministic--there are no global variables causing a different result to spit out.
(Not even the global state variable that lurks in the heart of rand -- it's obvious now that I've read Learn You A Haskell but I just somehow hadn't really had conscious awareness that rand uses a stateful global. You just make sure to consistently set the seed when you're testing functions that use it, yeah?)
And that's pretty much the fundamental disappointment for me at this point. Haskell is still safer... and I don't know what I was looking for, exactly ... somehow imagining I'd never have a hard-to-track-down game logic bug again? Because Haskell would provide infinite magic resources and never bash the original sources of truth, game state 0 and a chain of player input?
I had visions - maybe I'd participate in this LambdaHack project, or maybe I'd try to port Sixty Second Shooter to Haskell - and now I don't know if I want to do that. I may, at this point, have gotten most of the learning/training I'm going to get from Haskell. Though I'm still not quite ready to put it down just yet.
All that said I'd definitely still recommend it to other coders for the learning / mind-opening / mind-bending experience. Read Learn You A Haskell, do some of the exercises, make some kind of simple game without monads. And when you get to monads, stop. It's a lot of work to understand something that, with C++, you'd probably just do by taking away the const keyword.
I hope you give it a try and that tiny taste of Haskell levels you up as much as it leveled me up.
Because I am still certain my life as an indie coder with engines and games of my own construction has been less bug-prone than the ones I made back in the Spider-Man days. Apples and oranges of course: these games are smaller, less complicated projects with only one or two or three coders, usually written in high level languages, whereas Spider-Man 2 was a complicated native code mess with a bunch of people mucking with it. Still, I think approaching these indie engines & games with a mindset of reducing state changes is a significant part of why I'd been able to get by with almost no QA to speak of. My next goal - convince more of my co-workers it's a good mindset to have.
Comments