# Idiomatic Haskell and Common Pitfalls — Haskell

Source: https://www.geekswithgeeks.com/en/haskell/x-idioms

> Write clear Haskell and avoid frequent mistakes.

## Writing Haskell others can read

Idiomatic Haskell writes **type signatures** for all top-level definitions, models the domain with **ADTs and newtypes**, keeps **IO at the edges** and logic pure, prefers **total functions** (handling every input) to partial ones, uses `Maybe`/`Either` for expected failures, and chooses names and small functions over clever one-liners. Common pitfalls: **partial functions** (`head`, `tail`, `fromJust`, `read`, `!!` and `maximum` on empty lists); **`String`** for large text; **lazy `foldl`** and lazy accumulators causing space leaks; **`nub`** on large lists; **excessive point-free style** that hides meaning; **orphan instances**; **missing version bounds** on dependencies; overusing type-level features; confusing `return` with an early exit (in Haskell `return` just wraps a value; it does not stop a `do` block); `show` used for user-facing output or serialisation; and ignoring warnings. Enable `-Wall` (and consider `-Wincomplete-uni-patterns` and `-Wpartial-fields`), run HLint, and lean on the type checker: when refactoring, change a type and follow the compiler errors until everything compiles again.

## Partial versus total code

The same task written unsafely and safely.

```haskell
import Data.List (sortOn)
import qualified Data.List.NonEmpty as NE
import Data.List.NonEmpty (NonEmpty (..))
import Text.Read (readMaybe)

-- partial: crashes on empty input or a non-numeric price
cheapestBad :: [(String, String)] -> String
cheapestBad items = fst (head (sortOn (\(_, p) -> read p :: Int) items))

-- total: the type says what can go wrong, and every case is handled
data PriceError = NoItems | BadPrice String
  deriving Show

cheapest :: [(String, String)] -> Either PriceError String
cheapest items = do
  parsed <- traverse parse items
  case NE.nonEmpty parsed of
    Nothing -> Left NoItems
    Just ne -> Right (fst (NE.head (NE.sortWith snd ne)))
  where
    parse (sku, raw) = maybe (Left (BadPrice raw)) (\p -> Right (sku, p :: Int)) (readMaybe raw)

-- 'return' does not exit early: this always prints "done"
example :: IO ()
example = do
  _ <- return ()
  putStrLn "done"

main :: IO ()
main = do
  print (cheapest [("pen", "4950"), ("ink", "1990")])   -- Right "ink"
  print (cheapest [])                                    -- Left NoItems
  print (cheapest [("pen", "abc")])                      -- Left (BadPrice "abc")
  print (NE.head (3 :| [1, 2 :: Int]))                   -- 3: NonEmpty makes head total
```

## Let the compiler drive refactoring

In Haskell, changing a type is often the best first step of a refactor: GHC lists every place that needs updating, and when the code compiles again, most of the work is done.

**Quiz:** In a Haskell do block, what does `return x` do?

- [x] Wraps x in the monad; the following lines still run
- [ ] Exits the function immediately like in C
- [ ] Throws an exception
- [ ] Prints x

*Answer:* Wraps x in the monad; the following lines still run. return (like pure) only injects a value into the monad.
