One of the first things I noticed about working in a functional language versus an object-oriented one is that I can start writing code without worrying about which object should have that code.
I imagine that this is more a result of working with a language with an interpreter (F# Interactive) readily available than with
Step 1: Break the problem down:
1
2
3
4
|
// Given a directory
// 1. Get the files of that directory
// 2. Get the files in subdirectories of the directory
// 3. Print out all the file names
|
Step 2: Start implementing:
1
2
3
4
5
6
7
8
9
10
11
|
//Given a directory
let directory = "C:\Temp"
// 1. Get the files of that directory
open System.IO
let files = Directory.EnumerateFiles(directory)
// 2. Get the files in subdirectories of the directory
// 3. Print out all the file names
Seq.iter (fun f -> ignore(printfn "%s" f)) files
|
Here, all I did was just enough to make sure that I could take a directory, find the files in the directory, and then I went ahead and printed the files out to make sure that I was on the right path. I used the F# Interactive window to make sure I’m on the right track.
What do we know about file systems? That they have a tree structure, and you must travel the directory recursively to find all files in all subfolders. *Nevermind that we can use the SeachOptions argument of EnumerateFiles to do this. That would ruin our fun of learning F#!
Step 3: Set up the building blocks:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
open System.IO
// 1. Get the files of that directory
let getFiles directory =
Directory.EnumerateFiles(directory)
// 2. Get the files in subdirectories of the directory
let files = getFiles "C:\Temp"
// 3. Print out all the file names
let printFiles files =
Seq.iter (fun f -> ignore(printfn "%s" f)) files
printFiles files
|
Here I just created functions for the actions that the code is to perform. At the bottom, you can see that the program is starting to take shape.
Step 4: Do the recursion
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
open System.IO
// 1. Get the files of that directory
// 2. Get the files in subdirectories of the directory
let getFiles directory =
let rec getFilesHelper files dir =
let subDirectories = Directory.EnumerateDirectories(dir)
Seq.append (Directory.EnumerateFiles(dir)) (Seq.collect (fun s -> getFilesHelper files s) subDirectories)
getFilesHelper Seq.empty directory
// 3. Print out all the file names
let printFiles files =
Seq.iter (fun f -> ignore(printfn "%s" f)) files
let files = getFiles "C:\Temp"
printFiles files
|
This is quite brute force. There are two things to notice at this stage:
- The way the recursion works: I first get the subdirectories, and then use
Seq.collect
to call getFilesHelper
for each subdirectory. The result of getFilesHelper
is a sequence of file names, so each folder has the files appended into one large sequence.
- The use of a “helper” function: The
getFilesHelper
function takes an argument that collects the results of each of the recursive calls. By creating this function as a helper inside the main getFiles
function, we can spare the client those details; and avoid the smell of an output parameter.
Step 5: Clean up
1
2
3
4
5
6
7
8
9
10
11
12
13
|
let getFiles directory =
let rec getFilesHelper files dir =
Directory.EnumerateDirectories(dir)
|> Seq.collect (fun s -> getFilesHelper files s)
|> Seq.append (Directory.EnumerateFiles(dir))
getFilesHelper Seq.empty directory
let printFiles files =
Seq.iter (fun f -> ignore(printfn "%s" f)) files
let files = getFiles "C:\Temp"
printFiles files
|
Pipelining to the rescue! This is much more readable! The getFilesHelper
function now actually reads just like what it does: Enumerate the directories, collect the files in each directory, and append the results. This is one of my top 3 favorite things about F#.
Step 6: Make it ~generic~ functional
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
open System.IO
let actOnFiles directory fileOp =
let rec getFilesHelper files dir =
Directory.EnumerateDirectories(dir)
|> Seq.collect (fun s -> getFilesHelper files s)
|> Seq.append (Directory.EnumerateFiles(dir))
getFilesHelper Seq.empty directory |> fileOp
let print files =
Seq.iter (fun f -> ignore(printfn "%s" f)) files
actOnFiles "C:\Temp" print
|
Here’s the beauty of functional languages! We created a higher-order function, print, and passed that into our getFiles function (which we renamed to actOnFiles). Now, we have setup a function that takes a directory and a function to apply to each file in that directory and its subdirectories. Here are two more higher-order functions to demonstrate the point:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
open System.IO
let actOnFiles directory fileOp =
let rec getFilesHelper files dir =
Directory.EnumerateDirectories(dir)
|> Seq.collect (fun s -> getFilesHelper files s)
|> Seq.append (Directory.EnumerateFiles(dir))
getFilesHelper Seq.empty directory |> fileOp
let print files =
Seq.iter (fun f -> ignore(printfn "%s" f)) files
let getSize files =
Seq.map (fun f -> (new FileInfo(f)).Length) files |> Seq.sum
let delete files =
Seq.iter (fun f -> File.Delete(f))
actOnFiles "C:\Temp" print
actOnFiles "C:\Temp" getSize
// actOnFiles "C:\Temp" delete //DO AT YOUR OWN RISK!!!
|