Designing With Lambdas - Part III
In the previous two installments of the series we discussed how we can use lambdas to encapsulate more processing logic and to create contextual APIs. In this post I will show another example of the latter.
Navigating directory structures
This time around we will try to create a safe way to browse and create directories without losing track of the current location. When we write code to read files or directory information from a directory structure, and we need to look into more than one directory, we have to be careful to always be sure of what the current working directory is or use only absolute paths. Neither approach is without its inconveniences.
What I'll try to show here is a way to enforce the working directory as a lambda context. That can be a mouthful so let me try to put it in simpler terms. I'd like to have a way to assert that when a particular code runs, the working directory will be changed to a specified location, and after that code finishes the working directory is restored automatically.
Without lambdas, the context setting could resemble the following:
string previousDir = Environment.CurrentDirectory; try { Environment.CurrentDirectory = @"c:\myapp"; DoStuff(); try { Environment.CurrentDirectory = @"c:\myapp\subdirs\resources\images\toolbar"; DoOtherStuff(); } finally { Environment.CurrentDirectory = @"c:\myapp"; } DoMoreStuff(); } finally { Environment.CurrentDirectory = previousDir; }
All those try
and finally
are there to make sure we restore the appropriate working directory after we are done with each one. I don't know about you, but having a lot of those in my code would add a lot of noise, obscuring the real intent of the code.
What if we could, once again, encapsulate this pattern in a function that accepts a delegate? Here's what a way more revealing code would look like.
Dir.Change(@"c:\myapp", path => { //we're in c:\myapp Dir.Change(subdir => { //we're in c:\myapp\subdir string dirName = DateTime.Now.ToString("yyyy-MM-dd"); //we can ask it to create the directory if // not found (the "true" parameter) Dir.Change(dirName, true, dailyLogDir => { //we're in c:\myapp\subdir\2008-04-29 (for example) using(StreamWriter wr = File.CreateText("logfile.txt")) { wr.WriteLine("Hello file."); } }); //we're back in c:\myapp\subdir //we can pass in a deeper path Dir.Change(@"resources\images\toolbar", imagesDir => { //we're in c:\myapp\subdir\resources\images\toolbar //listing all files here foreach(string file in imagesDir.GetFiles("*.png")) Console.WriteLine("Toolbar icon: " + file); }); }); });
Within each Dir.Change
block we can be certain that the current working directory will be the one we specified (unless your own code intentionally changes it.) When the code block finishes, we will be back to whatever directory we were before the block, guaranteed.
Note in line 4 that the name of the directory can be gathered from the parameter's name subdir
. This is not always possible because we could be dealing with multi-level directory changes (line 22) or dynamically generated directory names (line 10). Additionally, the rules for naming directories are not the same as for naming C# identifiers. For these reasons, it's also possible to pass a string containing the name of the desired directory.
Here's the code that makes this possible.
public static class Dir { public static void Change(Action<DirectoryInfo> execute) { Change(false, execute); } public static void Change(string path, Action<DirectoryInfo> execute) { Change(path, false, execute); } public static void Change(bool createIfNeeded, Action<DirectoryInfo> execute) { string path = execute.Method.GetParameters()[0].Name; Change(path, createIfNeeded, execute); } public static void Change(string path, bool createIfNeeded, Action<DirectoryInfo> execute) { string previousDir = Environment.CurrentDirectory; try { if(createIfNeeded && !Directory.Exists(path)) Directory.CreateDirectory(path); var di = new DirectoryInfo(path); Environment.CurrentDirectory = path; execute(di); } finally { Environment.CurrentDirectory = previousDir; } } public static bool TryChange(string path, Action<DirectoryInfo> execute) { if(Directory.Exists(path)) { Change(path, false, execute); return true; } else { return false; } } }
So there you have it. Another example of incorporating lambdas to design APIs that attempt to reduce code noise and eventually read more naturally. Hopefully you're starting to see a pattern here in terms of what a context can be and how lambda-aware APIs can help formalizing that context.