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:

  1. 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.
  2. 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!!!