Back in my school days, when my friend and I were first learning C++,
we were voracious readers and exhausted our school’s programming
curriculum very quickly. Our teacher challenged us to make something
larger so we’d stop playing Tribes all
the time. It worked: I spent the rest of the term building a text-mode
platformer using <conio.h>
, and he spent the rest of
the term building and tweaking a small text-mode dungeon crawler.
Many new Haskellers make it through initial material (everything up
to and including the Monad
typeclass, let’s say), write a
couple of “Hello, world!”-tier projects that use the IO
type, but struggle to make the jump to industrial libraries and/or find
projects that excite them. I think text-mode games can grow very
smoothly alongside a programmer learning a new language, so here’s some
thoughts on how to get started, how you might extend a game, and some
advice for Haskell specifically.
I find Haskell a fantastic language for almost all of the programming I want to do: a (reasonably) expressive type system, a (reasonably) good library ecosystem, and (reasonably) good tooling together give me a very satisfying local maximum for getting stuff done. I can’t see myself giving up libraries for a more powerful type system, nor giving up Haskell’s guarantees for a larger library ecosystem.
Scripting a larger program is one of the few areas where Haskell
struggles. Despite some very impressive efforts like dyre
, I
think it’s a bit much to require a working Haskell toolchain and a “dump
state, exec, load state” cycle just to make a program scriptable. This
post discusses why Lua is a great
scripting runtime for compiled programs, its shortcomings as a
scripting language, how Fennel addresses many of these
shortcomings, and demonstrates a Haskell program calling Fennel code
which calls back into Haskell functions.
Quite a few of my favourite Haskell idioms involve the
Foldable
instance for Maybe
:
-- This is reimplemented all over the place as `whenJust`.
-- Pass our `a` to the function, if we have one,
-- and ignore its result; return `pure ()` otherwise.
for_ :: (Foldable t, Applicative f) => t a -> (a -> f b) -> f ()
@Maybe :: Applicative f => Maybe a -> (a -> f b) -> f ()
for_
-- Equivalent to `Data.Maybe.fromMaybe mempty`:
-- Return the `m` if we have one; otherwise, return `mempty`.
fold :: (Foldable t, Monoid m) => t m -> m
@Maybe :: Monoid m => Maybe m -> m
fold
-- Equivalent to `maybe mempty`:
-- Pass our `a` to our function, if we have one;
-- otherwise, return `mempty`.
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
foldMap @Maybe :: Monoid m => (a -> m) -> Maybe a -> m
Some of these confuse people more than I think they should, so this
post aims to help with that. Instead of looking at Maybe a
as “just-a
-or-nothing”, the key is to become comfortable
with Maybe
as “list of zero or one elements”. We’ll also go
looking for other types which can be seen as “lists of some number of
elements”.
I’ve run across a lot of extremely good Haskell resources over the years, and there are a few that I refer to over and over again when teaching. Instead of relying on my memory and hoping to recall the right talks in response to the right questions, I’ve collected the best of them on a wiki page. Hopefully it’s useful.
I added a wiki to the site, so that I can store important public notes and other stuff that’s not just point-in-time blog posts. This has allowed me to publish a tier list of Space Jam mashups.