I think for a lot of amateurs, their alignment is always out.
—Karrie Webb, Professional Golfer
Where Are My Braces?
Newcomers to F# are often disorientated by how different everything seems. Indentation is semantically significant – most code isn’t enclosed in curly brackets. There’s an increased emphasis on functions “floating free” without being in classes. And there are strange-seeming practices such as currying and partial application. These factors combine to undermine the comfortable naming and layout habits we might rely on in, say, C#. All this means that it can be hard to be sure that one is coding in a team-friendly, maintainable style. In this chapter, I’ll demonstrate some practices and conventions that should help you get over this feeling.
Incidentally, if you want to automate your layout, you might want to consider using Fantomas (https://github.com/fsprojects/fantomas). Fantomas automates layout and can even fix issues such as the use of the old-fashioned in keyword (verbose syntax). It’s automatically used by JetBrains Rider and can be installed as a plug-in/extension in Visual Studio Code and Visual Studio.
There is also a very comprehensive guide to layout and naming within the F# Style Guide (https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/), which I’d urge you to read as soon as you’ve come to grips with the basics of F# syntax. Rather than reiterate the Style Guide’s recommendations in this chapter, I’m going to take a case-study approach. We’ll start with some code that embodies some… let’s say “infelicities” I commonly see being perpetrated in F# code. We’ll progressively tidy and refactor the example until it is code to be proud of. Please don’t treat my suggestions as rules (I have a personal horror of “coding standards”), but as useful suggestions born of experience. It’s more important that you finish this chapter wanting to organize your code well, than it is to memorize this or that convention.
It’s Okay Pluto, I’m Not a Planet Either
Our example will be some code to process data from the International Astronomical Union’s Minor Planet Center. In case astronomy isn’t your forte, a minor planet is essentially anything natural orbiting the Sun, which isn’t a proper planet or a comet. The Minor Planet Center provides a data file of all the known minor planets, which you can download from here: www.minorplanetcenter.net/iau/MPCORB/MPCORB.DAT. The format is documented here: https://minorplanetcenter.net/iau/info/MPOrbitFormat.html.
The aim of our code is to let consumers easily query the data file, to produce information such as a list of the brightest minor planets, or those with the most eccentric orbits.
Note
This chapter has made use of data and/or services provided by the International Astronomical Union's Minor Planet Center.
To help understand the code, let’s take a quick look at the file format. Listing 13-1 shows an abridged version of the start of the file.
MINOR PLANET CENTER ORBIT DATABASE (MPCORB)
This file contains published orbital elements for all numbered and unnumbered multi-opposition minor planets for which it is possible to make reasonable
The MPCORB.DAT file begins with some explanatory text, then some heading information followed by a set of dashes, and finally lines of data in fixed-length columns. (I’ve wrapped and separated the data lines in Listing 13-1 to make it clearer where the break is.)
Let’s also look at the documentation file (Listing 13-2).
The column headed 'F77' indicates the Fortran 77/90/95/2003/2008 format specifier that should be used to read the specified value.
Columns F77 Use
1 - 7 a7 Number or provisional designation
(in packed form)
9 - 13 f5.2 Absolute magnitude, H
15 - 19 f5.2 Slope parameter, G
(several more columns)
124 - 126 i3 Number of oppositions
For multiple-opposition orbits:
128 - 131 i4 Year of first observation
132 a1 '-'
133 - 136 i4 Year of last observation
For single-opposition orbits:
128 - 131 i4 Arc length (days)
133 - 136 a4 'days'(several more columns)
Listing 13-2
Extract from the file format documentation
So essentially the logic of the code to read the file will need to be:
Skip all the lines up to and including the line that looks like -------…
For each subsequent line…
Take characters 1–7 and use them as a string for the designation.
Take characters 9–13 and interpret them as a floating-point value for the absolute magnitude.
And so forth for each data item.
One complication will be the data between columns 128 and 136, which is interpreted differently depending on the value of the preceding “Number of oppositions” item. An opposition is simply the passage of the body through the opposite side of the sky from the Sun, when viewed from Earth. It’s significant because during opposition, the body is as its most visible.
Some Infelicitous Code
With those requirements in mind, Listing 13-3 shows some messy but working code. Have a read – how typical is this of F# you have written or had to maintain?
module MinorPlanets =
open System
let toCharArray (s : string) =
s.ToToCharArray()
let toDouble (s : string) =
match Double.TryParse(s) with
| true, x -> Some x
| false, x -> None
let toChar (s : string) =
if String.IsNullOrWhiteSpace(s) then None
else
Some(s.[0])
let toInt (s : string) =
match Int32.TryParse(s) with
| true, x -> Some x
| false, x -> None
let columnAsString startInd endInd (line : string) =
let oppositions = line |> columnAsString 124 126 |> toInt
let range = line |> rangeFromLine oppositions
{
Designation = columnAsString 1 7 line
AbsMag = columnAsDouble 9 13 line
SlopeParam = columnAsDouble 15 19 line
Epoch = columnAsString 21 25 line
MeanAnom = columnAsDouble 27 35 line
Perihelion = columnAsDouble 38 46 line
Node = columnAsDouble 49 57 line
Inclination = columnAsDouble 60 68 line
OrbEcc = columnAsDouble 71 79 line
MeanDaily = columnAsDouble 81 91 line
SemiMajor = columnAsDouble 93 103 line
Uncertainty = columnAsChar 106 106 line
Reference = columnAsString 108 116 line
Observations = columnAsInt 118 122 line
Oppositions = oppositions
Range = range
RmsResidual = columnAsDouble 138 141 line
PerturbersCoarse = columnAsString 143 145 line
PerturbersPrecise = columnAsString 147 149 line
ComputerName = columnAsString 151 160 line
Flags = columnAsCharArray 162 165 line
ReadableDesignation = columnAsString 167 194 line
LastOpposition = columnAsString 195 202 line
}
let createFromData (data : seq<string>) =
data
|> Seq.skipWhile (fun line ->
line.StartsWith("----------")
|> not) |> Seq.skip 1
|> Seq.filter (fun line ->
line.Length > 0)
|> Seq.map (fun line -> create line)
Listing 13-3
Initial state of the minor planets reading code
It’s important to say that this code, messy though it is, actually works! Listing 13-4 gives some code you can use to try it out. As we make our way through the various issues, we won’t be changing any of the functionality at all: this chapter is entirely about organization and presentation.
open System.IO
// To run this program, please first download the data from:
// Brightest 10 minor planets (absolute magnitude)
// Edit the path to reflect where you stored the file:
@".MinorPlanetsMPCORB.DAT"
|> File.ReadLines
|> MinorPlanets.createFromData
|> Seq.sortBy (fun mp ->
mp.AbsMag |> Option.defaultValue Double.MaxValue)
|> Seq.truncate 10
|> Seq.iter (fun mp ->
printfn "Name: %s, Abs. magnitude: %0.2f"
mp.ReadableDesignation
(mp.AbsMag |> Option.defaultValue nan))
Name: (136199) Eris, Abs. magnitude: -1.11
Name: (134340) Pluto, Abs. magnitude: -0.45
Name: (136472) Makemake, Abs. magnitude: -0.12
Name: (136108) Haumea, Abs. magnitude: 0.26
Name: (90377) Sedna, Abs. magnitude: 1.57
Name: (225088) Gonggong, Abs. magnitude: 1.92
Name: (90482) Orcus, Abs. magnitude: 2.29
Name: (50000) Quaoar, Abs. magnitude: 2.50
Name: (532037) 2013 FY27, Abs. magnitude: 3.20
Name: (4) Vesta, Abs. magnitude: 3.31
Listing 13-4
Trying out the code
Note by the way that in astronomy, a lower magnitude number means a greater brightness.
Convenience Functions
So where do we start? It might help to organize the code into smaller modules, thus improving the semantic focus that’s available to the reader. One grouping is obvious: functions such as toCharArray and toDouble are general-purpose convenience functions that don’t have any direct relationship with the astronomy domain. We can move these into a module called Convert (Listing 13-5).
module Convert =
open System
let toCharArray (s : string) =
s.ToToCharArray()
let tryToDouble (s : string) =
match Double.TryParse(s) with
| true, x -> Some x
| false, _ -> None
let tryToChar (s : string) =
if String.IsNullOrWhiteSpace(s) then None
else
Some(s.[0])
let tryToInt (s : string) =
match Int32.TryParse(s) with
| true, x -> Some x
| false, _ -> None
Listing 13-5
A Convert module
Putting just these functions in a module helps us focus what else might be wrong with them. Some of them return option types, so I renamed them using the “try” idiom – for example, tryToDouble. Also, the match expressions contained a bound but unused value x in the false branch. I replaced these with underscores. Always try to remove unused bindings in your code: explicitly ignoring them using underscore shows that you didn’t just overlook them, adding motivational transparency.
Column Extraction Functions
Another obvious set of candidates for moving into a module is functions such as columnAsString and columnAsCharArray, which are all about picking out a substring from a data line and converting it into some type. Moving them into a Column module means we can get rid of the repetitive use of the column prefix in their names. We also use the “try” idiom here when an option type is returned. Many of the columns have missing values in the dataset – some minor planets are in the process of discovery so not all the parameters will be known. For robustness, I’ve assumed that almost anything might be missing (Listing 13-6).
Again, now that the functions are in a module, we can focus on what could be improved within them. Listing 13-7 shows an arguably more idiomatic version.
module Column =
let asString startInd endInd (line : string) =
let len = endInd - startInd + 1
line
.Substring(startInd-1, len)
.Trim()
let asCharArray startInd endInd =
(asString startInd endInd) >> Convert.toCharArray
let tryAsInt startInd endInd =
(asString startInd endInd) >> Convert.tryToInt
let tryAsDouble startInd endInd =
(asString startInd endInd) >> Convert.tryToDouble
let tryAsChar startInd endInd =
(asString startInd endInd) >> Convert.tryToChar
Listing 13-7
Alternative layout for dot notation and using function composition
The things that have changed in Listing 13-7 are the following:
In asString, I removed the length calculation that was being done on the fly in the Substring call. Instead, I’ve put it into a separate binding (let len = ...). This reduces the number of things the reader has to think about at any one time.
Also in the asString function, I changed the layout to an indented style where each method call (.Substring() and .Trim()) is on its own line. I quite like this style because, again, it lets the reader think about one thing at a time. It’s mimicking the F# pipeline style where you put each |> someFunction on a separate line.
In the other functions (asCharArray etc.), I’ve used function composition. For example, in asCharArray, we explicitly compose the asString and Convert.toCharArray to produce the desired mapping from a data line to a value. This means we can remove the explicit line parameter because the partial application of asString still leaves the requirement of a line input. You might want to reflect on whether this is truly an improvement: it’s one of those cases where it depends on the skill levels of the maintainers.
The Observation Range Type
The next category of code that deserves to go into a separate module is the code relating to the “observation range” data. Just to recap, one of the data items needs to be different depending on the number of oppositions of the minor planet that have been observed. When only one opposition has been seen, we need to show how many days the body was observed for. When more than one opposition has been seen, we give the calendar years of the first and last observation. Listing 13-8 shows the relevant section from the documentation.
124 - 126 i3 Number of oppositions
For multiple-opposition orbits:
128 - 131 i4 Year of first observation
132 a1 '-'
133 - 136 i4 Year of last observation
For single-opposition orbits:
128 - 131 i4 Arc length (days)
133 - 136 a4 'days'(several more columns)
Listing 13-8
Observation range of a minor planet
The existing code rightly models this as a Discriminated Union. But the DU and its constructing function need to be pulled out into their own module (Listing 13-9).
module Observation =
type Range =
private
| SingleOpposition of ArcLengthDays:int
| MultiOpposition of FirstYear:int * LastYear:int
let fromLine (oppositions : int option) (line : string) =
match oppositions with
| None ->
None
| Some o when o = 1 ->
line
|> Column.tryAsInt 128 131
|> Option.map SingleOpposition
| Some _ ->
let firstYear = line |> Column.tryAsInt 128 131
let lastYear = line |> Column.tryAsInt 133 136
match firstYear, lastYear with
| Some(fy), Some(ly) ->
MultiOpposition(FirstYear=fy, LastYear=ly) |> Some
| _ ->
None
Listing 13-9
The Observation module
This is a great pattern for F# code: define a domain type in a module of its own, and place one or more functions to create instances of that type (or otherwise work with it) in the same module. As we’ve discussed before, the one issue this does give you is that of choosing names for the module and the type. Here, I’ve settled on Observation and Range. I made both the case constructors for Range private, as we provide a means of creating instances within the module: the fromLine function. You might have to remove the private keyword if it caused problems with serialization or with use from other languages. In that case, you might as well name both the module and the type “ObservationRange” and place the type outside the module.
A few other things I’ve changed in the observation range functions:
I changed the layout of the DU so that each case is indented to the right of the keyword type. This isn’t required by F#’s indentation rules, but the coding guidelines firmly recommend it.
I named each of the fields of the DU (ArcLengthDays, FirstYear, and LastYear). This greatly improves motivational transparency. You might also notice that I used these labels when constructing the MultiOpposition instance near the end of Listing 13-9.
I renamed the rangeFromLine function as fromLine. The module name now gives sufficient context. The function will be invoked, thus:
let range = line |> Observation.fromLine oppositions
I bound firstYear and lastYear values explicitly, rather than doing it on the fly in the match expression. Again, this reduces the cognitive load on the reader. Heavily nested calls, each level of which does some separate calculation, are the absolute bane of code readability. And they make step debugging much harder.
I tidied up some of the slightly idiosyncratic indentation.
The Importance of Alignment
The indentation changes merit a little more commentary. In one of the changes in Listing 13-9, this
line |> columnAsInt 128 131
|> Option.map SingleOpposition
has become this:
line
|> Column.tryAsInt 128 131
|> Option.map SingleOpposition
It’s particularly heinous to mix new-line styles when writing pipelines. It makes the reader wonder whether there is some unnoticed reason why successive lines are different. To avoid this, the simple rule is this: single piping operations can go into a single line; multiple piping operations like this example should each go on a separate line. In that case, the forward-pipe operators go at the beginning of each line.
The second indentation change in Listing 13-9 was this:
MultiOpposition(FirstYear=fy, LastYear=ly) |> Some
| _ ->
None
Apart from the separate binding of firstYear and lastYear, the point here is that if one branch of a match expression (the bit after the ->) is on the same line as the ->, the other branches should also be on the same line. Conversely, as in this example, if any branch won’t nicely fit on the same line, all the branches should begin on an indented new line.
Why am I banging on about indentation so much? It’s to do with the way the human eye and brain process information. What we are aiming for is code laid out in a very rectilinear (lined-up) style, where items that perform a similar role (e.g., different branches of the same match expression, or different steps of the same pipeline) are all lined up with one another. Then the reader can run their eye down the code and quickly pick out all the lines of equivalent significance. This engages the visual pattern processing part of the brain, which works somewhat separately (and faster) than the part of the brain concerned with interpreting the language of the code itself. I’ve illustrated this in Figure 13-1, showing with boxes the kinds of categories the reader might be looking for. Finding them is so much easier when the boxes are left aligned!
The Minor Planet Type
Now we tackle the core “domain object”: the type that represents an individual minor planet. Here’s the initial state of the code (Listing 13-10).
It’s horrible! In an effort to make the code more compact, two record fields have been put on each line. Some fields are divided by a semicolon, and others are not. Some of the field names, such as AbsMag, are abbreviated, while others, such as PerturbersPrecise, are written out fully. There are no triple-slash comments on the fields, so the consumer won’t get tool tips explaining the significance of each field, its units, etc. Let’s move the type into its own module and tidy it up (Listing 13-11).
module MinorPlanet =
type Body = {
/// Number or provisional designation (packed format)
Designation : string
/// Absolute magnitude
H : float option
/// Slope parameter
G : float option
/// Epoch in packed form
Epoch : string
/// Mean anomaly at the epoch (degrees)
M : float option
/// Argument of perihelion, J2000.0 (degrees)
Perihelion : float option
/// Longitude of the ascending node, J2000.0 (degrees)
Node : float option
/// Inclination to the ecliptic, J2000.0 (degrees)
Inclination : float option
/// Orbital eccentricity
e : float option
/// Mean daily motion (degrees per day)
n : float option
/// Semimajor axis (AU)
a : float option
/// Uncertainty parameter
Uncertainty : char option
/// Reference
Reference : string
/// Number of observations
Observations : int option
/// Number of oppositions
Oppositions : int option
/// Year of first and last observation,
/// or arc length in days.
Range : Observation.Range option
/// RMS residual (arcseconds)
RmsResidual : double option
/// Coarse indicator of perturbers
PerturbersCoarse : string
/// Precise indicator of perturbers
PerturbersPrecise : string
/// Computer name
ComputerName : string
/// Flags
Flags : char[]
/// Readable designation
ReadableDesignation : string
/// Date of last observation included in orbit solution (YYYYMMDD)
LastOpposition : string }
Listing 13-11
A tidier version of the minor planet type
I’ve put the type in its own module, MinorPlanet, and called the type itself Body. (If I were going for the type-outside-the-module style, both the type and the module could simply have been called MinorPlanet.) Each field has its own line and its own triple-slash comment. More controversially, I’ve used shorter names for some of the fields, such as H for absolute magnitude. I did this because this is the officially accepted domain term for the item. When astronomers see a value H in the context of a Solar System body, they know it means absolute magnitude. I’ve even reflected the fact that some accepted domain terms are lowercase, for example, e for orbital eccentricity. I think this is reasonable in a domain such as this, where there is an accepted terminology having some terms conventionally expressed in lowercase.
How far you take use of domain terminology is an interesting question. In math-related code, I have occasionally found myself using Greek letters and symbols as names, as in Listing 13-12.
let eccentricity ? h μ =
1. + ((2. * ? * h * h) / (μ * μ))
|> sqrt
Listing 13-12
Using Greek characters in code
This has the advantage that your code can look a lot like the accepted formula for a particular mathematical calculation. But it does mean a lot of copy and pasting of special characters or use of ALT-xxx keyboard codes, so it is probably not to be encouraged!
Getting back to the minor planet record type, we also need to place the related functions into our MinorPlanet module. Listing 13-13 shows the tidied-up function to create a MinorPlanet.Body instance from a string.
module MinorPlanet =
type Body = {
// Code as Listing 13-11
...
let fromMpcOrbLine (line : string) =
let oppositions = line |> Column.asString 124 126 |> Convert.tryToInt
let range = line |> Observation.fromLine oppositions
{
Designation = line |> Column.asString 1 7
H = line |> Column.tryAsDouble 9 13
G = line |> Column.tryAsDouble 15 19
Epoch = line |> Column.asString 21 25
M = line |> Column.tryAsDouble 27 35
Perihelion = line |> Column.tryAsDouble 38 46
Node = line |> Column.tryAsDouble 49 57
Inclination = line |> Column.tryAsDouble 60 68
e = line |> Column.tryAsDouble 71 79
n = line |> Column.tryAsDouble 81 91
a = line |> Column.tryAsDouble 93 103
Uncertainty = line |> Column.tryAsChar 106 106
Reference = line |> Column.asString 108 116
Observations = line |> Column.tryAsInt 118 122
Oppositions = oppositions
Range = range
RmsResidual = line |> Column.tryAsDouble 138 141
PerturbersCoarse = line |> Column.asString 143 145
PerturbersPrecise = line |> Column.asString 147 149
ComputerName = line |> Column.asString 151 160
Flags = line |> Column.asCharArray 162 165
ReadableDesignation = line |> Column.asString 167 194
LastOpposition = line |> Column.asString 195 202
}
Listing 13-13
Creating a MinorPlanet.Body instance
I’ve taken another potentially controversial step here: I’ve aligned the start-and-end index positions as if they were numbers in a table. There are advantages and disadvantages to this. The obvious disadvantage is that it’s fiddly to do. And if you rename anything, you have to adjust the alignment. The advantage, and for me it’s an overwhelming one in this case, is again that you can run your eye down the code and spot patterns and anomalies.
If you are going to follow this approach, it’s well worth being familiar with your editor’s block selection features. In Visual Studio, you can ALT+drag to select a rectangular block, which makes it much easier to adjust alignment. In Visual Studio Code, it’s SHIFT+ALT+click. I would only do this kind of super-alignment in special cases such as Listing 13-13, where there are a lot of necessarily repetitive lines of code.
Listing 13-14 shows the original code for creating minor planet record instances from a sequence of strings.
let createFromData (data : seq<string>) =
data
|> Seq.skipWhile (fun line ->
line.StartsWith("----------")
|> not) |> Seq.skip 1
|> Seq.filter (fun line ->
line.Length > 0)
|> Seq.map (fun line -> create line)
Listing 13-14
Original code for creating minor planet instances
By now, you probably recognize what needs doing here. We should move the header-skipping code to its own function, and we should get rid of the crazy indenting. A good principle to adopt is to indent things the minimum amount that is required by the compiler. This applies even if you have to add extra line breaks to achieve it – unless the line is going to be pretty short anyway. Listing 13-15 shows the improved version.
Note
This principle of indenting things as little as possible – even if it means adding extra line breaks – is very different from the conventions adopted by other languages, notably Python. The big advantage of the minimal-indent approach is that your code won’t stop compiling due to indenting issues, if a label is renamed to a name with a different length.
module MinorPlanet =
// Code as Listing 13-11 and 13-13
...
let private skipHeader (data : seq<string>) =
data
|> Seq.skipWhile (fun line ->
line.StartsWith("----------") |> not)
|> Seq.skip 1
let fromMpcOrbData (data : seq<string>) =
data
|> skipHeader
|> Seq.filter (fun line -> line.Length > 0)
|> Seq.map fromMpcOrbLine
Listing 13-15
Improved code for creating minor planet instances
I’ve also renamed the createFromData function to fromMpcOrbData as this is a little more specific. The abbreviation MpcOrb is reasonable here because that is what the input file is called.
Finally, here’s how the demonstration code needs to change to reflect the improvements we’ve made (Listing 13-16).
open System.IO
// Get data from: https://www.minorplanetcenter.net/iau/MPCORB/MPCORB.DAT
// Brightest 10 minor planets (absolute magnitude)
@".MinorPlanetsMPCORB.DAT"
|> File.ReadLines
|> MinorPlanet.fromMpcOrbData
|> Seq.sortBy (fun mp ->
mp.H |> Option.defaultValue Double.MaxValue)
|> Seq.truncate 10
|> Seq.iter (fun mp ->
printfn "Name: %s, Abs. magnitude: %0.2f"
mp.ReadableDesignation
(mp.H |> Option.defaultValue nan))
Name: (136199) Eris, Abs. magnitude: -1.11
Name: (134340) Pluto, Abs. magnitude: -0.45
Name: (136472) Makemake, Abs. magnitude: -0.12
Name: (136108) Haumea, Abs. magnitude: 0.26
Name: (90377) Sedna, Abs. magnitude: 1.57
Name: (225088) Gonggong, Abs. magnitude: 1.92
Name: (90482) Orcus, Abs. magnitude: 2.29
Name: (50000) Quaoar, Abs. magnitude: 2.50
Name: (532037) 2013 FY27, Abs. magnitude: 3.20
Name: (4) Vesta, Abs. magnitude: 3.31
Listing 13-16
Calling the revised code
Recommendations
Use thoughtful naming and layout to maximize the readability of your code. In particular:
Choose names for types and functions that reflect exactly what they represent or do – but don’t be verbose. Remember that the name of the module in which items live can provide additional context, allowing you to keep the item names relatively short.
When you are forced to bind a value that you don’t later use, for example, in a match expression, use underscore to explicitly ignore it.
Use the try… idiom when a function returns an option type.
Don’t force the reader to think about too much at a time. For example, a line with heavy nesting and multiple calculations might benefit from being broken up into separate, explicit steps.
Isolate nondomain-specific items from domain-specific items, typically by placing them in separate modules. Different “domain objects” should also go into (or beside) their own modules, along with closely associated functions. More generally, keep modules short by ruthlessly classifying items into small groupings. Sometimes, this process can be helped by nesting modules.
Where there is already established domain terminology, derive the naming in your domain-specific code from that terminology.
When using Discriminated Unions, seriously consider giving explicit names to any case payload fields, especially when there are several fields that could be confused.
When a pipeline uses more than one forward-pipe operator, place each operation on a separate line. Never ever mix the single-line with the new-line style.
Within a match expression, be consistent on whether the code following -> is on the same or a new line.
When declaring and constructing records, place fields on separate lines unless the record definition is very small. Never mix single-line and new-line styles in the same record declaration or construction.
For domain classes, record types and API functions; use triple-slash comments to document members, fields, and public functions. Only rarely can you cram sufficient information into the name.
Above all, name items and align your code to maximize the eye’s ability to spot patterns – and exceptions to those patterns. If you only take away one principle from this chapter, make it this one!
Summary
It’s rare to be able to organize code perfectly on the first pass. It’s absolutely fine to hack something together just to see if it works and to help you understand the domain you are working on. This is in keeping with the exploratory spirit of F# coding. But what happens next is also important. Tirelessly polish your code using the principles from this chapter. What you are aiming for is code that, in the words of computer scientist Tony Hoare, has “obviously no deficiencies”:
There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.
As Hoare points out, achieving “obviously no deficiencies” isn’t easy. But the cost of bugs escalates exponentially as they become embedded in a larger system and in the associated processes. So designing code that has “obviously no deficiencies” is – even in the medium term – much cheaper. Remember what we said in Chapter 1 about complexity explosions!
In the next chapter, we’ll draw together the various threads from this book and remind ourselves of the key practices required to produce truly stylish F# code.
Exercise
Exercise 13-1 – Making Code Readable
The following working code searches for files below a certain path and returns those files whose names match a regular expression and which have the ReadOnly attribute set.