Community Series
Hi folks, Rebecca Mark here, kicking off the new Unison community focused blogpost series. In this series, we're going to be featuring different contributors' work in addition to the usual release notes and company news. We want the blog to highlight Unison community voices and projects because we know that no programming language exists without the people who write in it and the problems they're interested in solving. We hope to feature more community generated content in the future! If you have an engineering topic or Unison project you'd like to write about, email community@unison.cloud and get in touch.
With that in mind, we'll turn it over to the author of this post: Tavish Pegram. Tavish is a Software Engineer at Peloton. His interests include DDD, FP, TDD, XP, and other letters as well.
The Kata
Goals
This kata is normally used to practice TDD, dependency injection, and ports and adapters architectures in an Object Oriented style. Instead, we will tackle the same problem in Unison and explore TDD, dependency rejection, and how pure FP languages naturally encourage ports and adapters like architectures (through seperation of pure and impure functions).
If you aren't familiar with "ports and adapters" (also called "Hexagonal Architecture", "The Clean Architecture", and "The Onion Architecture") the idea is to invert dependencies between pure business logic and infrastructure code, usually using interfaces, so that implementation details like the DB, transfer protocols, etc. is fully decoupled from the actual business logic -- making it easier to extend, update, maintain, and test!
At a more technical level, we will explore how to do domain modeling with the Unison type system, how to use abilities, and how to write tests.
The Prompt
As the HR department, I want to send an email to every employee on their birthday so that they feel appreciated and company morale is kept high.
Technical Notes:
- Loads a set of employees from somewhere (we haven't decided yet if its a database, or a flat file, or just in memory)
- Sends an email to all employees whose birthday is today.
Platform Goals:
- Testable: we should be able to test the internal application logic with no need to ever send a real email.
- Flexible: we anticipate that the data source in the future could change from a flat file to a relational database, or perhaps a web service. We also anticipate that the email service could soon be replaced with a service that sends greetings through Facebook or some other social network.
- Well-designed: clearly separate the business logic from the infrastructure.
For further information about this kata please checkout the original post here.
Our Approach
First, we will come up with a rich domain model and discuss different ways we can model the system to take advantage of the compiler and what the resulting tradeoffs are.
Second, we will talk about Abilities, dependency injection / rejection, and how we can decouple possibly impure infrastructure from pure decision making logic.
Third, we will apply TDD to implement our behavior.
Modeling
Unison provides us with all the algebraic data types we need to build and compose types. In lieu of discussing them up front, we will just jump into modeling and discuss syntax and decisions as we go!
From skimming over the prompt, it seems pretty clear that top level types we need are Employee
and Email
. Let's start with Employee
.
Employee
So what is an employee to us? For one, they are an entity and therefore should have a unique id
. Some other fields we probably care about include the employee's name
, and importantly, their birthday
.
Obviously, there are millions of other fields we could choose to include in our model, but we would also like to only include what we care about. In a larger more complex system, the Employee model may have many other fields we don't care about but make sense to include in the broader modelling context (see "bounded context", Domain Driven Design).
So lets start with:
unique type EmployeeId = EmployeeId Text
unique type FirstName = FirstName Text
unique type LastName = LastName Text
unique type Name =
{ first: FirstName
, last: LastName
}
unique type Date =
{ day: Nat
, month: Nat
, year: Nat
}
unique type Employee =
{ id: EmployeeId
, name: Name
, birthday: Date
}
Decision 1 - Record Types
Our employee is a record type consisting of an id
, name
, and a birthday
. In Unison, the record type syntax is actually equivalent to a normal data type like
type Employee = Employee EmployeeId Name Date
except the compiler will automatically generate a collection of functions that allow for easy access and updates of this immutable data structure, which will be a big help later. You can see this in the output in ucm
:
I found and typechecked these definitions in ~/projects/scratch.u. If you do an `add` or
`update`, here's how your codebase would change:
⍟ These new definitions are ok to `add`:
unique type Date
unique type Employee
unique type EmployeeId
unique type FirstName
unique type LastName
unique type Name
Date.day : Date -> Nat
Date.day.modify : (Nat ->{g} Nat) -> Date ->{g} Date
Date.day.set : Nat -> Date -> Date
Date.month : Date -> Nat
Date.month.modify : (Nat ->{g} Nat) -> Date ->{g} Date
Date.month.set : Nat -> Date -> Date
Date.year : Date -> Nat
Date.year.modify : (Nat ->{g} Nat) -> Date ->{g} Date
Date.year.set : Nat -> Date -> Date
Employee.birthday : Employee -> Date
Employee.birthday.modify : (Date ->{g} Date) -> Employee ->{g} Employee
Employee.birthday.set : Date -> Employee -> Employee
Employee.id : Employee -> EmployeeId
Employee.id.modify : (EmployeeId ->{g} EmployeeId) -> Employee ->{g} Employee
Employee.id.set : EmployeeId -> Employee -> Employee
Employee.name : Employee -> Name
Employee.name.modify : (Name ->{g} Name) -> Employee ->{g} Employee
Employee.name.set : Name -> Employee -> Employee
Name.first : Name -> FirstName
Name.first.modify : (FirstName ->{g} FirstName) -> Name ->{g} Name
Name.first.set : FirstName -> Name -> Name
Name.last : Name -> LastName
Name.last.modify : (LastName ->{g} LastName) -> Name ->{g} Name
Name.last.set : LastName -> Name -> Name
Name
and Date
are record types as well.
Decision 2 - Nominal vs Structural Typing
Also note that we have opted in to using nominal typing (with the unique
keyword) instead of structural typing. If we had left out unique
the compiler would consider EmployeeId
, FirstName
, and LastName
all to be the same type (with all names being interchangeable). For our current use case, we want to make sure that the compiler considers all of these different types, so we can prevent mistakes like accidentally passing in a LastName
where an EmployeeId
should go.
Decision 3 - Avoiding Primitives
We could have also modeled the Employee like this (or any other similar way)
unique type Employee =
{ id: Text
, name: Name
, birthday: Date
}
unique type Name =
{ first: Text
, last: Text
}
unique type Date =
{ day: Nat
, month: Nat
, year: Nat
}
When deciding whether to use primitives or to wrap them in more rich domain types we should consider some tradeoffs:
- Using primitive types gives the compiler less context on what you are attempting to model. For example, using
Text
for the id, the first name, and the last name, means the compiler wouldn't catch an easy mistake like
foo =
id = "12345"
firstName = "John"
lastName = "Lennon"
employee = Employee id (Name lastName firstName) (Date 1 1 1991)
(We've transposed the employees name!) Or even worse, maybe we accidentally swap the id and last name! However, the compiler would catch this problem.
foo =
id = EmployeeId "12345"
firstName = FirstName "John"
lastName = LastName "Lennon"
employee = Employee id (Name lastName firstName) (Date 1 1 1991)
()
The 1st argument to `Name`
has type: LastName
but I expected: FirstName
9 | unique type LastName = LastName Text
10 | unique type Name =
11 | { first: FirstName
.
23 | employee = Employee id (Name lastName firstName) (Date 1 1 1991)
Of course, you can still make a mistake like
firstName = LastName "Lennon"
But it's much harder to make this mistake. If we are ok with the extra types and the overhead of deconstructing them, we can catch many more typos/bugs at compile time, which means fewer tests are necessary to ensure correctness!
- In the future, Unison may introduce the ability to make to make data constructors private and/or refined types. Doing so would allow us to to avoid another huge source of bugs when using primitive types directly, which is that the set of possible values is much larger than what we are attempting to model. You can see this in our model for
Date
:
unique type Date =
{ day: Nat
, month: Nat
, year: Nat
}
By modeling day/month/year all as Nat
we have introduced the possibility of an illegal state into our system. For example, in this model, we could have illegal dates like 0/500/99999999999999
. In fact, lets try to restrict the possible values a bit, to reduce the surface area for future bugs. This means more tests and more runtime validation (and runtime validation failures).
Lets try to contrain the possible values a bit.
unique type Date =
{ day: Day
, month: Month
, year: Year
}
unique type Month
= January
| February
| March
| April
| May
| June
| July
| August
| September
| October
| November
| December
unique type Day = Day Nat
unique type Year = Year Nat
It is really easy for us to enumerate all of the possible months. We have eliminated the possiblity of an invalid month appearing in our system! Now we have to decide how far we want to go to make illegal state unrepresentable. Maybe Nat
is ok for representing a year if we don't care about BCE
and we are ok with possibly talking about birthdays millions of years in the future (not ideal). But it would be nice to disallow days of months that don't exist, and this is where refined types, dependent types, or private constructors could be leveraged. Hopefully, we will have those one day!
Let's apply some similar decision making to get an Email.
unique type Email =
{ to: EmailAddress
, subject: Subject
, body: Body
}
unique type EmailAddress = EmailAddress Text
unique type Subject = Subject Text
unique type Body = Body Text
Let's also update Employee
to have an email address
unique type Employee =
{ id: EmployeeId
, name: Name
, birthday: Date
, email: EmailAddress
}
But maybe we can build a little bit toward the future here. The prompt told us the future will likely include sending birthday messages via other platforms, like SMS or robocalls.
Maybe our model should actually be about a Message
of which there is some relationship with an Email
. One possible way to model this would be
unique type Message
= Email EmailAddress Subject Body
which would give us the option of extending it in the future as needed
unique type Message
= Email EmailAddress Subject Body
| SMS PhoneNumber Body
| TwitterDM TwitterHandle Body
Summary
There isn't one "best" way to model this, but this is one possible way:
unique type Employee =
{ id: EmployeeId
, name: Name
, birthday: Date
}
unique type EmployeeId = EmployeeId Text
unique type FirstName = FirstName Text
unique type LastName = LastName Text
unique type Name =
{ first: FirstName
, last: LastName
}
unique type Date =
{ day: Day
, month: Month
, year: Year
}
unique type Month
= January
| February
| March
| April
| May
| June
| July
| August
| September
| October
| November
| December
unique type Day = Day Nat
unique type Year = Year Nat
unique type Message
= Email EmailAddress Subject Body
unique type EmailAddress = EmailAddress Text
unique type Subject = Subject Text
unique type Body = Body Text
Abilities
Now that we have our model, let's quickly talk about the effects/infrastructure we know we will need.
One of the goals of this exercise (and generally working as a software engineer) is to decouple infrastructure from our business logic (which is often approximately the same thing as separating pure and impure code). In OO we would use interfaces to define contracts and dependency inject in whatever implementations we need (be it Postgres, AWS, an in-memory store, mocks, etc).
In Unison, we leverage Abilities (also called Algebraic Effects) to accomplish something similar. See the abilities documentation for a more detailed overview of Abilities.
In our case, let's limit ourselves to the following effects: sending a message, fetching employees from somewhere (a flat file, a db, an api, anything), and fetching the current date. For the sake of simplicity let's ignore an exception throwing ability (and failures in general) or any other abilities that may make sense to add in a real production system.
unique ability MessageService where
send: Message -> Message
unique ability EmployeeRepository where
fetchAllByBirthday: (Day, Month) -> [Employee]
unique ability Calendar where
today: Date
Let's walk through what these abilities are doing:
MessageService
is an ability that provides the interface for a single commandsend
.send
s type signature can be read as "a function that takes a message and returns a (same) message, requiring theMessageService
ability."EmployeeRepository
provides the interfacefetchAllByBirthday
which takes aDay
and aMonth
pair and returns a list ofEmployees
, requiring theEmployeeRepository
ability.Calendar
provides the interfacetoday
which is a function that takes no arguments and returns aDate
, requiring theCalendar
ability.
Note that abilities
- can provide multiple interfaces if we wanted
unique ability EmployeeRepository where
fetchAllByBirthday: (Day, Month) -> [Employee]
fetchAll: [Employee]
findById: EmployeeId -> {Employee}
- have their behavior defined elsewhere (using handler syntax we will get to later)
- are composable (hence "algebraic effects")
- require that any calling function provide the ability in their type signature, which bubbles all the way to the top of the call stack (until it finds a handler).
So let's scaffold our main function for this exercise and see this stuff in action.
sendBirthdayEmails : [Message]
sendBirthdayEmails =
todo "get the current date"
todo "fetch all the employees with birthdays today"
todo "create happy birthday messages"
todo "send and return the messages"
Let's say we want a function we can call that will find all the employees whose birthday is today, create emails for them, send those emails, and then return those emails so we can see what was created and sent.
As an aside, let's avoid modeling failures simply because it will clutter this exercise a bit. In real life, many of our effects would have type signatures with Either
so we could model possible failures, but let's just naively pretend everything always works.
So let's do some more scaffolding!
sendBirthdayEmails : [Message]
sendBirthdayEmails =
today' = today
employees = fetchAllByBirthday (Date.day today', Date.month today')
messages = map (Employee.toBirthdayMessage) employees
map (send) messages
Employee.toBirthdayMessage : Employee -> Message
Employee.toBirthdayMessage _ = todo "toBirthdayMessage"
So we will grab the date, use that to fetch all the employees with birthdays today, map those employees to messages, and then send out (and return) those messages.
Uh oh! The compiler is mad!
The expression in red needs the {Calendar} ability, but this location does not have access to any abilities.
136 | today' = today)
Ah, turns out that any function with an ability in its signature can only be called by another function with that ability available in its signature. So we need to add all our abilities to the function signature. So let's just go ahead and add all three.
sendBirthdayEmails : {MessageService, EmployeeRepository, Calendar} [Message]
But we are stil getting the same error. 🙁
Well, it turns out that these effects can actually only be run in the context of a handler and they have to be in a function body. We should defer the computation with a thunk (so all effects become deferred) and then wire our handler up.
sendBirthdayEmails : '{MessageService, EmployeeRepository, Calendar} [Message]
sendBirthdayEmails = 'let
today' = today
employees = fetchAllByBirthday (Date.day today', Date.month today')
messages = map (Employee.toBirthdayMessage) employees
map (send) messages
Employee.toBirthdayMessage : Employee -> Message
Employee.toBirthdayMessage _ = todo "toBirthdayMessage"
And the compiler seems happy so far! Note that we just deferred the entire computation by putting the 'let
at the beginning.
Let's move on applying TDD in order to implement the remaining code.
Implementation!
Let's write a first failing test!
test> sendBirthdayEmails.tests.noBirthdaysToday =
check let
actual = !sendBirthdayEmails
expected = []
if expected === actual
then true
else bug (exected, actual)
The expression in red needs these abilities: {Calendar, EmployeeRepository, MessageService}, but this location does not have access to any abilities.
70 | actual = !sendBirthdayEmails
Well we can't defer this call any more, this is the boundary of the system! So it's time to implement handlers for our abilities!
test> sendBirthdayEmails.tests.noBirthdaysToday =
check let
actual =
handle
handle
handle
!sendBirthdayEmails
with (Calendar.handler.mock (Date (Day 1) January (Year 1991)))
with (EmployeeRepository.handler.mock [])
with MessageService.handler.mock
expected = []
if expected === actual
then true
else bug (expected, actual)
Calendar.handler.mock : Date -> {Calendar} k -> k
Calendar.handler.mock date k =
h : Request {Calendar} k -> k
h = cases
{today -> resume} -> handle resume date with h
{ resume } -> resume
handle k with h
EmployeeRepository.handler.mock : [Employee] -> {EmployeeRepository} k -> k
EmployeeRepository.handler.mock employees k =
h : Request {EmployeeRepository} k -> k
h = cases
{ fetchAllByBirthday birthday -> resume } ->
birthdays = filter (employee -> (Date.day (Employee.birthday employee), Date.month (Employee.birthday employee)) === birthday) employees
handle resume birthdays with h
{ resume } -> resume
handle k with h
MessageService.handler.mock : {MessageService} k -> k
MessageService.handler.mock k =
h : Request {MessageService} k -> k
h = cases
{ send message -> resume } -> handle resume message with h
{ resume } -> resume
handle k with h
This is a lot so lets dig into it a bit!
The syntax for a handler is handle <fn> with <handler>
. I've wrapped our call to !sendBirthdayEmails
in three nested handle blocks (we can make this cleaner later).
When you write a handler, your goal is to pattern match on the possible ability function signatures, and then "resume" the computation (or not) along with providing the expected value for the ability. Let's dig in to our mock Calendar handler:
Calendar.handler.mock : Date -> {Calendar} k -> k
Calendar.handler.mock date k =
h : Request {Calendar} k -> k
h = cases
{today -> resume} -> handle resume date with h
{ resume } -> resume
handle k with h
- our mock calendar handler dates a
Date
as input. This is so we can specify the return value of `today`, so we can "mock" it in our tests. - the
k
is the "continuation", our handler is being consulted to try to handle an ability, and then we call k to "continue" the existing computation. - we define some handler
h
locally which does the actual bulk of the work. It pattern matches on any ability operations we care about. Note that its signature has access to theCalendar
ability. - we pattern match on two cases. The first is the
today
case. If we are handling thetoday
method, all we are doing is handling the continuation (by calling resume) and providing the value fortoday
which is the provideddate
since are just "mocking" the date in this handler. The continuation is called and handled recursively. - we have a base
resume
pattern where we are basically saying "if this handler encounters behavior which doesn't call the ability operations, just bubble this result up to the caller."
In summary, Calendar.handler.mock
takes a Date
, and any function it handles that calls `today` will return that date.
Now let's look at the EmployeeRepository
:
EmployeeRepository.handler.mock : [Employee] -> {EmployeeRepository} k -> k
EmployeeRepository.handler.mock employees k =
h : Request {EmployeeRepository} k -> k
h = cases
{ fetchAllByBirthday birthday -> resume } ->
birthdays = filter (employee -> (Date.day (Employee.birthday employee), Date.month (Employee.birthday employee)) === birthday) employees
handle resume birthdays with h
{ resume } -> resume
handle k with h
This is another mock to make it easy to test our program.
- This handler takes a list of employees, which it uses to generate the return value for
fetchAllByBirthday
. - Our
h
take a request with theEmployeeRepository
ability. - We match on the
fetchAllByBirthday
method and it's argumentbirthday
. - When we match on that method, we filter the provided list of employees based on the
birthday
arg, and resume the computation with those employees as the return value. - In real life, we might read from a DB here.
- Otherwise we bubble up the function in case other handlers know how to handle it.
And finally
MessageService.handler.mock : {MessageService} k -> k
MessageService.handler.mock k =
h : Request {MessageService} k -> k
h = cases
{ send message -> resume } -> handle resume message with h
{ resume } -> resume
handle k with h
- This handler does not take any args because it doesn't need any outside info to handle the
send
function. - We match on the
send
function and its message argument and resume the computation with that same message as a return value - A real version of this might instead reach out to Twilio or some external service to perform an effect before continuing.
- And we bubble up any functions we don't care about.
Great! And then we just wrap all these handlers around our birthday function.
test> sendBirthdayEmails.tests.noBirthdaysToday =
check let
actual =
handle
handle
handle
!sendBirthdayEmails
with (Calendar.handler.mock (Date (Day 1) January (Year 1991)))
with (EmployeeRepository.handler.mock [])
with MessageService.handler.mock
expected = []
if expected === actual
then true
else bug (expected, actual)
And thats how we do "dependency injection" in unison! We can easily swap out these handlers with other behaviors at the boundary of our system!
Now that we have a passing test, let's take a minute to refactor a bit before we add some more interesting test cases.
All these nested handlers are kind of gross. Let's combine them into a single "multihandler".
sendBirthdayEmails.handlers.mock : Date -> [Employee] -> '{Calendar, EmployeeRepository, MessageService} k -> k
sendBirthdayEmails.handlers.mock date employees k =
h : Request {Calendar, EmployeeRepository, MessageService} k -> k
h = cases
{today -> resume} -> handle resume date with h
{ send message -> resume } -> handle resume message with h
{ fetchAllByBirthday birthday -> resume } ->
birthdays = filter (employee -> (Date.day (Employee.birthday employee), Date.month (Employee.birthday employee)) === birthday) employees
handle resume birthdays with h
{ resume } -> resume
handle !k with h
test> sendBirthdayEmails.tests.noBirthdaysToday =
check let
today = Date (Day 1) January (Year 2021)
actual = sendBirthdayEmails |> sendBirthdayEmails.handlers.mock today []
expected = []
if expected === actual
then true
else bug (expected, actual)
Note that we have changed how we use the handlers to something slightly more idiomatic: a normal function call! I've chosen to pipe in the function as the last argument because I think it puts the "important" bit at the beginning of the line.
Note (again) that we updated the handler's signature to defer the first k
so we no longer need to add !
to the top level test since it is done inside the handler.
Alright let's test with the next test case, a single birthday today!
test> sendBirthdayEmails.tests.oneBirthdayToday =
check let
employees =
[
Employee
(EmployeeId "1")
(Name (FirstName "John") (LastName "Smith"))
(Date (Day 1) January (Year 1991))
(EmailAddress "john@smith.com")
, Employee
(EmployeeId "2")
(Name (FirstName "Fred") (LastName "Smith"))
(Date (Day 2) March (Year 1991))
(EmailAddress "fred@smith.com")
]
today = Date (Day 1) January (Year 2021)
actual = sendBirthdayEmails |> sendBirthdayEmails.handlers.mock today employees
expected =
[
Email
(EmailAddress "john@smith.com")
(Subject "Happy Birthday!")
(Body "Congrats! Love, HR.")
]
if expected === actual
then true
else bug (expected, actual)
💔💥
I've encountered a call to builtin.todo with the following value:
"toBirthdayMessage"
That's a good failure! Let's fix it!
Employee.toBirthdayMessage : Employee -> Message
Employee.toBirthdayMessage employee =
Email (Employee.email employee) (Subject "Happy Birthday!") (Body "Congrats! Love, HR.")
70 | check let
✅ Passed : Proved.
81 | check let
✅ Passed : Proved.
It's working! Let's do one more test case just to be sure.
test> sendBirthdayEmails.tests.twoBirthdaysToday =
check let
employees =
[
Employee
(EmployeeId "1")
(Name (FirstName "John") (LastName "Smith"))
(Date (Day 1) January (Year 1991))
(EmailAddress "john@smith.com")
, Employee
(EmployeeId "2")
(Name (FirstName "Fred") (LastName "Smith"))
(Date (Day 2) March (Year 1991))
(EmailAddress "fred@smith.com")
, Employee
(EmployeeId "3")
(Name (FirstName "Abe") (LastName "Lincoln"))
(Date (Day 2) March (Year 1776))
(EmailAddress "abe@lincoln.com")
]
today = Date (Day 2) March (Year 2021)
actual = sendBirthdayEmails |> sendBirthdayEmails.handlers.mock today employees
expected =
[
Email
(EmailAddress "fred@smith.com")
(Subject "Happy Birthday!")
(Body "Congrats! Love, HR.")
, Email
(EmailAddress "abe@lincoln.com")
(Subject "Happy Birthday!")
(Body "Congrats! Love, HR.")
]
if expected === actual
then true
else bug (expected, actual)
70 | check let
✅ Passed : Proved.
81 | check let
✅ Passed : Proved.
113 | check let
✅ Passed : Proved.
Great! And of course there many more tests you can add to make sure this is working for various corner cases, but this seems pretty good for now!
So let's go back and refactor our main function a bit now that we have the stability of some tests.
sendBirthdayEmails : '{Calendar, EmployeeRepository, MessageService} [Message]
sendBirthdayEmails = 'let
today
|> toDayMonthPair
|> fetchAllByBirthday
|> toBDayMessages
|> sendAll
toDayMonthPair : Date -> (Day, Month)
toDayMonthPair = cases
Date day month _ -> (day, month)
toBDayMessages : [Employee] -> [Message]
toBDayMessages employees =
map (Employee.toBirthdayMessage) employees
sendAll : [Message] ->{MessageService} [Message]
sendAll messages =
map (send) message
Looks a lot cleaner, its easier to read, and its composed of a bunch of small modular easy to understand functions!
Conclusion
Let's see it all together!
unique type Employee =
{ id: EmployeeId
, name: Name
, birthday: Date
, email: EmailAddress
}
unique type EmployeeId = EmployeeId Text
unique type FirstName = FirstName Text
unique type LastName = LastName Text
unique type Name =
{ first: FirstName
, last: LastName
}
unique type Date =
{ day: Day
, month: Month
, year: Year
}
unique type Month
= January
| February
| March
| April
| May
| June
| July
| August
| September
| October
| November
| December
unique type Day = Day Nat
unique type Year = Year Nat
unique type Message
= Email EmailAddress Subject Body
unique type EmailAddress = EmailAddress Text
unique type Subject = Subject Text
unique type Body = Body Text
unique ability MessageService where
send: Message -> Message
unique ability EmployeeRepository where
fetchAllByBirthday: (Day, Month) -> [Employee]
unique ability Calendar where
today: Date
sendBirthdayEmails : '{Calendar, EmployeeRepository, MessageService} [Message]
sendBirthdayEmails = 'let
today
|> toDayMonthPair
|> fetchAllByBirthday
|> toBDayMessages
|> sendAll
toDayMonthPair : Date -> (Day, Month)
toDayMonthPair = cases
Date day month _ -> (day, month)
toBDayMessages : [Employee] -> [Message]
toBDayMessages employees =
map Employee.toBirthdayMessage employees
sendAll : [Message] ->{MessageService} [Message]
sendAll messages =
map send messages
Employee.toBirthdayMessage : Employee -> Message
Employee.toBirthdayMessage employee =
Email (Employee.email employee) (Subject "Happy Birthday!") (Body "Congrats! Love, HR.")
sendBirthdayEmails.handlers.mock : Date -> [Employee] -> '{Calendar, EmployeeRepository, MessageService} k -> k
sendBirthdayEmails.handlers.mock date employees k =
h : Request {Calendar, EmployeeRepository, MessageService} k -> k
h = cases
{today -> resume} -> handle resume date with h
{ send message -> resume } -> handle resume message with h
{ fetchAllByBirthday birthday -> resume } ->
birthdays = filter (employee -> (Date.day (Employee.birthday employee), Date.month (Employee.birthday employee)) === birthday) employees
handle resume birthdays with h
{ resume } -> resume
handle !k with h
test> sendBirthdayEmails.tests.noBirthdaysToday =
check let
today = Date (Day 1) January (Year 2021)
actual = sendBirthdayEmails |> sendBirthdayEmails.handlers.mock today []
expected = []
if expected === actual
then true
else bug (expected, actual)
test> sendBirthdayEmails.tests.oneBirthdayToday =
check let
employees =
[
Employee
(EmployeeId "1")
(Name (FirstName "John") (LastName "Smith"))
(Date (Day 1) January (Year 1991))
(EmailAddress "john@smith.com")
, Employee
(EmployeeId "2")
(Name (FirstName "Fred") (LastName "Smith"))
(Date (Day 2) March (Year 1991))
(EmailAddress "fred@smith.com")
]
today = Date (Day 1) January (Year 2021))
actual = sendBirthdayEmails |> sendBirthdayEmails.handlers.mock today employees
expected =
[
Email
(EmailAddress "john@smith.com")
(Subject "Happy Birthday!")
(Body "Congrats! Love, HR.")
]
if expected === actual
then true
else bug (expected, actual)
test> sendBirthdayEmails.tests.twoBirthdaysToday =
check let
employees =
[
Employee
(EmployeeId "1")
(Name (FirstName "John") (LastName "Smith"))
(Date (Day 1) January (Year 1991))
(EmailAddress "john@smith.com")
, Employee
(EmployeeId "2")
(Name (FirstName "Fred") (LastName "Smith"))
(Date (Day 2) March (Year 1991))
(EmailAddress "fred@smith.com")
, Employee
(EmployeeId "3")
(Name (FirstName "Abe") (LastName "Lincoln"))
(Date (Day 2) March (Year 1776))
(EmailAddress "abe@lincoln.com")
]
today = Date (Day 2) March (Year 2021)
actual = sendBirthdayEmails |> sendBirthdayEmails.handlers.mock today employees
expected =
[
Email
(EmailAddress "fred@smith.com")
(Subject "Happy Birthday!")
(Body "Congrats! Love, HR.")
, Email
(EmailAddress "abe@lincoln.com")
(Subject "Happy Birthday!")
(Body "Congrats! Love, HR.")
]
if expected === actual
then true
else bug (expected, actual)
References
Scott Wlaschin - Domain Modeling Made Functional
Mark Seemann - Dependency Injection to Dependency Rejection
Get In Touch
If you've been working on a Unison project or have a programming topic you'd like to explore in Unison, we want to hear from you! Email community@unison.cloud and we'll help you get published here.