How to write unmaintainable Haskell code
December 11, 2016 · View on GitHub
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 #-} -
IOis 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.IORefsare 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 >>= returnjust won't bemagain? A-- hack, carefulshould 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
Tand typeclassesCand 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-rulesflag 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),xsstands for "x singular", andxcontains 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
succin 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
otherwiseis not a language feature, but simply defined to beTrueinData.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 ℝ = DoublePrettyprint special values:
(∞) = 1/0Use 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" -
LANGUAGEpragmas 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.cabalormakefile. -
Use
BangPatternssyntax without importing the language extension.f !x = f x -
The
RebindableSyntaxGHC 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
Stringeverywhere anyway. Switch toTextly typed programs when performance complaints arise. -
Use
wherelike it creates a submodule. Use submodules extensively to structure your code. -
All types up to functions can be composed out of
(,),Either,(),Voidand the primitives such asCharandInt. -
Before you start inventing fancy variable names, make good use of the first 26 ones.