Let's imagine for a moment that you have a stack of functions to create "output values". You also put on your stack some "input" values. If you want a value of a given type you can:
- go through the list of existing values and if you find one with the desired type return it
- otherwise try to find a function returning a value of the given type
- if you find such a function apply the same algorithm to build all its input values
- every newly built value is put on top of the stack so it is available as an input to another function
You can eventually create a value out of the registry if:
- the value type is one of the existing values types
- or if its type is the output type of one of the functions
- the function inputs types are also existing value types or output types of other functions
- there are no cycles!
Let's use a registry
to deal with the "encoders" example given in the motivation section. We need first to introduce the type of encoders, Encoder
:
data Encoder a = Encoder { encode :: a -> JSON }
Then we can define a list of encoders and encoder functions:
nameEncoder = Encoder { encode (Name n) = string n }
ageEncoder = Encoder { encode (Age a) = number a }
employeeEncoder nameE ageE = Encoder {
encode (Employee n a) = obj ["n" .= nameE n , "a" .= ageE a]
}
departmentEncoder employeeE = Encoder {
encode (Department es) = obj ["employees" .= arr (employeeE <$> es)]
}
companyEncoder departmentE = Encoder {
encode (Company ds) = obj ["department" .= arr (departmentE <$> ds)
}
We can already see something interesting. The right levels of abstraction are respected because the departmentEncoder
doesn't have to know how the employeeEncoder
is implemented for example.
Now we put everything in a Registry
import Data.Registry
registry =
fun companyEncoder
<: fun departmentEncoder
<: fun employeeEncoder
<: fun ageEncoder
<: fun nameEncoder
In the code above <:
adds a new element to the registry. Functions are added with fun
which uses their Typeable
instance, to add a bit of description to exactly what has been added to the registry. Other values, if they have a Show
instance, provided a more complete description and can be added with the val
function. See the Reference guide for a list of all the functions which can be used to modify a registry.
With that registry
we can ask to make any encoder
-- enable {-# LANGUAGE TypeApplications #-}
nameEncoder1 = make @(Encoder Name) registry
companyEncoder1 = make @(Encoder Company) registry
Can we produce an Encoder Company
where all the names will be capitalized? Yes, by adding another Encoder Name
on top of the existing one in the registry:
nameCapitalizedEncoder = Encoder {
encode (Name n) = (nameEncoder & encode) (Name (capitalize n))
}
registry' = fun nameCapitalizedEncoder +: registry
companyEncoder2 = make @(Encoder Company) registry'
Since the resolution algorithm looks for values "top to bottom" on the registry stack it will find nameCapitalizedEncoder
to be used when building other encoders.
That's all it takes! Now you can have a look at the main reason for this library to exist: how to build applications.