© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. EasonStylish F# 6https://doi.org/10.1007/978-1-4842-7205-3_13

13. Layout and Naming

Kit Eason1  
(1)
Farnham, Surrey, UK
 

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
(about 30 more lines of explanation)
Des'n     H     G    Epoch    M          Peri.       Node       Incl.
e          n           a          Reference    #Obs  #Opp  Arc        rms
Perts              Computer
---------------------------------------------------------------------------
00001    3.53  0.15 K2175 248.40797   73.73770   80.26762   10.58820  0.0783941  0.21429254   2.7656551  0 MPO600179  7283 120 1801-2021 0.51 M-v 30k Pan        0000      (1) Ceres              20210128
00002    4.22  0.15 K2175 230.07779  310.44122  172.91972   34.89867  0.2297622  0.21335178   2.7737791  0 MPO636060  8823 119 1804-2021 0.58 M-c 28k Pan        0000      (2) Pallas             20210716
Listing 13-1

The start of the MPCORB.DAT file

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) =
        line.Substring(startInd-1,endInd-startInd+1).Trim()
    let columnAsCharArray startInd endInd (line : string) =
        toCharArray(columnAsString startInd endInd line)
    let columnAsInt startInd endInd (line : string) =
        toInt(columnAsString startInd endInd line)
    let columnAsDouble startInd endInd (line : string) =
        toDouble(columnAsString startInd endInd line)
    let columnAsChar startInd endInd (line : string) =
        toChar(columnAsString startInd endInd line)
    type ObservationRange =
    | SingleOpposition of int
    | MultiOpposition of int * int
    let rangeFromLine (oppositions : int option) (line : string) =
        match oppositions with
        | None -> None
        | Some o when o = 1 ->
            line |> columnAsInt 128 131
            |> Option.map SingleOpposition
        | Some o ->
            match (line |> columnAsInt 128 131),
                  (line |> columnAsInt 133 136) with
            | Some(firstObservedYear), Some(lastObservedYear) ->
                MultiOpposition(firstObservedYear,
                   lastObservedYear) |> Some
            | _ -> None
    type MinorPlanet = {
        Designation : string; AbsMag : float option
        SlopeParam : float option; Epoch : string
        MeanAnom : float option; Perihelion : float option
        Node : float option; Inclination : float option
        OrbEcc : float option; MeanDaily : float option
        SemiMajor : float option; Uncertainty : char option
        Reference : string; Observations : int option
        Oppositions : int option; Range : ObservationRange option
        RmsResidual : double option; PerturbersCoarse : string
        PerturbersPrecise : string; ComputerName : string
        Flags : char[]; ReadableDesignation : string
        LastOpposition : string }
    let private create (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:
// https://www.minorplanetcenter.net/iau/MPCORB/MPCORB.DAT
// 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).
module Column =
    let asString startInd endInd (line : string) =
        line.Substring(startInd-1,endInd-startInd+1).Trim()
    let asCharArray startInd endInd (line : string) =
        Convert.toCharArray(asString startInd endInd line)
    let tryAsInt startInd endInd (line : string) =
        Convert.tryToInt(asString startInd endInd line)
    let tryAsDouble startInd endInd (line : string) =
        Convert.tryToDouble(asString startInd endInd line)
    let tryAsChar startInd endInd (line : string) =
        Convert.tryToChar(asString startInd endInd line)
Listing 13-6

A Column module

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:
                match (line |> columnAsInt 128 131),
                      (line |> columnAsInt 133 136) with
                | Some(firstObservedYear), Some(lastObservedYear) ->
                    MultiOpposition(firstObservedYear,
                       lastObservedYear) |> Some
                | _ -> None
to this:
                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

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!
../images/462726_2_En_13_Chapter/462726_2_En_13_Fig1_HTML.png
Figure 13-1

Code is more readable when thoughtfully 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).
    type MinorPlanet = {
        Designation : string; AbsMag : float option
        SlopeParam : float option; Epoch : string
        MeanAnom : float option; Perihelion : float option
        Node : float option; Inclination : float option
        OrbEcc : float option; MeanDaily : float option
        SemiMajor : float option; Uncertainty : char option
        Reference : string; Observations : int option
        Oppositions : int option; Range : Observation.Range option
        RmsResidual : double option; PerturbersCoarse : string
        PerturbersPrecise : string; ComputerName : string
        Flags : char[]; ReadableDesignation : string
        LastOpposition : string }
Listing 13-10

Initial state of the minor planet type

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.
open System.IO
open System.Text.RegularExpressions
let find pattern dir =
    let re = Regex(pattern)
    Directory.EnumerateFiles
                    (dir, "*.*", SearchOption.AllDirectories)
    |> Seq.filter (fun path -> re.IsMatch(Path.GetFileName(path)))
    |> Seq.map (fun path ->
        FileInfo(path))
    |> Seq.filter (fun fi ->
                fi.Attributes.HasFlag(FileAttributes.ReadOnly))
    |> Seq.map (fun fi -> fi.Name)
find "[a-z]." @"c: emp"

How would you reorganize this code to make it easier to read, maintain, and extend?

Hint: You might want to add a few modules, which may each have only one function.

Exercise Solution

Exercise 13-1 – Making Code Readable
Here’s my attempt to improve this code. How does yours compare?
open System.IO
open System.Text.RegularExpressions
module FileSearch =
    module private FileName =
        let isMatch pattern =
            let re = Regex(pattern)
            fun (path : string) ->
                let fileName = Path.GetFileName(path)
                re.IsMatch(fileName)
    module private FileAttributes =
        let hasFlag flag filePath =
            FileInfo(filePath)
                .Attributes
                .HasFlag(flag)
    /// Search below path for files whose file names match the specified
    /// regular expression, and which have the 'read only' attribute set.
    let findReadOnly pattern dir =
        Directory.EnumerateFiles(dir, "*.*", SearchOption.AllDirectories)
        |> Seq.filter (FileName.isMatch pattern)
        |> Seq.filter (FileAttributes.hasFlag FileAttributes.ReadOnly)
find "[a-z]." @"c: emp"
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.179.58