author: 'Tom Harding' title: 'Microdisservices' patat: incrementalLists: true theme: code: [onDullWhite] codeBlock: [onVividWhite] ...
-
An event of some type is raised.
-
Some handler listening for that event is called.
-
The handler produces some number of new events.
type ExampleHandler
= HelloRequest
→ [Either HelloResponse InternalServerError]
- Fancier?
type ExampleHandler
= HelloRequest
→ [Either HelloResponse (Either ValidationError InternalServerError)]
- This does not scale.
-
We want any number of return types.
-
Generate nested
Either
with TemplateHaskell? -
Let's use a GADT to generalise
Either
instead:
data Variant (xs :: [Type]) where
Here :: x -> Variant (x ': xs)
There :: Variant xs -> Variant (y ': xs)
- Kind of a "pointer" in a list.
eg :: [ Variant '[String, Int, Bool] ]
eg
= [ Here "hello"
, There (Here 3)
, There (There (Here True))
]
-
An event of some type is raised.
-
Some handler listening for that event is called.
-
The handler produces some number of new events.
type Route (input :: Type) (outputs :: [Type])
= input -> [Variant outputs]
- ... with effects
type Route (f :: Type -> Type) (input :: Type) (outputs :: [Type])
= input -> f [Variant outputs]
- We have an event handler!
- A set of routes. That's... it.
type Service (f :: Type -> Type) (input :: Type) (outputs :: [Type])
= [ Route f input outputs ]
- We need to be a bit cleverer.
data Service (f :: Type -> Type) (routes :: [ (Type, [Type]) ]) where
SNil :: Service f '[]
(:+) :: Route f i o -> Service f xs -> Service f ( '(i, o) ': xs)
-
A service is a set of routes, indexed by a description of its router.
-
Services can only return events that they say they'll return!
-
We can define different semantics for how to handle overlaps (e.g. differently-indexed - or versioned - copies of the same event).
data EchoRequest
= EchoRequest
{ text :: String
, enthusiasm :: Bool
}
newtype EchoResponse
= EchoResponse
{ text :: String
}
route :: Route IO EchoRequest '[EchoResponse]
route (EchoRequest text enthusiasm) = do
putStrLn "RECEIVED REQUEST"
pure if enthusiasm
then [ Here $ EchoResponse (text <> "!") ]
else [ Here $ EchoResponse text ]
echo :: Service IO '[ '(EchoRequest, '[EchoResponse]) ]
echo = route :+ SNil
-
A microservice-architected app is just a list of microservices.
-
... but the elements of our lists have different types...
-
An
HList
! Act surprised.
data HListF (f :: k -> Type) (xs :: [k]) where
HNilF :: HListF f '[]
(:++) :: f x -> HListF f xs -> HListF f (x ': xs)
- (Note the
f
and thek
).
type App (f :: Type -> Type) (routes :: [[ (Type, [Type]) ]])
= HListF (Service f) routes
- 🎺 Haskell all the things 🎺
-
We have the description in the types!
-
We can write functions on this information!
-
"What events are being produced in this app?"
type family (xs :: [k]) ++ (ys :: [k]) :: [k] where
'[ ] ++ ys = ys
(x ': xs) ++ ys = x ': (xs ++ ys)
type family Published (app :: [[ Route_ ]]) :: [Type] where
Published '[ ] = '[]
Published ( x ': xs ) = Published_ x ++ Published xs
type family Published_ (app :: [ Route_ ]) :: [Type] where
Published_ '[ ] = '[]
Published_ ( '(_, x) ': xs ) = x ++ Published_ xs
- Small rant about
UnsaturatedTypeFamilies
not existing yet.
- "What events are being consumed in this app?"
type family Subscribed (app :: [[ Route_ ]]) :: [Type] where
Subscribed '[ ] = '[]
Subscribed ( x ': xs ) = Subscribed_ x ++ Subscribed xs
type family Subscribed_ (app :: [ Route_ ]) :: [Type] where
Subscribed_ '[ ] = '[]
Subscribed_ ( '(x, _) ': xs ) = x ': Subscribed_ xs
-
We can use
Published_
andSubscribed_
to interrogate individual services. -
This isn't just for curiosity's sake - we can make guarantees!
-
Let's write some other useful combinators at the type level...
- Drop an element from a type-level list.
type family Drop (x :: k) (xs :: [k]) :: [k] where
Drop x (x ': xs) = Drop x xs
Drop x (y ': xs) = y ': Drop x xs
Drop x '[ ] = '[]
- Difference of two type-level lists.
type family (xs :: [k]) \\ (ys :: [k]) :: [k] where
xs \\ '[] = xs
xs \\ (x ': ys) = Drop x xs \\ ys
- So...
- "What are we ignoring?"
type family AllHeard (routes :: [[ Route_ ]]) :: Constraint where
AllHeard routes = Unheard (Published routes \\ Subscribed routes)
type family Unheard (events :: [Type]) :: Constraint where
Unheard '[] = ()
Unheard xs = TypeError
( 'Text "The following events are produced, but nothing cares: "
':<>: 'ShowType xs
)
- We can detect "forgotten events" at compile time.
- "What are we waiting for?"
type family AllSaid (routes :: [[ Route_ ]]) :: Constraint where
AllSaid routes = Unsaid (Subscribed routes \\ Published routes)
type family Unsaid (events :: [Type]) :: Constraint where
Unsaid '[] = ()
Unsaid xs = TypeError
( 'Text "No one is producing any of the following events: "
':<>: 'ShowType xs
)
-
We can detect "dead routes".
-
Drum roll...
check :: (AllSaid rs, AllHeard rs) => App IO rs -> App IO rs
check = id
-
Describe data structures in a language-agnostic way.
-
... ish.
-
We need types that are generated from external files.
-
Maybe YAML because we don't like ourselves.
-
Turning
IO
to syntax... -
You guessed it.
braceYourself :: App IO $(yamlToApp "IDL.yaml")
- Surprisingly straightforward.
type Spec = [ Map String [ String ] ]
yamlToApp :: String -> Q Type
yamlToApp path
= decodeFileThrow @_ @Spec path
>>= pure . typeLevelList toService
typeLevelList :: (a -> Type) -> [a] -> Type
typeLevelList f
= foldr (AppT . AppT PromotedConsT . f) PromotedNilT
toService :: Map String [ String ] -> Type
toService = typeLevelList (uncurry toRoute) . assocs
where
toRoute :: String -> [String] -> Type
toRoute input
= InfixT (ConT (mkName input)) (mkName "~~>")
. typeLevelList (ConT . mkName)
-
Static analysis (
Selective
, anyone?) -
Deployment orchestration
-
Detecting infinite propagation
-
Visualisation tools