@@ -398,7 +398,7 @@ What if I wanted to write a function that processed items without adding or
398398removing 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.
402402updateItems :: Checklist -> IO Checklist
403403```
404404
@@ -408,21 +408,31 @@ implementations that don't modify the length of `items`?
408408Again 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
427437This intersects a lot with the [Higher - Kinded Data ][hkd] pattern . Maybe we _do_
428438have 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
628638var's ` s ` is different than the inner memory bank's ` s ` ), but this also
629639prevents variables from leaking. That's because the result type ` a ` must be
630640fully _ 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
633644By 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