Skip to content

Commit bc934d2

Browse files
committed
clarify
1 parent ee0648e commit bc934d2

File tree

1 file changed

+42
-21
lines changed

1 file changed

+42
-21
lines changed

copy/entries/five-point-haskell-1.md

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,24 @@ slug: five-point-haskell-part-1-total-depravity
88
series: five-point-haskell
99
---
1010

11-
Times have changed. The entire discipline of software development and
12-
engineering is under question, and over the past years has come under multiple
13-
movements re-thinking what it even means to write good, robust, correct code.
14-
Multiple language and framework wars have been fought, the zeitgeist constantly
15-
shifts. It is long overdue to clarify exactly the mindset on which we approach
16-
and define "good" coding principles.
17-
18-
In [this series][Five-Point Haskell], let's set out to establish a five-point
19-
unified framework of the "Typed Functional Programming" (and Haskell-derived)
20-
programming philosophy aimed to create code that is maintainable, correct,
21-
long-lasting, extensible, and beautiful to write and work with. These points
22-
will attempt to dispel thought leader sound-bytes that have become all too
23-
popular on Twitter --- "heresies", if you may permit the terminology.
11+
I have thought about distilling the principles in which I program Haskell, and
12+
how I've been able to steer long-lived projects (both personal and on teams)
13+
over years of growth, refactorings, and changes in demands. I find myself
14+
coming back to a few distinct and helpful "points" --- "doctrines", if you may
15+
allow me to say --- that have yet to lead me astray.
16+
17+
With a new age of software development coming, what does it even mean to write
18+
good, robust, correct code? It is long overdue to clarify exactly the mindset
19+
on which we approach and define "good" coding principles.
20+
21+
In this series, [*Five-Point Haskell*][Five-Point Haskell], let's set out to
22+
establish a five-point unified framework of the "Typed Functional Programming"
23+
(and Haskell-derived) programming philosophy aimed to create code that is
24+
maintainable, correct, long-lasting, extensible, and beautiful to write and
25+
work with. We'll try to reference real world case studies with and actual
26+
examples when we can, and also attempt to dispel thought leader sound-bytes
27+
that have become all too popular on Twitter --- "heresies", if you may permit
28+
the terminology.
2429

2530
[Five-Point Haskell]: https://blog.jle.im/entries/series/+five-point-haskell.html
2631

@@ -59,7 +64,7 @@ be better and better at keeping more in your mind.
5964
Actually _addressing_ these issues in most languages requires a lot of overhead
6065
and clunkiness. But luckily we're in Haskell!
6166

62-
### ID Mix-ups
67+
### Explicit Tags
6368

6469
The [2022 Atlassian Outage][atlassian], in some part, was the result of passing
6570
the wrong type of ID. The operators were intended to pass _App_ IDs, but
@@ -231,6 +236,12 @@ it (because it can run any `DbConnection a`)...but if any sub-function of a
231236
sub-function calls `clearTestEnv`, it will have to unite with `DbConnection
232237
Test`, which is impossible for a prod connection.
233238

239+
This is somewhat similar to using "mocking-only" subclasses for dependency
240+
injection, but with a closed universe. I discuss patterns like this in my
241+
[Introduction to Singletons][singletons] series.
242+
243+
[singletons]: https://blog.jle.im/entries/series/+introduction-to-singletons.html
244+
234245
Correct Representations
235246
-----------------------
236247

@@ -304,8 +315,6 @@ There are examples:
304315
* C's `fgetc()`, `getchar()`, returns -1 for `EOF`. And if you cast to
305316
`char`, you basically can't distinguish EOF from `0xff`, `ÿ`.
306317
* `malloc()` returning the pointer 0 means not enough memory
307-
* In other cases, an integer pointer being `0` means null pointer, a default
308-
null value for non-existence
309318
* Some languages have a special `NULL` pointer value as well --- or even a
310319
value `null` that can be passed in for any expected type or object or
311320
value.
@@ -343,6 +352,10 @@ unsafeIndexOf :: String -> String -> Int
343352
indexOf :: String -> String -> (Int -> r) -> (() -> r) -> r
344353
```
345354

355+
All of this just to [fake having actual sum types][faking].
356+
357+
[faking]: https://blog.jle.im/entry/faking-adts-and-gadts.html
358+
346359
We don't really have an excuse in Haskell, since we can just return `Maybe`:
347360

348361
```haskell
@@ -410,8 +423,10 @@ already return `NonEmpty` by default (like `some :: f a -> f (NonEmpty a)` or
410423
`group :: Eq a => [a] -> [NonEmpty a]`), allowing you to beautifully chain
411424
post-conditions directly into pre-conditions.
412425

413-
Sometimes the answer and issue will be a bit more subtle. This is our reminder
414-
to never let these implicit assumptions go unnoticed.
426+
Accessing containers is, in general, very fraught...even things like indexing
427+
lists can send us into a graveyard spiral. Sometimes the answer and issue will
428+
be a bit more subtle. This is our reminder to never let these implicit
429+
assumptions go unnoticed.
415430

416431
### Separate Processed Data
417432

@@ -434,9 +449,9 @@ saveUser :: Connection -> String -> IO (Maybe UUID)
434449
saveUser conn s
435450
| validUsername s = do
436451
newId <- query conn "INSERT INTO users (username) VALUES (?) returning user_id" (Only s)
437-
case newId of
438-
[] -> pure Nothing
439-
Only i : _ -> pure (Just i)
452+
pure $ case newId of
453+
[] -> Nothing
454+
Only i : _ -> Just i
440455
| otherwise = pure Nothing
441456

442457
getUser :: Connection -> UUID -> IO (Maybe String)
@@ -637,11 +652,13 @@ processAll :: Map Username Handle -> IO (Set Username)
637652
allocateFile :: FilePath -> ResourceT IO (ReleaseKey, Handle)
638653
allocateFile fp = allocate (openFile fp ReadMode) hClose
639654

655+
-- Guarantees that all handlers will eventually close, even if `go` crashes
640656
doTheThings :: Map Username FilePath -> IO ()
641657
doTheThings paths = runResourceT $ do
642658
releasersAndHandlers <- traverse allocateFile paths
643659
go releasersAndHandlers
644660
where
661+
-- normal operation: slowly releases handlers as we drop them
645662
go :: Map Username (ReleaseKey, Handle) -> ResourceT IO ()
646663
go currOpen = do
647664
toClose <- liftIO $ processAll (snd <$> currOpen)
@@ -709,6 +726,10 @@ anything, it might be _the_ bottleneck. If we can provide LLMs with properly
709726
giving them the right iterative tooling --- I (maybe naively) believe this
710727
might be the key to unlocking the full potential of agentic coding.
711728

729+
And...not whatever [this tweet is][tweet].
730+
731+
[tweet]: https://x.com/rywalker/status/2003525268821188746
732+
712733
### The Next Step
713734

714735
Total depravity is all about using types to _prevent errors_. However, most

0 commit comments

Comments
 (0)