6 ms·
A Clone of Chips Challenge in Haskell
- alexchamberlain 12y agoChips was one of the first applications I ever used as a kid. It was Windows 3.1, and I distinctly remember knowing how to start `windows` from the DOS prompt.
- misja 12y agoI'm learning Haskell and I find this project very helpful and educating. There's one thing I'm wondering about, and that's the lack of (unit) tests. I'm coming from the Java world and I'm used to write unit and integration tests for most of the code I write. But I have noticed that in Haskell projects this is not so common. Why is that? Is it a cultural thing, or is Haskell's strict type system and immutability so helpful that unit tests are not so necessary comparing to for instance Java?
- gamegoblin 12y agoYou may have just looked at some poor projects. In fact, Haskell has a two really great testing frameworks: HUnit, directly based on JUnit: http://hackage.haskell.org/package/HUnit http://hackage.haskell.org/package/HUnit Quickcheck, which uses randomized testing: http://hackage.haskell.org/package/QuickCheck http://hackage.haskell.org/package/QuickCheck HUnit will be very familiar to you, Quickcheck is definitely more interesting. You specify some invariants and the heuristic algorithm tries to come up with random inputs to break your function. It's potentially less accurate as good unit tests, but it's also a ton less work. I can just say something like "This function takes an array of integers as input, and I expect an output array of the same size, where all numbers are even, and there are no duplicates" (in math-y form, of course), and quickcheck will try however man random inputs I specify to try to find a case that breaks that invariant. Consider a well known Haskell library, attoparsec. It has extensive tests and benchmarks: https://github.com/bos/attoparsec https://github.com/bos/attoparsec That being said, there are those in the Haskell community who take Haskell's referential transparency and equational reasoning to a whole new level and don't write tests, but instead write proofs. For an example, see the popular Pipes library: https://github.com/Gabriel439/Haskell-Pipes-Library/blob/master/laws.md https://github.com/Gabriel439/Haskell-Pipes-Library/blob/mas...
- PieSquared 12y agoIn addition to the libraries mentioned above, there's also some very good testing frameworks (which can run test suites composed of HUnit and Quickcheck tests), such as HSpec and tasty (and various extensions such as tasty-golden for golden tests and tasty-quickcheck for quickcheck tests). I've used these in various projects of mine; as with other languages, sometimes TDD is the right approach, sometimes you write a few tests afterwards to make sure everything works, and sometimes you can get away with no testing at all. That said, I do feel that the type of testing I do in Haskell is often quite different from other languages. Due to the strict type system, some types of errors can be eliminated at compile time, and thus do not need to be tested for. Of course, as the parent pointed out, some people take this to a (very awesome) extreme, but even in a less rigorous form the static guarantees can eliminate many things that I would write unit tests for in other languages. Finally, you may also be interested in SmallCheck, an alternative to QuickCheck. From its documentation: > The big difference [between SmallCheck and QuickCheck] is that instead of using a sample of randomly generated values, SmallCheck tests properties for all the finitely many values up to some depth, progressively increasing the depth used. For data values, depth means depth of construction. For functional values, it is a measure combining the depth to which arguments may be evaluated and the depth of possible results. This means that SmallCheck will often find small and interesting counterexamples to the properties you wish to affirm. (As an aside, note that writing property-based tests is a skill, just like writing good unit tests.) tl;dr: Testing can sometimes be less necessary, but is certainly not unnecessary, and there are some very high quality and well-documented libraries for testing in Haskell.
- dllthomas 12y agoI think it is true that unit tests are not so necessary compared to Java, but I think many in the Haskell community (myself included) have a tendency to rely too heavily on this. Tests aren't unnecessary in Haskell. gamegoblin is absolutely correct that there is some great tooling for tests in Haskell.
- Dewie 12y agoI think unit tests are overrated. No, not because testing is overrated, but because unit tests aren't the only way you can test software. QuickCheck for Haskell is a fuzzy testing library, for example.
- egonschiele 12y agoJust poor practices on my part :) Here's another project of mine that has tests and gets run on travis-ci: https://github.com/egonSchiele/salmon https://github.com/egonSchiele/salmon
- implicit 12y agoMy experience is that you can generally skate along on Haskell's type system and get further into a project without hitting the complexity threshold where you need tests to confidently make improvements. Bigger and longer-lived projects benefit enormously from an automated test suite no matter what they're written in.
- marcosdumay 12y agoYou'll always need tests. That said, when you refactor a program in Haskell, instead of running the tests and some functionality falling, usually the compiler itself will tell you there's a problem at line X of file Y. That use case of tests is just not there - and that's the main selling point of unit tests. Thus, I'd expect Haskell developers to not write as much of this kind of tests. But again, you'll always need some kind of it.
- crimsonalucard 12y agoHonestly, is functional programming really better for this type of game? I find the structure and nature of the game more fitting to an object oriented approach. IMO Chips, blocks, player character all tend to be better described as mutable objects, because that's what they actually are from the users perspective....
- crimsonalucard 12y agoFYI I'm in no way challenging the impressive nature of making this game with Haskell. I'm just challenging the hype around the style and asking whether it really is the most efficient solution to programming a game...
- orclev 12y agoIt isn't really about efficient, it's about making it simpler to reason about. Functional style is a different way of reasoning about state than OO, but they're both fundamentally about abstracting state and making it simpler to work with and reason about. OO takes the divide and conquer approach, by dividing state into progressively smaller chunks until each piece is small enough to reason about. The flaw with that is that at some level you need to understand and reason about the entirety of the state, and if you have a bug in some lower level piece it often exhibits as bad behavior in one of the higher level pieces. Figuring out which of the lower level pieces is ultimately responsible is 90%+ of the time spent on debugging in a OO program. Another challenge is that because state is mutable, just finding the starting point to debug the problem is often a chore because you must first get the system into the state necessary to trigger the bug. Even worse is that often the state you initially detect the bug in, is actually significantly further along than where the bug originates so you end up having to attempt to progressively walk backwards through time to arrive at the originating point (which in practice means running a debug session to a point, finding the next piece of state that looks wrong, setting new breakpoints, and then repeating the whole exercise). Functional programming takes a different tack by instead strongly discouraging state altogether. A well written functional program will contain only the absolute bare minimum state necessary to accomplish the task at hand. Further state tends to be divided into two distinct camps, the global state that's referenced by name (largely non-existent barring mutable IO refs and such), and the local state that's passed explicitly as function arguments (ignoring for the moment the monad abstraction around that). Like with OO you sometimes arrive at a point where behavior isn't what you expect it to be and need to go to a debugger to figure out where the state has deviated from the expected. Unlike in OO however you have the call stack to reference and can see exactly where in the history the problem originated. Finding the source of a bug in a large functional program tends to be rather trivial compared to the similar task in a large OO program, due to the more concise and limited state that exists in functional programs.
- egonschiele 12y agoHi HN! Author here. I made this game because I wanted to see what it was like to make video games with Haskell. It turned out to be a pretty good fit. If you are interested, here's an explanation of the code: http://vimeo.com/109663514 http://vimeo.com/109663514 You might also be interested in this framework if you want to make games of your own: https://github.com/egonSchiele/actionkid https://github.com/egonSchiele/actionkid
- freyrs3 12y agoThis is extremely well done, thanks for taking the time to make a screencast.
- CoffeeDregs 12y agoI gotta say that, after not having used it in about 4 years, I love looking at Haskell again. One question: could CurrentTile.hs be DRYed up? https://github.com/egonSchiele/chips/blob/master/src/Chips/CurrentTile.hs https://github.com/egonSchiele/chips/blob/master/src/Chips/C... This repeating pattern seems ripe for DRYing: checkCurTile (Chip _) = do liftIO $ playSound (soundDir ++ "collect_chip.wav") False tileAt Current .= (Empty def) checkCurTile (Gate _) = do liftIO $ playSound (soundDir ++ "door.wav") False tileAt Current .= (Empty def) checkCurTile (KeyYellow _) = do yellowKeyCount += 1 tileAt Current .= (Empty def) checkCurTile (KeyBlue _) = do blueKeyCount += 1 tileAt Current .= (Empty def) [...]
- egonschiele 12y agoAh yeah, it could definitely be refactored. There's some repetitive code in there.
- codygman 12y agoSorry for bad formatting and not compile checking the code, but this is an improvement imo: checkCurTile b = do case b of Chip -> liftIO $ playSound (soundDir ++ "collect_chip.wav") False Gate -> liftIO $ playSound (soundDir ++ "door.wav") False (KeyYellow _) -> yellowKeyCount += 1 (KeyBlue _) -> blueKeyCount += 1 tileAt Current .= (Empty def)
- iwasanewt 12y agoAha! A bug! http://i.imgur.com/2vNiRzn.gif http://i.imgur.com/2vNiRzn.gif (you can walk on ice if you have skates, but you can't collide with walls without being "repelled") Also, I completely forgot about this wonderful game. Thanks for the trip on memory lane.
- egonschiele 12y agoOuch! I'll have to fix that. In the meantime you can skip to the next level by pressing 'n'.
- iwasanewt 12y agooh, there's no need to skip the level. you can escape the bump by pressing forward+left/right