Skip to content

Commit f65199d

Browse files
committed
it's coming together
1 parent 4902bf6 commit f65199d

File tree

2 files changed

+157
-30
lines changed

2 files changed

+157
-30
lines changed

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -791,14 +791,17 @@ And...not whatever [this tweet is][tweet].
791791

792792
### The Next Step
793793

794-
Total depravity is all about using types to _prevent errors_. However, most
795-
discussions about the power of types stop here. Oh, how boring types would be
796-
if this was all they gave us!
797-
798-
Types are also a power tool for _program design_, helping you guide the
799-
creation of your abstractions and pushing you to more expressive and robust
800-
designs. They aren't just for preventing bad code, they're for opening your
801-
mind to new avenues of design that were impossible before.
794+
Total depravity is all about using types to _prevent errors_. However, you can
795+
only go so far with defensive programming and carefully picking the structure
796+
of your types. Sometimes, it feels like you are expending a lot of energy and
797+
human effort just picking the perfectly designed data type, only for things out
798+
of your hand to ruin your typed castle.
799+
800+
In the next chapter, we'll see how a little-discussed aspect of Haskell's type
801+
system gives you a powerful tool for opening your mind to new avenues of design
802+
that were impossible before. At the same type, we'll see how we can leverage
803+
universal properties of mathematics itself to help us analyze our code in
804+
unexpected ways.
802805

803806
Let's explore this further in the next principle of [Five-Point
804807
Haskell][Five-Point Haskell], **Unconditional Election**!

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

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ What if I wanted to write a function that processed items without adding or
398398
removing any? Just each item in-place?
399399

400400
```haskell
401-
-- | Invariant: does not add or remove items
401+
-- | Invariant: Preserves the ordering of items, and their number.
402402
updateItems :: Checklist -> IO Checklist
403403
```
404404

@@ -408,21 +408,31 @@ implementations that don't modify the length of `items`?
408408
Again the answer can be: add quantification!
409409

410410
```haskell
411-
data Checklist f = Checklist
411+
data Checklist t = Checklist
412412
{ updated :: UTCTime
413-
, items :: f (Status, String)
413+
, items :: t (Status, String)
414414
}
415415

416-
-- | Guaranteed not to add or remove items
417-
updateItems :: Functor f => Checklist f -> IO (Checklist f)
416+
-- | Guaranteed not to add or remove or re-arrange items, but can still perform
417+
-- IO to get the new Status and String
418+
updateItems :: Traversable t => Checklist t -> IO (Checklist t)
418419
```
419420

420-
Your values would always be `Checklist []` in practice. We are _not_ using the
421-
type parameter to be able to "customize" our types and their structure, like
422-
sometimes having `Checklist Maybe` and `Checklist IO`. No, instead we are
423-
picking them to intentionally use them universally quantified in functions that
424-
process them, in order to take advantage of these automatically enforced
425-
properties.
421+
(Note: this guarantee relies on the `Traversable f` instance being lawful)
422+
423+
You can add even further guarantees: what if we wanted `updateItems` to only
424+
apply _pure_ functions to the checklist items? In that case, we can pick:
425+
426+
```haskell
427+
-- | Guaranteed not to add or remove or re-arrange items, but can must get the
428+
-- Status and String purely
429+
updateItems :: Functor t => Checklist t -> IO (Checklist t)
430+
```
431+
432+
Note, we are _not_ adding type parameters for abstraction or to be able to use
433+
"exotic checklists" (`Checklist Maybe`). Instead, we are intentionally using
434+
them universally quantified in functions that process them, in order to take
435+
advantage of these automatically enforced properties.
426436

427437
This intersects a lot with the [Higher-Kinded Data][hkd] pattern. Maybe we _do_
428438
have data we want to have multiple structural versions of:
@@ -628,19 +638,133 @@ Right off the bat, this prevents passing variables into nested calls (the first
628638
var's `s` is different than the inner memory bank's `s`), but this also
629639
prevents variables from leaking. That's because the result type `a` must be
630640
fully _independent_ of the `s`, so returning a `Var s` is illegal, since that
631-
would require the `a` to depend on `s`.
641+
would require the `a` to depend on `s`. (This is exactly how the `ST` monad
642+
works in GHC standard libraries, actually)
632643

633644
By requiring the user to give up control of the `s`, we ensure safety both of
634-
the library and of the user-given
645+
the library and of the user-given continuation. Now our memory-safety doesn't
646+
come from carefully tracking variables and their source memory blocks. Instead,
647+
it is assured through the universal of the `forall` and the unconditional
648+
properties it enforces.
649+
650+
Habits to Build
651+
---------------
652+
653+
Let's look at what it looks like to recognize this principle in practice, and
654+
use it in your code. Let's imagine we have a function that you can use to
655+
deploy a new `Config` in your environment:
656+
657+
```haskell
658+
deployConfig :: Config -> IO ()
659+
```
660+
661+
But, deployment is a bit expensive. So we want to de-duplicate our deploys:
662+
deploying the same `Config` twice would be a no-op. We can do this by keeping a
663+
`Config` in an `IORef`:
664+
665+
```haskell
666+
-- | returns True if changed, otherwise False if already deployd
667+
updateConfig :: IORef Config -> Int -> IO Bool
668+
updateConfig cache newConfig = do
669+
oldConfig <- readIORef cache
670+
if oldConfig == newConfig
671+
then pure False
672+
else do
673+
deployConfig newConfig
674+
writeIORef cache newConfig
675+
pure True
676+
```
677+
678+
This _works_, but after learning about the principles in this post, that type
679+
synonym should feel a little bit suspicious to you. Note that our function
680+
never actually _inspects_ the `Config` at all. The logic is independent.
681+
Would there be any value in pulling out the cacheing logic generically?
682+
683+
```haskell
684+
cachedUpdate :: Eq a => (a -> IO a) -> IORef a -> a -> IO Bool
685+
cachedUpdate action cache newVal = do
686+
oldConfig <- readIORef cache
687+
if oldConfig == newConfig
688+
then pure False
689+
else do
690+
deployConfig newConfig
691+
writeIORef cache newConfig
692+
pure True
693+
694+
updateConfig :: IORef Config -> Int -> IO Bool
695+
updateConfig = cachedUpdate deployConfig
696+
```
697+
698+
Let's presume that we never intend on re-using `cachedUpdate`. So, we just
699+
increased our total lines of code...and for what? What does having
700+
`cachedUpdate` get us?
701+
702+
Firstly, in the original monomorphic `updateConfig`, written directly against
703+
`Config`, there is so much that could go wrong. Maybe you could mis-handle the
704+
config or accidentally modify it. You could set certain fields to certain
705+
fixed. You might end up deploying a configuration that was never passed in
706+
directly.
707+
708+
In our `cachedUpdate` implementation, we are _sure_ that any `Config`
709+
deployed will _only_ come _directly_ from calls to `updateConfig`. No other
710+
operations are possible
711+
712+
Secondly, the type signature of `cachedUpdate` tells us a lot more about what
713+
`cachedUpdate`'s intended logic is and what exactly it can support. Let's say
714+
in the future, a new requirement comes: Deploy a "default" config if
715+
`deployConfig` ever fails.
716+
717+
You _want_ something as drastic like this to require you to change your types
718+
and your contracts. In fact, if a new requirement comes along and you are able
719+
to implement it without changing your types, that should be extremely scary to
720+
you, because you previously allowed way too many potentially invalid programs
721+
to compile.
722+
723+
If we were to add such a change ("deploy a default `Config`"), it _should_ have
724+
us go back to the type signature of `cachedUpdate` and see how it must change
725+
in order for us to support it. That process of interrogation makes us think
726+
about what we actually want to do and how it would fundamentally change our
727+
data flow.
728+
729+
If you subscribe to ["SOLID" programming][solid], this should all remind you of
730+
"Dependency Inversion".
731+
732+
[solid]: https://en.wikipedia.org/wiki/SOLID
733+
734+
Basically: treat all monomorphic code with suspicion. Maybe it's a symptom of
735+
you trying to hold on to more control, when you really should be letting go.
736+
737+
Embracing Unconditional Election
738+
--------------------------------
739+
740+
What sort of control are you trying to hang on to in life, in a way
741+
that puts you in your own prison?
742+
743+
To me, the fact that making code more polymorphic and giving up information is
744+
valuable not just for abstraction, but for taking advantage of universal
745+
properties, was a surprising one. But ever since starting writing Haskell,
746+
it's a fact that I take advantage of every day. So, next time you see the
747+
opportunity, try thinking about what that `forall` can do for you?
748+
749+
### The Next Step
750+
751+
Embracing Total Depravity and Unconditional Election should redefine your
752+
relationship with your code. But not all code lives in the nice pure world
753+
where we can cordon off effects. `forall a. [a] -> [a]` is very different than
754+
`forall a. [a] -> IO [a]`, after all.
755+
756+
To extend these boundaries to useful code, we have to deal with that boundary
757+
between the world of the pure and the world where [things actually happen for
758+
real][xkcd]. We'll explore the nuances of that boundary in the next chapter of
759+
[Five-Point Haskell][Five-Point Haskell], **Limited Atonement**.
760+
761+
[xkcd]: https://xkcd.com/1312/
635762

636-
<!-- bump abstraction, updator -->
763+
Special Thanks
764+
--------------
637765

638-
<!-- * Parametric polymorphism --- guess the implementation -->
639-
<!-- * `[Int] -> [Int]` vs `[a] -> [a]` -->
640-
<!-- * Compare with refinement types -->
641-
<!-- * higher-kinded data, parametric over functors -->
642-
<!-- * Subtyping via parametric polymorphism -->
643-
<!-- * Phantoms + parametric polymoirphism, restrictions, ST trick -->
644-
<!-- * Princple of least strength, Monad vs Applicative -->
645-
<!-- * typeclass-based limitation of functions -->
766+
I am very humbled to be supported by an amazing community, who make it possible
767+
for me to devote time to researching and writing these posts. Very special
768+
thanks to my supporter at the "Amazing" level on [patreon][], Josh Vera! :)
646769

770+
[patreon]: https://www.patreon.com/justinle/overview

0 commit comments

Comments
 (0)