A decent amount of the classic How to write unmaintainable code is biased towards imperative programming. This is an attempt at reaching equality.
-
Use rewrite rule pragmas to alter functions. The compiler doesn't check whether the rewrite rule makes sense at all, so feel free to sprinkle your code with interesting rewrites.
{-# RULES "reverse map/append" forall f xs ys. map f (xs ++ ys) = map f ys ++ map f xs #-}
-
IO
is your friend! Take the restrictivity of Haskell away, when you're done use QuickCheck to show it's referentially transparent and wrap it inunsafePerformIO
.len xs = unsafePerformIO $ do count <- newIORef 0 let getLength ys = case ys of [] -> return () (y:ys') -> modifyIORef' count (+1) >> getLength ys' getLength xs readIORef count
(Make sure to use
modifyIORef'
or your code may be unsafe.) -
Writer
<State
-
State
<IORef
-
IORef
<RWST IO
-
A suitable type to write something is
Writer
. A suitable type to modify state isState
.IORefs
are good in general. I'm sure you already guessed how to combine this with the previous points. -
QuickCheck is a statistical test that generates false sense of security, and should be avoided for that reason.
-
All laws in Haskell are meant to be broken. Got a monad, but
m >>= return
just won't bem
again? A-- hack, careful
should be sufficient documentation. -
Haskell's expressiveness allows coding in a very concise style. For example it renders comments redundant and makes you able to call all your types
T
and typeclassesC
and them import them qualified. -
Never import a module qualified under the same name twice. Combines well with the previous point.
-
Import the same module multiple times, qualified under different (suggestive) names.
-
Boilerplate code should be avoided; GHC will complain when it requires explicit type annotations.
-
If you want your program to be run just as you've written it, disable compiler optimization rewrites. The nicer way of doing this is by using the
-fno-enable-rewrite-rules
flag when compiling, however it is more effective to define a rule that makes GHC go into an infinite loop when compiling, forcing the compilation to be done like this.loop a b = () {-# RULES "loop" forall x y. loop x y = loop y x #-}
Note that you have to use loop somewhere so it's not optimized away. A good way is having
return (loop 1 2)
as the last function in main. -
Naming conventions can help make code more readable. For example in
(xs:x)
,xs
stands for "x singular", andx
contains the rest. -
Use built-in functions as identifiers. Make sure to mention the name in the docs multiple times. Then create a base case that doesn't work for that operator.
times 0 _ _ = 1 times n (+) x = x + times (n-1) (+) x -- 2*3 = ?
-
Redefine Prelude elements. The following one will teach proper use of
succ
in the future.a + b = a +. b' where (+.) = (Prelude.+) b' | b == 1 = b +. 1 | otherwise = b
... unless ...
succ x = Prelude.succ (Prelude.succ x) -- Eta reduction needs -XMonomorphismRestriction :-(
Another thing worth noting is that
otherwise
is not a language feature, but simply defined to beTrue
inData.Bool
.otherwise = False
-
Make all functions pointfree. If you can't do it yourself, ask Lambdabot.
-
Unicode is your friend! Make mathematicians cry:
type ℕ = Integer type ℝ = Double
Prettyprint special values:
(∞) = 1/0
Use creative symbolism:
(☃) = show (☠) = undefined (≸) = (==) a ‽ b = a `seq` (a, b)
And last the really golden part about Unicode: characters that look alike, for example none of the following is in ASCII: аеорсух АВЕКМНОРСТУХ.
let map mар maр = mар : map mар maр; mаp mар = map mар mар in mаp "map"
-
LANGUAGE
pragmas alter the language, so they are not and should not be part of the source code. If you need language extensions, specify them as compiler flags in your.cabal
ormakefile
. -
Use
BangPatterns
syntax without importing the language extension.f !x = f x
-
The
RebindableSyntax
GHC extension makes certain operators not refer to their Prelude versions, but to whatever is in scope. This means you can redefine functions likeifThenElse
,(>>=)
and(>>)
. The last two are especially devious as do-blocks are still desugared using the same rules, but the meaning of operators can be completely different - even non-monadic definitions are possible. As a bonus, you can do the re-definition inside a do block, affecting only the following code. -
The presence of algebraic data types does not mean we cannot use
String
everywhere anyway. Switch toText
ly typed programs when performance complaints arise. -
Use
where
like it creates a submodule. Use submodules extensively to structure your code. -
All types up to functions can be composed out of
(,)
,Either
,()
,Void
and the primitives such asChar
andInt
. -
Before you start inventing fancy variable names, make good use of the first 26 ones.