Skip to content

Instantly share code, notes, and snippets.

@boj
Last active February 3, 2020 22:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boj/7ad0423e1ca10b7f0076a22bfb8b64e1 to your computer and use it in GitHub Desktop.
Save boj/7ad0423e1ca10b7f0076a22bfb8b64e1 to your computer and use it in GitHub Desktop.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RecordWildCards #-}
module Main where
--------------------------------------------------------------------------------
import Data.Coerce
import Data.Foldable
--------------------------------------------------------------------------------
-- a data type that must start in the Unbuilt state
-- and must end in the Built state to be considered safe
data Buildable = Unbuilt | Built
-- wrapper for an arbitrary data type which has been queried
newtype Queried a = Queried { unQueried :: a }
--------------------------------------------------------------------------------
data DataASource = DataASource Int deriving (Show)
getDataASources :: Int -> IO (Queried [DataASource])
getDataASources _ =
pure (Queried (fmap DataASource [1..5])) -- imagine an IO query here
--------------------------------------------------------------------------------
data DataA
= DataA
{ iid :: Int
} deriving (Show)
-- something similar to Beam -> Internal conversion
dasToDa :: DataASource -> DataA
dasToDa (DataASource i) = DataA i
--------------------------------------------------------------------------------
data DataB (b :: Buildable)
= DataB
{ datas :: [DataA]
} deriving (Show)
-- this should be the only way to construct a new `DataB`
-- via exported smart constructors
mkDataB :: DataB Unbuilt
mkDataB = DataB [] -- perhaps all but the internal list is initialized
-- in order to get a `DataB Built` it must go
-- through this function, and only this function
--
-- an empty list cannot be passed, it must be explicitly `Queried`
builtDataB :: DataB Unbuilt -> Queried [DataASource] -> DataB Built
builtDataB db@DataB{} das = coerce (db { datas = fmap dasToDa (unQueried das) })
getDataBById :: Int -> IO (DataB Unbuilt)
getDataBById _ = pure mkDataB -- imagine an IO query here
--------------------------------------------------------------------------------
-- logic
--------------------------------------------------------------------------------
buildDataB :: Int -> DataB Unbuilt -> IO (DataB Built)
buildDataB i dbb = do
das <- getDataASources i -- based on related id
pure (builtDataB dbb das) -- returns a full `DataB Built`
-- queries a `DataB` by some `Int` id
businessLogic :: Int -> IO (DataB Built)
businessLogic i = do
db <- getDataBById i -- unbuilt `DataB`
buildDataB i db -- built `DataB`
main :: IO ()
main = do
db <- businessLogic 1
print db
@boj
Copy link
Author

boj commented Feb 3, 2020

Based on a discussion where we had a hard to troubleshoot bug.

  1. Parent data type contains an empty list
  2. A query from an external system which populates the list can still return an empty list as a valid result
  3. Programmer forgot to wire up the query resulting in a lot of wasted time tracking down the problem

The goal with the above code is to make sure that the functions are wired up properly. In this case DataB starts in the Unbuilt case via the mkDataB smart constructor. The only way to convert it to a DataB Built is via the builtDataB function, and must take a Queried type so as to not receive an arbitrarily initialized value and defeat the purpose of all this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment