Fixing Haskellings Filepaths
Hey folks! I'm experimenting with a new content format for Monday Morning Haskell. Every Monday Evening now, I'm going to stream myself writing some Haskell or working on a Haskell problem, and then the following Monday I'll post an overview of that stream on YouTube.
Last week was the first streaming session, where I was working on an issue with Haskellings. So this video will have some highlights from that. For broader context, I was looking to replace some custom functions I had built for filepath manipulation with the more well tested System.Filepath library.
This being the first stream, I hope you'll understand things are still a bit rough around the edges, but I hope you enjoy it! If you want to tune in to watch me on Monday Evenings, head over to my Twitch page!
Monday Evening Haskell!
We have an exciting announcement this week! Tonight, I'll be trying out a new form of content. I'll be streaming myself writing some Haskell. This will likely be a weekly event for quite a while. To see some Haskell in action, head to our Twitch Stream page from 7:30 PM until 9:30 PM Pacific Daylight Time (UTC-7).
Tonight's focus will be on updating Haskellings to use a library for file paths instead of its current custom system. The next few weeks will probably also be centered around Haskellings, but I'll also venture into some areas, like trying out some example coding problems with Haskell. I'll also change around the streaming time to give a chance to followers from around the world.
So don't miss out, head to our Twitch page, follow us, and tune in tonight at 7:30!
Summer Sale Ending!
Today is the last day of the Monday Morning Haskell summer sale! If you subscribe today, you'll get a discount code to use on all of our courses! This includes our new Making Sense of Monads course. If you're relatively new to Haskell, this is a great way to learn about this tricky topic that's a stumbling block for many newcomers. It's a short, one-module course covering these topics:
- Starting out with simpler functional structures (e.g. Functors)
- The syntactic elements involved in writing monadic functions
- The most common monads and how to combine them
- Bonus challenges to test your knowledge
You can get a closer overview of the content on the course page here. You can also look at our full course listings here. And if you subscribe today (July 26th) you'll get a discount code for all these courses! So don't wait!
Hidden Identity: Using the Identity Monad
Last week we announced our new Making Sense of Monads course. If you subscribe to our mailing list in the next week, you can get a special discount for this and our other courses! So don't miss out!
But in the meantime, we've got one more article on monads! Last week, we looked at the "Function monad". This week, we're going to explore another monad that you might not think about as much. But even if we don't specifically invoke it, this monad is actually present quite often, just in a hidden way! Once again, you can watch the video to learn more about this, or just read along below!
On its face, the identity monad is very simple. It just seems to wrap a value, and we can retrieve this value by calling runIdentity
:
newtype Identity a = Identity { runIdentity :: a }
So we can easily wrap any value in the Identity monad just by calling the Identity
constructor, and we can unwrap it by calling runIdentity
.
We can write a very basic instance of the monad typeclass for this type, that just incorporates wrapping and unwrapping the value:
instance Monad Identity where
return = Identity
(Identity a) >>= f = f a
A Base Monad
So what's the point or use of this? Well first of all, let's consider a lot of common monads. We might think of Reader
, Writer
and State
. These all have transformer variations like ReaderT
, WriterT
, and StateT
. But actually, it's the "vanilla" versions of these functions that are the variations!
If we consider the Reader
monad, this is actually a type synonym for a transformer over the Identity monad!
type Reader a = ReaderT Identity a
In this way, we don't need multiple abstractions to deal with "vanilla" monads and their transformers. The vanilla versions are the same as the transformers. The runReader
function can actually be written in terms of runReaderT
and runIdentity
:
runReader :: Reader r a -> r -> a
runReader action = runIdentity . (runReaderT action)
Using Identity
Now, there aren't that many reasons to use Identity
explicitly, since the monad encapsulates no computational strategy. But here's one idea. Suppose that you've written a transformation function that takes a monadic action and runs some transformations on the inner value:
transformInt :: (Monad m) => m Int -> m (Double, Int)
transformInt action = do
asDouble <- fromIntegral <$> action
tripled <- (3 *) <$> action
return (asDouble, tripled)
You would get an error if you tried to apply this to a normal unwrapped value. But by wrapping in Identity
, we can reuse this function!
>> transformInt 5
Error!
>> transformInt (Identity 5)
Identity (5.0, 15)
We can imagine the same thing with a function constraint using Functor
or Applicative
. Remember that Identity
belongs to these classes as well, since it is a Monad
!
Of course, it would be possible in this case to write a normal function that would accomplish the simple task in this example. But no matter how complex the task, we could write a version relying on the Identity
monad that will always work!
transformInt' :: Int -> (Double, Int)
transformInt' = runIdentity . transformToInt . Identity
...
>> transformInt' 5
(5.0, 15)
The Identity
monad is just a bit of trivia regarding monads. If you've been dying to learn how to really use monads in your own programming, you should sign up for our new course Making Sense of Monads! For the next week you can subscribe to our mailing list and get a discount on this course as well as our other courses!
Making Sense of Monads!
We have a special announcement this week! We have a new course available at Monday Morning Haskell Academy! The course is called Making Sense of Monads, and as you might expect, it tackles the concept of monads! It's a short, one module course, but it goes into a good amount of detail about this vital topic, and includes a couple challenge projects at the end. Sign up here!. If you subscribe to our mailing list, you can get a special discount on this and our other courses!
In addition to this, we've also got some new blog content! Once again, there's a video, but you can also follow along by scrolling down!
Last week we discussed the function application operator, which I used for a long time as a syntactic crutch without really understanding it. This week we'll take another look at a function-related concept, but we'll relate it to our new monads course. We're going to explore the "function monad". That is, a single-argument function can act as a monad and call other functions which take the same input in a monadic fashion. Let's see how this works!
The Structure of Do Syntax
Let's start by considering a function in a more familiar monad like IO. Here's a function that queries the user for their name and writes it to a file.
ioFunc :: IO ()
ioFunc = do
putStrLn "Please enter your name"
name <- getLine
handle <- openFile "name.txt" WriteMode
hPutStrLn handle name
hClose handle
Do syntax has a discernable structure. We can see this when we add all the type signatures in:
ioFunc :: IO String
ioFunc = do
putStrLn "Please enter your name" :: IO ()
(name :: String) <- getLine :: IO String
(handle :: Handle) <- openFile "name.txt" WriteMode :: IO Handle
hPutStrLn handle name :: IO ()
hClose handle :: IO ()
return name :: IO String
Certain lines have no result (returning ()
), so they are just IO ()
expressions. Other lines "get" values using <-
. For these lines, the right side is an expression IO a
and the left side is the unwrapped result, of type a
. And then the final line is monadic and must match the type of the complete expression (IO String
in this case) without unwrapping it's result.
Here's how we might expression that pattern in more general terms, with a generic monad m
:
combine :: a -> b -> Result
monadFunc :: m Result
monadFunc = do
(result1 :: a) <- exp1 :: m a
(result2 :: b) <- exp2 :: m b
exp3 :: m ()
return (combine result1 result2) :: m Result
Using a Function
It turns out there is also a monad instance for (->) r
, which is to say, a function taking some type r
. To make this more concrete, let's suppose the r
type is Int
. Let's rewrite that generic expression, but instead of expressions like m Result
, we'll instead have Int -> Result
.
monadFunc :: Int -> Result
monadFunc = do
(result1 :: a) <- exp1 :: Int -> a
(result2 :: b) <- exp2 :: Int -> b
exp3 :: Int -> ()
return (combine result1 result2) :: Int -> Result
So on the right, we see an expression "in the monad", like Int -> a
. Then on the left is the "unwrapped" expression, of type a
! Let's make this even more concrete! We'll remove exp3
since the function monad can't have any side effects, so a function returning ()
can't do anything the way IO ()
can.
monadFunc :: Int -> Int
monadFunc = do
result1 <- (+) 5
result2 <- (+) 11
return (result1 * result2)
And we can run this function like we could run any other Int -> Int
function! We don't need a run
function like some other functions (Reader, State, etc.).
>> monadFunc 5
160
>> monadFunc 10
315
Each line of the function uses the same input argument for its own input!
Now what does return
mean in this monadic context? Well the final expression we have there is a constant expression. It must be a function to fit within the monad, but it doesn't care about the second input to the function. Well this is the exact definition of the const
expression!
const :: a -> b -> a
const a _ = a -- Ignore second input!
So we could replace return
with const
and it would still work!
monadFunc :: Int -> Int
monadFunc = do
result1 <- (+) 5
result2 <- (+) 11
const (result1 * result2)
Now we could also use the implicit input for the last line! Here's an example where we don't use return:
monadFunc :: Int -> Int
monadFunc = do
result1 <- (+) 5
result2 <- (+) 11
(+) (result1 * result2)
...
>> monadFunc 5
165
>> monadFunc 10
325
And of course, we could define multiple functions in this monad and call them from one another:
monadFunc2 :: Int -> String
monadFunc2 = do
result <- monadFunc
showInput <- show
const (show result ++ " " ++ showInput)
Like a Reader?
So let's think about this monad more abstractly. This monadic unit gives us access to a single read-only input for each computation. Does this sound familiar to you? This is actually exactly like the Reader
monad! And, in fact, there's an instance of the MonadReader
typeclass for the function monad!
instance MonadReader r ((->) r) where
...
So without changing anything, we can actually call Reader
functions like local
! Let's rewrite our function from above, except double the input for the call to monadFunc
:
monadFunc2 :: Int -> String
monadFunc2 = do
result <- local (*2) monadFunc
showInput <- show
const (show result ++ " " ++ showInput)
...
>> func2 5
"325 5"
>> func2 10
"795 10"
This isomorphism is one reason why you might not use the function monad explicitly so much. The Reader monad is a bit more canonical and natural. But, it's still useful to have this connection in mind, because it might be useful if you have a lot of different functions that take the same input!
If you're not super familiar with monads yet, hopefully this piqued your interest! To learn more, you can sign up for Making Sense of Monads! And if you subscribe to Monday Morning Haskell you can get a special discount, so don't wait!
Function Application: Using the Dollar Sign ($)
Things have been a little quiet here on the blog lately. We've got a lot of different projects going on behind the scenes, and we'll be making some big announcements about those soon! If you want to stay up to date with the latest news, make sure you subscribe to our mailing list! You'll get access to our subscriber-only resources, including our Beginners Checklist and our Production Checklist!
The next few posts will include a video as well as a written version! So you can click the play button below, or scroll down further to read along!
Let's talk about one of the unsung heroes, a true foot soldier of the Haskell language: the function application operator, ($)
. I used this operator a lot for a long time without really understanding it. Let's look at its type signature and implementation:
infixr 0
($) :: (a -> b) -> a -> b
f $ a = f a
For the longest time, I treated this operator as though it were a syntactic trick, and didn't even really think about it having a type signature. And when we look at this signature, it seems really basic. Quite simply, it takes a function, and an input to that function, and applies the function.
At first glance, this seems totally unnecessary! Why not just do "normal" function application by placing the argument next to the function?
add5 :: Int -> Int
add5 = (+) 5
-- Function application operator
eleven = add5 $ 6
-- Same result as "Normal" function application
eleven' = add5 6
This operator doesn't let us write any function we couldn't write without it. But it does offer us some opportunities to organize our code a bit differently. And in some cases this is cleaner and it is more clear what is going semantically.
Grouping with $
Because its precedence is so low (level 0) this operator can let us do some kind of rudimentary grouping. This example doesn't compile, because Haskell tries to treat add5
as the second input to (+)
, rather than grouping it with 6, which appears to be its argument.
-- Doesn't compile!
value = (+) 11 add5 6
We can group these together using parentheses. But the low precedence of ($)
also allows it to act as a "separator". We break our expression into two groups. First we add and apply the first argument, and then we apply this function with the result of add5 6
.
-- These work by grouping in different ways!
value = (+) 11 $ add5 6
value' = (+) 11 (add5 6)
Other operators and function applications bind more tightly, so can have expressions like this:
value = (+) 11 $ 6 + 7 * 11 - 4
A line with one $
essentially says "get the result of everything to the right and apply it as one final argument". So we calculate the result on the right (79) and then perform (+) 11
with that result.
Reordering Operations
The order of application also reverses with the function application operator as compared to normal function application. Let's consider this basic multiplication:
value = (*) 23 15
Normal function application orders the precedence from left-to-right. So we apply the argument 23 to the function (*)
, and then apply the argument 15 to the resulting function.
However, we'll get an error if we use $
in between the elements of this expression!
-- Fails!
value = (*) $ 23 $ 15
This is because ($)
orders from right-to-left. So it first tries to treat "23" as a function and apply "15" as its argument.
If you have a chain of $
operations, the furthest right expression should be a single value. Then each grouping to the left should be a function taking a single argument. Here's how we might make an example with three sections.
value = (*) 23 $ (+10) $ 2 + 3
Higher Order Functions
Having an operator for function application also makes it convenient to use it with higher order functions. Let's suppose we're zipping together a list of functions with a list of arguments.
functions = [(+3), (*5), (+2)]
arguments = [2, 5, 7]
The zipWith
function is helpful here, but this first approach is a little clunky with a lambda:
results = zipWith (\f a -> f a) functions arguments
But of course, we can just replace that with the function application operator!
results = zipWith ($) functions arguments
results = [5, 25, 9]
So hopefully we know a bit more about the "dollar sign" now, and can use it more intelligently! Remember to subscribe to Monday Morning Haskell! There will be special offers for subscribers in the next few weeks, so you don't want to miss out!
Haskellings Beta!
We spent a few months last year building the groundwork for Haskellings in this YouTube series. Now after some more hard work, we're happy to announce that Haskellings is now available in the beta stage. This program is meant to be an interactive tutorial for learning the Haskell language. If you've never written a line of Haskell in your life, this program is designed to help you take those first steps! You can take a look at the Github repository to learn all the details of using it, but here's a quick overview.
Overview
Haskellings gives you the chance to write Haskell code starting from the very basics with a quick evaluation loop. It currently has 50 different exercises for you to work with. In each exercise, you can read about a Haskell concept, make appropriate modifications, and then run the code with a single command to check your work!
You can do exercises individually, but the easiest way to do everything in order is to run the program in Watcher mode. In this mode, it will automatically tell you which exercise is next. It will also re-run each exercise every time you save your work.
Haskellings covers a decent amount of ground on basic language concepts. It starts with the very basics of expressions, types and functions, and goes through the basic usage of monads.
Haskellings is an open source project! If you want to report issues or contribute, learn how by reading this document! So go ahead, give it a try!
Don't Forget: Haskell From Scratch
Haskellings is a quick and easy way to learn the language basics, but it only touches on the surface of a lot of elements. To get a more in-depth look at the language, you should consider our Haskell From Scratch video course. This course includes:
- Hours of video lectures explaining core language concepts and syntax
- Dozens of practice problems to help you hone your skills
- Access to our Slack channel, so you can get help and have your questions answered
- A final mini-project, to help you put the pieces together
This course will help you build a rock-solid foundation for your future Haskell learnings. And even better, we've now cut the price in half! So don't miss out!
Advanced Series Updated!
We're back again with some more site improvements, this time to our Advanced Content. All of these series now have improved syntax highlighting and code blocks for better readability. In addition, we've revised three of them with updated companion code! Here's a summary.
Real World Haskell Series
Once you've mastered the foundations of the language, this series should be your first stop! It will walk you through several different libraries demonstrating how you can perform some real-world tasks with Haskell, like connecting to a database and running a web server. You can follow along with all the code here on GitHub.
Parsing Series
As a functional language, Haskell thrives on being able to seemlessly compose smaller pieces of code together into a large, coherent whole. Parsing libraries are one area where this paradigm fits very well. In this series, we go through a few different parsing libraries and compare them. The code is available in this repository if you want to try it out yourself!
API Integrations Series
A lot of coding projects involved connected with outside services and APIs. Luckily, Haskell has a few libraries for interacting with these services! In this series, we'll explore integrations with Twilio and Mailgun so that we can send text messages and emails to your users! You can get a detailed breakdown of the code on GitHub. You can even fork the repository and run the code for yourself!
What's Coming Up?
Our next area of focus will be working on a first release of Haskellings, an interactive beginner tutorial for the language. We built this over the course of the last few months of 2020 in an extended video series that you can catch here on YouTube. The project is Open Source and currently available for contributions! Stay tuned for more updates on it!
Beginners Series Updated!
Where has Monday Morning Haskell been? Well, to ring in 2021, we've been making some big improvements to the permanent content on the site. So far we've focused on the Beginners section of the site. All the series here are updated with improved code blocks and syntax highlighting. In addition, we've fully revised most of them and added companion Github repositories so you can follow along!
Liftoff
Our Liftoff Series is our first stop for Haskell beginners. If you've never written a line of Haskell in your life but want to learn, this is the place to start! You can follow along with all the code in the series by using this Github repository.
Monads Series
Monads are a big "barrier" topic in Haskell. They don't really exist much in most other languages, but they're super important in Haskell. Our Monads Series breaks them down, starting with simpler functional structures so you can understand more easily! The code for this series can be found on Github here.
Testing Basics
You can't do serious production development in any language until you've mastered the basics of unit testing. Our Testing Series will school you on the basics of writing and running your first unit tests in Haskell. It'll also teach you about profiling your code so you can see improvements in its runtime! And you can follow along with the code right here on Github!
Haskell Data Basics
Haskell's data types are one of the first things that made me enjoy Haskell more than other languages. In this series we explore the ins and outs of Haskell's data declaration syntax and related topics like typeclasses. We compare it side-by-side with other languages and see how much easier it is to express certain concepts! Take a look at the code here!
What's Next?
Next up we'll be going through the same process for some of our more advanced series. So in the next couple weeks you can look forward to improvements there! Stay tuned!
Countdown to 2021!
At last. 2020 is nearly over. It's been a tumultuous year for the entire world, and I think most of us are glad to be turning over a new page, even if the future is still uncertain. As I always do, I'll sign off the year with a review of the different concepts we've looked at this year, and give a preview of what to expect in 2021.
2020 In Review
There were three major themes we covered this year. For much of the start of this year, we focused on AI. The main product of this was our work on a Haskell version of Open AI Gym. We explored ways to generalize the idea of an AI agent, including cool integrations of Haskell ideas like type families. We even wrote this in such a way that we could incorporate Tensor Flow! You can read about that work in our Open AI Series.
Over the summer, we switched gears a bit and focused on Rust. In our Rust Web Series we solved some more interesting problems to parallel our Real World Haskell Series. This included building a simple web server and connecting to a database.
Then our final work area was on our Haskellings program. Modeled after Rustlings, this is intended to be an automated beginner tutorial for the Haskell language. For the first time, I changed up the content a bit and did a video series, rather than a written blog series. So you can find the videos for that series on our YouTube Channel!
We're looking for people to contribute exercises (and possibly other code) to the Haskellings project, so definitely visit the repository if you'd like to help!
Looking Forward
There will be some big changes to the blog in 2021. Here are some of the highlights I'm looking forward to:
Spending more time on how Haskell fits into the broader programming ecosystem and what role it can play for those new to the industry. What can beginning programmers learn from the Haskell language and toolchain? What lessons of Haskell are applicable across many different languages? More exploration of different content types and media. As mentioned above, I spent the last part of 2020 experimenting with video blogs. I expect to do more of this type of experimenting this year. Upgrading the site's appearance and organization. Things have been pretty stagnant for a while, and there are a lot of improvements I'd like to make. For one example, I'd like to make coding sections more clear and interactive in blog posts. New, lighter-weight course material. Right now, our course page has 2 rather large courses. This year I'm going to look at breaking the material in these out into smaller, more manageable chunks, as well as adding a couple totally new course offerings at this smaller size.
I've set a lot of these goals before and fallen short. Unfortunately, I've found that these priorities often get pushed aside due to my desire to publish new content weekly, as I've been doing for over 4 years now (how time flies!). But starting in 2021, I'm going to focus on quality over quantity. I do not plan on publishing every week, and a lot of the blogs I do publish will highlight improvements to old content, rather than being new, detailed technical tutorials. I hope these changes will take the most important content on the blog and make it much more useful to the intended audiences.
I also have a tendency of creating projects to demonstrate concepts, but leave the projects behind once I am done writing about those concepts. This year, I hope to take a couple of my projects, specifically Open AI Gym and the Haskellings Beginner Tutorial and turn them into polished products that other developers will want to use. This will take a lot of focused time and effort, but I think it will be worth it.
So even though you might not see a new post every Monday, never fear! Monday Morning Haskell is here to stay! I hope all of you have a happy and safe new year!
Open Sourcing Haskellings!
In the last couple months we've been working on "Haskellings", an automated Haskell tutorial inspired by Rustlings. This week, I'm happy to announce that this project is now open source! You can find the (very early) version here on Github. I'll be working on making the project more complete throughout 2021, but I would really value any contributions the community has to this project! In this article, I'll list a few specific areas that would be good to work on!
More Exercises
The first and most important thing is that we need more exercises! I've done a couple simple examples to get started, but I'd like to crowd-source the creation of exercises. You can use the set of Rustlings exercises as some sort of inspiration. The most important topics to start out with would be things that explain the Haskell type system, so the different sorts of types, expressions and functions, as well as making our own data types. Other good concepts include things like syntax elements (think "where" and "case") and type classes.
Operating System Compatibility
I've definitely cut a few corners when it comes to the MVP of this project. I've only been working on Linux, so it's quite possible that there are some Linux-specific assumptions in the file-system level code. There will need to be some testing of the application on Windows and Mac platforms, and some adjustments will likely be necessary.
GHC Precision
Another area that will need some attention is the configuration section. Is there a cleaner way to determine where the GHC executable lives? What about finding the package database that corresponds to our Stack snapshot? My knowledge of Stack and package systems is limited, so it's very likely that there are some edge cases where the logic doesn't work out.
Exercise Cleanup
Right now, we list all the exercises explicitly in the ExerciseList module. But they're listed in two different places in the file. It would be good to clean this up, and potentially even add a feature for automated detection of exercise features. For example, we can figure out the filename, the directory, and whether or not it's runnable just by examining the file at its path! Right now the only thing that would need to be specified in "code" would be the order of exercises and their hints.
Contributing
If you're interested in contributing to this project, you can fork the repository, put up a pull request, and email me at james@mondaymorninghaskell.me! I'll be working on this periodically throughout 2021, hoping to have a more complete version to publish by the end.
Dependencies and Package Databases
Here's the final video in our Haskellings series! We'll figure out how to add dependencies to our exercises. This is a bit trickier than it looks, since we're running GHC outside of the Stack context. But with a little intuition, we can find where Stack is storing its package database and use that when running the exercise!
Next week, we'll do a quick summary of our work on this program, and see what the future holds for it!
Adding Hints
If a user is struggling with a particular exercise, we don't want them to get stuck! In this week's video, we'll see how to add "hints" to the Watcher. This way, a user can get a little extra help when they need it! We can also use this functionality to add more features in the future, like skipping an exercise, or even asking the user questions as part of the exercise!
Executing Executables!
In Haskell, we'd like to think "If it compiles, it works!" But of course this isn't generally the case. So in addition to testing whether or not our exercises compile, we'll also want to run the code the user wrote and see if it works properly! In this week's video, we'll see how we can distinguish between these two different exercise types!
Testing the Watcher
In our Haskellings program, the Watcher is a complex piece of functionality. It has to track changes to particular files, re-compile them on the spot, and keep track of the order in which these exercises should be done. So naturally, writing unit tests for it presents challenges as well! In this video, we'll explore how to simulate file modifications in the middle of a unit test.
Unit Testing Compilation
We've used Hspec before on the blog. But now we'll apply it to our Haskellings program, in conjunction with the program configuration changes we made last week! We'll write a couple simple unit tests that will test the basic compilation behavior of our program. Check out the video to see how!
Using the Handle Abstraction
Our haskellings program is starting to get a bit more complicated, so it would be nice to write some unit tests for it. But this is very difficult to do for a command line application! To set this up, we'll start by refactoring our program to use the Handle abstraction. This allows the program to work regardless of whether it's using the command line or file for its input. So the user will see the command line version, but our tests will be able to use files! We'll also use the process of Compile Driven Development to systematically refactor our program to use these new configuration elements!
Sequencing Exercises in the Watcher
Our watcher program can keep tabs on our exercise files, but it doesn't have any notion of an exercise order. In this week's video, we'll fix this! Our watcher will focus on one specific "current" exercise. When we're done with that, it will move us on to the next one automatically! This video involves a good example of using recursion in our Haskell workflow. But even better, we'll see a rudimentary usage of an MVar to pass information between the threads of our program. Take a look!
Overriding Process Handlings and Terminal Colors
Right now, our Haskellings program doesn't give the user any information or output aside from the actual compilation stream. For this week's video blog, we'll dig in a bit deeper to the process to give the user better information. We'll use the process's exit information and override its handles to control which output the user sees and when. Then as a final flourish, we'll color the user's terminal output result to be more helpful!
Watching Files with FS-Notify!
This week we continue laying out the groundwork for our "Haskellings" project. A key part of the this program is the automated aspect of "watch" mode. A user should be able to modify their file and then immediately see the results of their changes in the watcher window. This week, we get familiar with the fsnotify package, which lets us watch files and directories and take actions in our program when they change!