Welcome to the third installment of our Deno series, where we build a script that pairs up people for coffee.
In the last post, we're dynamically pulling members of the Justice League from GitHub instead of a hardcoded list.
Like any good project, this approach works, but now the major problem is that we have to run the script, copy the output, and post it into our chat tool so everyone knows the schedule.
It'd be better if we could update our script to post this message instead. In this example, we're going to use Slack and their incoming webhook, but you could easily tweak this approach to work with other tools like Microsoft Teams or Discord.
For this step, we'll follow the instructions in the docs, ensuring that we're hooking it up to the right channel.
After following the steps, you should see something like the following:
We can test that things are working correctly by running the curl command provided. If the message Hello World appears in the channel, congrats, you've got the incoming webhook created!
With this secret added, we can write a new function, sendMessage, that'll make the POST call to Slack. Since this is a new integration, we'll add a new file, slack.ts to put it in.
// Using axiod for the web connectionimportaxiodfrom"https://deno.land/x/axiod@0.26.2/mod.ts";// function to send a message to the webhookasyncfunctionsendMessage(message:string):Promise<void>{// Get the webhookUrl from our environmentconstwebhookUrl=Deno.env.get("SLACK_WEBHOOK")!;try{// Send the POST requestawaitaxiod.post(webhookUrl,message,{headers:{"Content-Type":"application/json",},});}catch(error){// Error handlingif(error.response){returnPromise.reject(`Failed to post message: ${error.response.status}, ${error.response.statusText}`);}returnPromise.reject("Failed for non status reason "+JSON.stringify(error));}}export{sendMessage};
With sendMessage done, let's update index.ts to use this new functionality.
import{load}from"https://deno.land/std@0.195.0/dotenv/mod.ts";import{GetOrganizationMemberResponse,getMembersOfOrganization,}from"./github.ts";import{sendMessage}from"./slack.ts";import{Pair,createPairsFrom,shuffle}from"./utility.ts";awaitload({export:true});// Replace this with your actual organization nameconstorganizationName="JusticeLeague";constnames=awaitgetMembersOfOrganization(organizationName);constpairs=createPairsFrom(shuffle(names));constmessage=createMessage(pairs);// Slack expects the payload to be an object of text, so we're doing that here for nowawaitsendMessage(JSON.stringify({text:message}));functioncreateMessage(pairs:Pair<GetOrganizationMemberResponse>[]):string{constmapper=(p:Pair<GetOrganizationMemberResponse>):string=>`${p.first.login} meets with ${p.second.login}${p.third?` and ${p.third.login}`:""}`;returnpairs.map(mapper).join("\n");}
And if we were to run the above, we can see the following message get sent to Slack.
Nice! We could leave it here, but we could make the message prettier (having an unordered list and italicizing names), so let's work on that next.
So far, we could leave the messaging as is, however; it's a bit muddled. To help it pop, let's make the following changes.
Italicize the names
Start each pair with a bullet point
Since Slack supports basic Markdown in the messages, we can use the _ for italicizing and - for the bullet points. So let's modify the createMessage function to add this formatting.
functioncreateMessage(pairs:Pair<GetOrganizationMemberResponse>[]):string{// Let's wrap each name with the '_' characterconstformatName=(s:string)=>`_${s}_`;constmapper=(p:Pair<GetOrganizationMemberResponse>):string=>// and start each pair with '-'`- ${formatName(p.first.login)} meets with ${formatName(p.second.login)}${p.third?` and ${formatName(p.third.login)}`:""}`;returnpairs.map(mapper).join("\n");}
By making this small change, we now see the following message:
The messaging is better, but we're still missing some clarity. For example, what date is this for? Or what's the purpose of the message? Looking through these docs, it seems like we could add different text blocks (like titles). So let's see what this could look like.
One design approach is to encapsulate the complex logic for dealing with Slack and only expose a "common-sense" API for consumers. In this regard, I think using a Facade pattern would make sense.
We want to expose the ability to set a title and to set a message through one or more lines of text. Here's what that code would look like
// This class allows a user to set a title and lines and then use the// 'build' method to create the payload to interact with SlackclassMessageFacade{// setting some default valuesprivateheader:string;privatelines:string[];constructor(){this.header="";this.lines=[];}// I like making these types of classes fluent// so that it returns itself.publicsetTitle(title:string):MessageFacade{this.header=title;returnthis;}publicaddLineToMessage(line:string|string[]):MessageFacade{if(Array.isArray(line)){this.lines.push(...line);}else{this.lines.push(line);}returnthis;}// Here's where we take the content that the user provided// and convert it to the JSON shape that Slack expectspublicbuild():string{// create the header block if set, otherwise nullconstheaderBlock=this.header?{type:"header",text:{type:"plain_text",text:this.header,emoji:true},}:null;// convert each line to it's own sectionconstlineBlocks=this.lines.map((line)=>({type:"section",text:{type:"mrkdwn",text:line},}));returnJSON.stringify({// take all blocks that have a value and set it hereblocks:[headerBlock,...lineBlocks].filter(Boolean),});}}
With the facade in place, let's look at implementing this in index.ts
// ... code to get the pairs and formatted lines// using the facade with the fluent syntaxconstmessage=newMessageFacade().setTitle(`☕ Random Coffee ☕`).addLineToMessage(formattedPairs).build();awaitsendMessage(message);
When we run the script now, we get the following message:
In this post, we changed our script from posting its Random Coffee message to the console window to instead posting it into a Slack channel using an Incoming Webhook. By making this change, we were able to remove a manual step (e.g., us copying the message into the channel), and we were able to use some cool emojis and better formatting.
In the final post, we'll take this one step further by automating the script using scheduled jobs via GitHub Actions.
As always, you can find a full working version of this bot on my GitHub.
As a leader, I'm always looking for ways to help my team to be more efficient. To me, an efficient team is self-sufficient, able to find the information needed to solve their problems.
I've found that having up-to-date documentation is critical for a team because it scales out knowledge in asynchronously, removing the need for manual knowledge transfers.
For example, my team has a wiki that contains information for onboarding into our space, how to complete certain processes (requesting time off, resetting a password), how to run our Agile activities, and our support guidebook. At any point, if someone on the team doesn't know how to do something, they can consult the wiki and find the necessary information.
I enjoy up-to-date documentation, but the main problem with them is that they captured the state of the world when they were written, but they don't react to changes. If the process for resetting your password changes, the documentation doesn't auto-update. So unless you're spending time reviewing the docs, they'll grow stale and be worthless, or even worse, mislead others to do the wrong things.
A good mental model for documentation is to think of them as a garden. When planted, it looks great, and everyone enjoys the environment. Over time, weeds will grow, and plants will become overgrown, causing the garden to be less attractive. Eventually, people will stop visiting, and the garden will go into disrepair. To prevent this, we must take care of the garden, removing the weeds and trimming the plants.
Alright, I get it, documentation is important, but my team has commitments, so how do we carve out time to review?
I started my career in healthcare, and one of my first jobs was writing software for a medical diagnostic device. We were ISO 9001 certified, and the device was considered a Class II from the FDA. Long story short, this meant that we have to provide documentation for our device and software and also show that we were keeping things up to date.
To comply, we would find docs that hadn't been updated in a specific time period (like 90 days) and review them. If everything checked out, we'd bump up the review date. Otherwise, we'd make the necessary changes and revalidate the document.
At the time, all of our files were in Word, so it wasn't the easiest to search them (I recall that we had Outlook reminders, but this was many moons ago).
By baking this into our process, this helped make our work more visible, which in turn, gave us a better idea of the team's capacity for that sprint.
Thankfully, we have better technology than Word for sharing information, so how can we take this approach and bring it up to the modern day?
First, I think that having your docs in source control is a great idea. If you're using tools like Git, you already have a way of leaving comments and keeping track of approvals through pull requests.
To make the most of Git, you should keep your changes in plaintext as it's easy to see the differences. and I enjoy using Markdown and tools like Mkdocs make this workflow possible.
With this figured out, our next step is to know when the file was last reviewed. We can do that by adding a new line to the bottom of each file, Last Reviewed On: YYYY/MM/DD. To come up with the initial date, we could use the last time the file was modified (thanks git log!).
At this point, we have a way to see the last time the file was reviewed, next step is to write a script that can find files that haven't been reviewed in the last 90 days. At a high level, we'd do the following:
Get the latest for the doc repository.
Get all the markdown files for the repository.
Get the last line of the file.
If the line doesn't start with Last Reviewed On:, we flag it for review as it's never been reviewed.
If the line has a date, but it's older than 90 days, we flag it for review as it might be stale.
Print all flagged files to the screen.
With the script created, we could manually run this on Mondays. But we're technical, right? Why not create a scheduled task to execute this script instead? This removes a manual task to be ran and it gives us visibility on what docs need reviewed.
When scaling your knowledge out, having great documentation is necessary as it allows your team to self-serve and work in a more asynchronous manner. The main problem with documentation is that it captures the state of the world when the docs were written, but they don't automatically update when the world changes.
Therefore, we need to have some process to flag and review stale docs. To ensure it gets done, we provide visibility by creating work items and committing to them during the sprint.
constnames=["Batman","Superman","Green Lantern","Wonder Woman","Static Shock",// one of my favorite DC heroes!"The Flash","Aquaman","Martian Manhunter",];constpairs=createPairsFrom(shuffle(names));constmessage=createMessage(pairs);console.log(message);functionshuffle<T>(items:T[]):T[]{constresult=[...items];for(leti=result.length-1;i>0;i--){constj=Math.floor(Math.random()*i);[result[i],result[j]]=[result[j],result[i]];}returnresult;}typePair<T>={first:T;second:T;third?:T};functioncreatePairsFrom<T>(items:T[]):Pair<T>[]{if(items.length<2){return[];}constresults=[];for(leti=0;i<=items.length-2;i+=2){constpair:Pair={first:items[i],second:items[i+1]};results.push(pair);}if(items.length%2===1){results[results.length-1].third=items[items.length-1];}returnresults;}functioncreateMessage(pairs:Pair<string>[]):string{constmapper=(p:Pair<string>)=>`${p.first} meets with ${p.second}${p.third?` and ${p.third}`:""}`;returnpairs.map(mapper).join("\n");}
Even though this approach works, the major problem is that every time there's a member change in the Justice League (which seems to happen more often than not), we have to go back and update the list manually.
It'd be better if we could get this list dynamically instead. Given that the League are great developers, they have their own GitHub organization. Let's work on integrating with GitHub's API to get the list of names.
To get the list of names from GitHub, we'll need to do the following.
First, we need to figure out which GitHub endpoint will give us the members of the League. This, in turn, will also tell us what permissions we need for our API scope.
Now that we have a secret, we need to update our script to read from an .env file.
Once we have the secret being read, we can create a function to retrieve the members of the League.
Miscellaneous refactoring of the main script to handle a function returning complex types instead of strings.
Before we start, we should reactor our current file. It works, but we have a mix of utility functions (shuffle and createPairsFrom) combined with presentation functions (createMessage). Let's go ahead and move shuffle and createPairsFrom to their own module.
import{Pair,createPairsFrom,shuffle}from"./module.ts";constnames=["Batman","Superman","Green Lantern","Wonder Woman","Static Shock",// one of my favorite DC heroes!"The Flash","Aquaman","Martian Manhunter",];constpairs=createPairsFrom(shuffle(names));constmessage=createMessage(pairs);console.log(message);functioncreateMessage(pairs:Pair<string>[]):string{constmapper=(p:Pair<string>)=>`${p.first} meets with ${p.second}${p.third?` and ${p.third}`:""}`;returnpairs.map(mapper).join("\n");}
What's the difference between the two? In GitHub parlance, an Organization is an overarching entity that consists of members which, in turn, can be part of multiple teams.
Using the Justice League as an example, it's an organization that contains Batman, and Batman can be part of the Justice League Founding Team and a member of the Batfamily Team.
Since we want to pair everyone up in the Justice League, we'll use the Get members of an Organization approach.
To interact with the endpoint, we'll need to create an API token for GitHub. Looking over the docs, our token needs to have the read:org scope. We can create this token by following the instructions here about creating a Personal Auth Token (PAT).
Once we have the token, we can invoke the endpoint with cURL or Postman to verify that we can communicate with the endpoint correctly.
After we've verified, we'll need a way to get this API token into our script. Given that this is sensitive information, we absolutely should NOT check this into the source code.
Our problem now is that if we check git status, we'll see this file listed as a change. We don't want to check this in, so let's add a .gitignore file.
import{configasloadEnv}from"https://deno.land/x/dotenv@v3.2.2/mod.ts";// other imports// This loads the .env file and adds them to the environment variable listawaitloadEnv({export:true});// Deno.env.get("name") retrieves the value from an environment variable named "name"console.log(Deno.env.get("GITHUB_BEARER_TOKEN"));// remaining code
When we run the script now with deno run, we get an interesting prompt:
Deno requests read access to ".env".
- Requested by `Deno.readFileSync()` API.
- Run again with --allow-read to bypass this prompt
- Allow?
This is one of the coolest parts about Deno; it has a security system that prevents scripts from doing something that you hadn't intended through its Permissions framework.
The permissions can be tuned (e.g., you're only allowed to read from the .env file), or you can give blanket permissions. In our cases, two resources are being used (the ability to read the .env file and the ability to read the GITHUB_BEARER_TOKEN environment variable).
Let's run the command with the allow-read and allow-env flags.
deno run --allow-run --allow-env ./index.ts
If the bearer token gets printed, we've got the .env file created correctly and can proceed to the next step.
// Brining in the axiod libraryimportaxiodfrom"https://deno.land/x/axiod@0.26.2/mod.ts";asyncfunctiongetMembersOfOrganization(orgName:string):Promise<any[]>{consturl=`https://api.github.com/orgs/${orgName}/members`;// Necessary headers are found on the API docsconstheaders={Accept:"application/vnd.github+json",Authorization:`Bearer ${Deno.env.get("GITHUB_BEARER_TOKEN")}`,"X-GitHub-Api-Version":"2022-11-28",};try{constresp=awaitaxiod.get<any[]>(url,{headers:headers,});returnresp.data;}catch(error){// Response was received, but non-2xx status codeif(error.response){returnPromise.reject(`Failed to get members: ${error.response.status}, ${error.response.statusText}`);}else{// Response wasn't receivedreturnPromise.reject("Failed for non status reason "+JSON.stringify(error));}}}
To prove this is working, we can call this function in the index.ts file and verify that we're getting a response.
import{configasloadEnv}from"https://deno.land/x/dotenv@v3.2.2/mod.ts";import{getMembersOfOrganization}from"./github.ts";import{Pair,createPairsFrom,shuffle}from"./utility.ts";awaitloadEnv({export:true});constmembersOfOrganization=awaitgetMembersOfOrganization("JusticeLeague");console.log(JSON.stringify(membersOfOrganization));// rest of the file
At this point, we're making the call, but we're using a pesky any for the response, which works, but it doesn't help us with what properties we have to work with.
Looking at the response schema, it seems the main field we need is login. So let's go ahead and create a type that includes that field.
typeGetOrganizationMemberResponse={login:string;};asyncfunctiongetMembersOfOrganization(orgName:string):Promise<GetOrganizationMemberResponse[]>{//codeconstresp=awaitaxiod.get<GetOrganizationMemberResponse[]>(url,{headers:headers,});// rest of the code}
We can rerun our code and verify that everything is still working, but now with better type safety.
// Need to update the input to be Pair<GetOrganizationMemberResponse>functioncreateMessage(pairs:Pair<GetOrganizationMemberResponse>[]):string{// Need to update mapper function to get the login propertyconstmapper=(p:Pair<string>):string=>`${p.first.login} meets with ${p.second.login}${p.third?` and ${p.third.login}`:""}`;returnpairs.map(mapper).join("\n");}
With this last change, we run the script and verify that we're getting the correct output, huzzah!
Congratulations! We now have a script that is dynamically pulling in heroes from the Justice League organization instead of always needing to see if Green Lantern is somewhere else or if another member of Flash's SpeedForce is here for the moment.
A working version of the code can be found on GitHub.
Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.
In this week's post, we look at how Alan can help their engineer figure out what they want to be when they grow up.
Hey Cameron!
I have a front-end engineer who's sharp, but they're not sure what their career growth looks like. I get the sense that they're interested in other roles outside of software development. How do you navigate this and help them grow?
In a previous post, I mention my strategy of building relationships through one-on-ones. One approach in the post was leveraging a Slack plugin, Random Coffee, to automate scheduling these impromptu conversations.
I wanted to leverage the same idea at my current company; however, we don't use Slack, so I can't just use that bot.
Thinking more about it, the system wouldn't be too complicated as it has three moving parts:
Get list of people
Create random pairs
Post message
To make it even easier, I could hardcode the list of people, and instead of posting the message to our message application, I could print it to the screen.
With these two choices made, I would need to build something that can shuffle a list and create pairs.
Even though we're hardcoding the list of names and printing a message to the screen, I know that the future state is to get the list of names dynamically, most likely through an API call. In addition, most messaging systems support using webhooks to create a message, so that would be the future state as well.
With these restrictions in mind, I know I need to use a language that is good at making HTTP calls. I also want this automation to be something that other people outside of me can maintain, so if I can use a language that we typically use, that makes this more approachable.
In my case, TypeScript fit the bill as we heavily use it in my space, the docs are solid, and it's straightforward to make HTTP calls. I'm also a fan of functional programming, which TypeScript supports nicely.
My major hurdle at this point is that I'd like to execute this single file of TypeScript, and the only way I knew how to do that was by spinning up a Node application and using something like ts-node to execute the file.
Talking to a colleague, they recommended I check out Deno as a possible solution. The more I learned about it, the more I thought this would fit perfectly. It supports TypeScript out of the box (no configuration needed), and a file can be ran with deno run, no other tools needed.
This project is simple enough that if Deno wasn't a good fit, I could always go back to Node.
With this figured out, we're going to create a Deno application using TypeScript as our language of choice.
Once Deno has been installed and configured, you can run the following script and verify everything is working correctly. It creates a new directory called deno-coffee, writes a new file and executes it via deno.
constnames=["Batman","Superman","Green Lantern","Wonder Woman","Static Shock",// one of my favorite DC heroes!"The Flash","Aquaman","Martian Manhunter",];constpairs=createPairsFrom(shuffle(names));constmessage=createMessage(pairs);console.log(message);
This code won't compile as we haven't defined what shuffle, createPairsFrom, or createMessage does, but we can tackle these one at a time.
Since we don't want the same people meeting up every time, we need a way to shuffle the list of names. We could import a library to do this, but what's the fun in that?
In this case, we're going to implement the Fisher-Yates Shuffle (sounds like a dance move).
functionshuffle(items:string[]):string[]{// create a copy so we don't mutate the originalconstresult=[...items];for(leti=result.length-1;i>0;i--){// create an integer between 0 and iconstj=Math.floor(Math.random()*i);// short-hand for swapping two elements around[result[i],result[j]]=[result[j],result[i]];}returnresult;}constwords=["apples","bananas","cantaloupes"];console.log(shuffle(words));// [ "bananas", "cantaloupes", "apples" ]
Excellent, we have a way to shuffle. One refactor we can make is to have shuffle be generic as we don't care what array element types are, as long as we have an array.
But what happens if Martian Manhunter is called away and isn't available? That would leave Aquaman without a pair to have coffee with (sad trombone noise).
In the case that we have an odd number of heroes, the last pair should instead be a triple which would look like the following:
Given that we've been using the word Pair to represent this grouping, we have a domain term we can use. This also means that createPairsFrom has the following type signature.
// Using an optional propertytypePair{first:string,second:string,third?:string}// Using Discriminated UnionstypePair={kind:'double',first:string,second:string}|{kind:'triple',first:string,second:string;third:string}
For now, I'm thinking of going with the optional property and if we need to tweak it later, we can.
functioncreatePairsFrom(names:string[]):Pair[]{// if we don't have at least two names, then there are no pairsif(names.length<2){return[];}constresults=[];for(leti=0;i<=names.length-2;i+=2){constpair:Pair={first:names[i],second:names[i+1]};results.push(pair);}if(names.length%2===1){// we have an odd length// Assign the left-over name to the third of the tripleresults[results.length-1].third=names[names.length-1];}returnresults;}// Example executionconsole.log(createPairsFrom(["apples","bananas","cantaloupes","dates"]));// [{first:"apples", second:"bananas"}, {first:"cantaloupes", second:"dates"}]console.log(createPairsFrom(["ants","birds","cats"]));// [{first:"ants", second:"birds", third:"cats"}]
Similarly to shuffle, we can make this function generic as it doesn't matter what the array element types are, as long as we have an array to work with.
functioncreateMessage(pairs:Pair<string>[]):string{constmapper=(p:Pair<string>)=>`${p.first} meets with ${p.second}${p.third?` and ${p.third}`:""}`;pairs.map(mapper);}
From here, we can join the strings together using the \n (newline) character.
functioncreateMessage(pairs:Pair<string>[]):string{constmapper=(p:Pair<string>)=>`${p.first} meets with ${p.second}${p.third?` and ${p.third}`:""}`;returnpairs.map(mapper).join("\n");}
denoruncoffee.ts
"Superman meets with Wonder WomanBatman meets with The FlashMartian Manhunter meets with AquamanStatic Shock meets with Green Lantern"
From here, we have a working proof of concept of our idea. We could run this manually on Mondays and then post this to our messaging channel (though you might want to switch the names out). If you wanted to be super fancy, you could have this scheduled as a cron job or through Windows Task Scheduler.
The main takeaway is that we've built something we didn't have before and can continue to refine and improve. If no one likes the idea, guess what? We only had a little time invested. If it takes off, then that's great; we can spend more time making it better.
In this post, we built the first version of our Random Coffee script using TypeScript and Deno. We focused on getting our tooling working and building out the business rules for shuffling and creating the pairs.
In the next post, we'll look at making this script smarter by having it retrieve a list of names dynamically from GitHub's API!
As always, you can find a full working version of this bot on my GitHub.
Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.
In this week's post, we look at how Chase can balance writing the perfect code and shipping something.
My question: As a young developer, I notice that sometimes I get paralyzed by options. I want to write the perfect piece of code. This helps me in writing good code but usually at the cost of efficiency. Especially when I am faced with multiple good options. Sometimes I want to KNOW I’m gonna write the right thing before I’m writing it when I my be better off with some trial and error
Are these common problems that you see people face?
What rules of thumb or other pieces of advice do you have to avoid writing nothing instead of something as a result of seeking the ideal?
How important is planning vs trial and error ("failing fast" as they say) to good software development flow?
In a recent post, I spoke about the flaw of using a single metric to tell the story and how Goodhart's Law tells us that once we start measuring a metric, it stops being a useful metric.
Let's look at a real-world example with the popular fast food chain, Five Guys.
Five Guys is known for making good burgers and delivering a mountain of piping hot fries as part of your order. Seriously, an order of small fries is a mountain of spuds. Five Guys make their fries to order, so they're not sitting around under a heat lamp.
This approach works great when ordering in person, but what happens if you order online? The process is essentially the same, the crew works on the burgers, but they won't start the fries until you're at the restaurant, so they're always guaranteeing that you get fresh made fries.
At this point, it's clear that receiving a mountain of hot, cooked-to-order fries is part of the experience and what customers expect, right?
Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.
In this week's post, we look at how test123 can improve the mentoring experience for their new intern.
I recently had an intern join my time and I’m going to be his mentor. I’ve had interns in the past, but this one doesn’t understand any fundamentals and struggles with everything.
My question to you is this, how can I help him? He doesn’t know HTML/CSS/JS, so I’m trying to teach him those, but it’s taking away a lot of time. I suggested for him to watch some videos and then we can sync twice a day to go over the topics and discuss them further.
My issue: I don’t want to just say “go watch videos.” Bc, that’s not the best way to learn - I want him to dive into the code and try things and break, that’s how I learned at least.
How do you think I should handle this? I wanna be a good mentor and I want him to learn and grow. I don’t wanna fail the kid bc I don’t know the proper way to mentor.
When learning about functional programming, you won't go far before you run into the concept of a function. But we're not talking about some syntax or keywords in the language, but from a mathematical sense.
A function is a mapping between two sets such that for every element in the first set, it's mapped to a single element in the second set.
Since a set is a collection of elements, this is similar to a type where values are part of that type. With this understanding, we can modify our definition of a function to be:
A function is a mapping between two types such that for every value in the first type, it's mapped to a single value in the second type.
Before diving into code, let's build up our understanding of functions more. When drawing out the function, we can model it like this.
The first type (A) is a circle where each possible value is listed, using ... to denote a pattern. The arrows map from each value of A to a value in B.
With this example, we know we have a function if the mapping satisfies the following rule:
Every element on the left maps to a single element on the right.
This rule seems easy enough to follow, but let's look at a mapping where this rule doesn't hold.
Let's say that we needed to write some code that could take a given month and return the number of days it has. Given this, here's what the mapping would look like.
To check if we have a function, we need to see if there's any element on the left with two or more arrows coming out.
In this case, February is breaking our rule because it could map to 28 or 29, depending on if it's a leap year. Since there isn't a parameter to denote if it's a leap year, our mapping isn't consistent and can't be a function.
One way to fix this would be to change our type on the left from MonthName to MonthName and year. Making this change gives us this new mapping.
First, we have the pure function. These functions depend wholly on their inputs and they do not interact with outside state. For example, pure functions won't interact with databases, file systems, random generation, or time.
Pure functions are great because they're easy to test, composed with other functions, and don't modify state. Another advantage is that pure functions can be replaced with the result of their call (in other words, you could replace a call to square(3) with its result, 9 and the program is the same). This is known as referential transparency and is a great tool for troubleshooting an application.
The main downside to pure functions is that since they don't talk to other systems (including input/output), it's impossible to write a useful program with just pure functions.
Impure functions, on the other hand, focus on interacting with outside state. These functions will call to the database or other systems, get the time, and generate random data as needed.
They allow us to write useful programs because they interact with input/output, however, the trade off is that they can be harder to test, they don't compose, and since they modify state, you may not be able to run them multiple times and get the same result.
One way to identify an impure function is by looking at its type signatures. For example, a function that takes inputs but returns void has to be modifying state or talking to another system, otherwise, why would you call it? Another signature is a function that takes no inputs, but it can return a value (like readLine() from nodejs), where did it get the value from? Just short of returning a constant value, it had to get it from somewhere.
Building an application requires both pure and impure functions, but how do we leverage the best of both worlds?
When I think about software, I think about how data flows through the system. For example, if someone is using a console application, they're inputting their commands to the terminal, which in turn converts them to commands to run, where the output is displayed on the screen.
As such, an application is made of three kinds of functions.
Boundary functions - These are responsible for getting input/output. They should have zero business rules (or the least possible as they are impure functions.
Business functions - These are the business specific rules that need to be ran on the given inputs. As these are typically the most important of an application, they are designed as pure functions
Workflow functions - This is the combination of boundary components and business components to build something useful. Since they're using impure functions, this will also be impure, but I often use this as my composition root for the application.
// Impure function that allows us to get number from userfunctiongetInput():number{// using prompt-sync https://github.com/heapwolf/prompt-syncconstprompt=require("prompt-sync")({sigint:true});constresponse=prompt("What number to calculate FizzBuzz to?");if(!+response||+response<1){console.log("Invalid response, please enter a positive number greater than 1");returngetInput();}return+response;}// Function that wraps console.log for printingfunctionprintOutput(content:string):void{console.log(content);}
At this point, we have a way of getting a number via getInput and a way to print a string via printOutput. In printOutput, this is a tiny function with no business rules whatsoever. getInput, however, has some business rules about validation, but we'll see later on how to refactor this.
For now, let's leave these two and look into creating our business rule functions.
// Business rules for FizzBuzzfunctioncalculateFizzBuzz(input:number):string{if(input%3==0&&input%5==0){return"FizzBuzz";}if(input%3==0){return"Fizz";}if(input%5==0){return"Buzz";}return`${input}`;}// Helper function to create a range of numbers from [1..end]functioncreateRangeFromOneTo(end:number):number[]{if(number<1){return[];}returnArray.from(Array[number].keys()).map((x)=>x+1);}
With calculateFizzBuzz defined, we could write unit tests to ensure the correctness of the behavior. We could also create a mapping to double-check that we have a function.
Now, let's revisit our getInput function. We've got some business rules that deal with validation (e.g. the input must be a number and greater than 1). Given that this is a light business rule, we could leave it here; however, testing this becomes harder because we don't have a way to ensure that the validation works as expected.
To solve this problem, we could extract the validation logic to its own pure function and update getInput to use the new function.
functionisInputValid(input:string):boolean{if(!+input){returnfalse;}return+input>1;}functiongetInput():number{// using prompt-sync https://github.com/heapwolf/prompt-syncconstprompt=require("prompt-sync")({sigint:true});constresponse=prompt("What number to calculate FizzBuzz to?");if(!isInputValid(response)){console.log("Invalid response, please enter a positive number greater than 1");returngetInput();}return+response;}
Nice! With this in place, we can go ahead and implement our last function, the FizzBuzz workflow.
functionrunFizzBuzzWorkflow():void{// Data coming inconstmaximumNumber=getInput();// Calculating resultsconstresults=createRangeFromOneTo(maximumNumber).map((x)=>calculateFizzBuzz(x));// Print Resultsresults.forEach((x)=>printOutput(x));}// example invocationrunFizzBuzzWorkflow();
This is a straightforward implementation as we get the maximumNumber to calculate, create an array of numbers from 1 to maximumNumber, map each of those to their FizzBuzz representation, and then print them to the screen.
Let's go one step forward. In our example, we assumed that the input and output was coming from the console, but what if we needed to change to read and write to a file?
We could move the boundary functions to be parameters to runFizzBuzzWorkflow, let's take a look at what that would give us.
functionrunFizzBuzzWorkflow(readInput:()=>number,writeOutput:(string)=>void){// Data coming inconstmaximumNumber=readInput();// Calculating resultsconstresults=createRangeFromOneTo(maximumNumber).map((x)=>calculateFizzBuzz(x));// Print Resultsresults.forEach((x)=>writeOutput(x));}// example invocationsrunFizzBuzzWorkflow(getInput,printOutput);// using console read/writerunFizzBuzzWorkflow(()=>42,printOutput);// using hardcoded input with console log
With this change, we can now swap out how we can input or output by creating new functions with the right type signatures. This makes testing workflow components easy because you can stub in your own mocks (no need for mocking frameworks).
If you understand the power of switching out your boundaries, then you also understand other modern architectures like Ports and Adapters as they follow a similar principle.
In this post, we looked at what a function is, how it relates to types, and how to tell if a mapping is a function. From there, we covered the differences between pure and impure functions and how you need both to build any useful application. Finally, we wrapped up by implementing the FizzBuzz problem using this approach.
As a leader, you're responsible for coaching and growing your team, helping them be successful. To do this, you need to set the tone and example of the behaviors you want the team to have.
No matter how good of a team you have or how good of a leader you are, you will have to have a conversation about performance. Whether it's delivery, professional skills, or technology skills, you will have a moment where you need someone to change their behaviors.
Having these types of conversations can be scary, no matter how much experience in leadership you might have. However, when done correctly, these moments can greatly impact the other person, helping them grow tremendously.
On the other hand, poor coaching will do the absolute opposite. The other person can become confused or angry. They could even shut down and disengage altogether, making coaching them that much harder.
So what does good coaching look like? I can't guarantee that these steps will solve all your woes; however, I do guarantee that following these tips will increase the odds of the other person listening and at least consider your feedback. Remember, you want to do this coaching because some behavior has caught your attention, and you want to correct it. If the other person doesn't listen or want to engage, then you literally can't make this happen.
The sooner you can have this conversation, the more effective it will be. Remember, the point of feedback is to let the other person know how they're doing and correct if needed. They can't do this if the behavior happened three weeks ago because there's no correlation at that point.
Imagine if you had a test suite that only told you about failing tests a week after the build started. There's no way you could make the right decisions, so why would we think that's the case for behavior?
If I've noticed a pattern and feel that it's time to coach, I will get that feedback to them that week, if not the next day.
When giving this feedback, the behavior may be obvious to you but not even a thought for the other person. Because we can't control what the other person is thinking, we need to set the context for the feedback so they know what you're talking about.
Let's take my son, for example. He's particular about his food, so when he says that "dinner was awesome," that makes me feel great as I'm happy he enjoyed dinner. But I have no clue what he actually liked or why he thought that. Was it the food? The way it was served? The fact that we had a picnic? No idea, so I'd respond with, "What made it stand out to you?". When he mentions that he liked the pizza, I go "Ah! He enjoyed the food, nice!"
Providing this specific context is crucial for the other person because it lets them kow what caught your attention and drastically reduces the confusion in the conversation. For those who like more concrete details, sharing links to chats, emails, or other artifacts with the behavior can be helpful because you can use it as the foundation for the conversation.
There's a reason that you're having this conversation. There's something that's important to you, and, in your opinion, it wasn't important to the other person. We've got to explain why it's important and why you're commenting on it.
I never want to remove someone's autonomy as I like to set the direction and let the team blaze a path, with me guiding to make sure we don't get lost in the wilderness. However, for someone to have autonomy, they need to understand the goals and the reasoning behind it.
If they don't have this knowledge, then it's that much harder for them to make the right decisions. Ensuring they know the why is a leader's responsibility.
You're working with a team of professionals. A professional makes the right decisions based on their knowledge and experience. If the person is making mistakes, we need to understand why they made the choice that they did.
For example, let's say I'm coaching someone who's consistently missing meetings. I'm frustrated that they're unresponsive and that they don't care. The issue here is that it's okay for me to feel frustrated, but I can't make the judgment that they don't care. I don't know that, and it causes more problems than it solves. I won't vent my feelings to the other person because even though it'd make me feel better, it doesn't help the situation.
A better approach would be to understand why they're missing meetings. Is it something outside of work? Could it be that they don't know why they need to attend? What if they didn't receive an invite? In any of the above cases, there was a solid reason why they didn't attend, and I wouldn't have known that if I had not opened the conversation.
Don't assume malice or apathy when something happens. We are humans first, which means we're going to make mistakes.
The entire point of coaching is to help the person improve, and we also don't want to take away their autonomy. To make this happen, we need to work with the other person to come up with ideas that can help improve the situation. It's not any one person's responsibility, but it's your responsibility to brainstorm with them and help guide them down the correct path.
The key here is to have an open mind and really consider all ideas. One of my favorite leadership books, First, Break All The Rules, talks about how great leaders work with their people to have their strengths shine and to make their weaknesses a non-issue.
In the missing meeting example, I found out that the issue was that they didn't know why they needed to attend the meeting, so they didn't attend, in order to focus on their development work. Working together, I changed invites to include the reason for attending and encouraged them to chat with me so we could figure it out if they didn't know why they needed to be there.
In this example, let's explore where we would need to do some coaching.
While reviewing a pull request from Bruce, you see a comment from Alvin, a member of your team, where they were particularly critical of the work. Reading through the pull request, you see Alvin has left more harsh comments about Bruce's work.
Talking with Bruce, they mention that they don't work well with Alvin as it seems like he's always critical of Bruce.
Based on this scenario, we know that Alvin has left some harsh words for Bruce, which makes them less likely to work together. If Alvin keeps this behavior up with other people, this will impact others wanting to work with him, reducing his effectiveness.
After collecting your thoughts, you reach out to Alvin to see if he's got a few minutes to chat about Bruce's pull request.
"Hey Alvin, I noticed you left some pretty harsh comments that in Bruce's pull request. For example, saying that 'this code is convoluted, rewrite it'. Even if that was the case, it's not clear why you think that. I'm more concerned with how the messaging came across because we work with others to accomplish our tasks, and that communication style can make people not want to work with us.
I don't believe you intend to alienate others, so can you walk me through your thought process here and why you thought this was the right approach?"
In this example, we've already hit four out of the five tips. Our feedback was timely and specific to the problem. We included why it caught our attention and started with an open-ended question for the conversation about the behavior.
In the follow-up conversation, Alvin mentions that he was having a rough day, particularly outside of work, and that he wasn't entirely focused on his tone. Given that this is the first time Alvin has done this, we want to focus on fixing the issue before it becomes a pattern.
"I understand that it can be hard to focus on your tone when you're having a rough time, however, we can't speak to others this way. I don't want this to become a pattern, so what are some things that we could do instead when we're not in the right mental head space for code reviews?"
At this point, we've acknowledged what was said and reaffirmed expectations. Using another open-ended question, we can start brainstorming things that we could do to help improve Alvin's tone. Since we're opening the conversation, Alvin is also giving feedback on what might work for him and what wouldn't work.
Giving critical feedback to someone is not the easiest thing to do, however, it can have the most impact for them. To help frame the conversation, our coaching should: