© 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_14

14. Summary

Kit Eason1  
(1)
Farnham, Surrey, UK
 

We are what we repeatedly do. Excellence, then, is not an act, but a habit.

—Will Durant, Historian and Philosopher (paraphrasing Aristotle)

F# and the Sense of Style

Well, you reached the end congratulations! I very much hope you picked up some useful habits from these pages. They’re distilled from several years of very happy experience of using F# in a wide variety of industries. While you may not agree with everything I say, I hope I’ve helped you become a more reflective practitioner of the art of programming in F#.

Before I let you go, let me reiterate the key points from each of the preceding chapters. If any of these still feel unfamiliar, it might be worth turning back to the chapters in question.

Designing Functions with Types

In Chapter 2, I talked about how to design and write that fundamental unit of work in F#: the function. My approach is to start by defining the required signature. Then I write a body that matches the signature. Likely as not, doing this causes me to rethink what the function should really do and hence what its signature should be. I repeatedly refine the signature and body, trying to eliminate as many errors as possible at the signature (type) level – but also making sure any remaining potential errors are handled explicitly in the function body.

I also pointed out the usefulness of Single-Case Discriminated Unions for modeling some business types. It’s often useful to place such a union into or beside a module named after the type in question, together with functions to create and work with instances of the union.

In passing, I also mentioned how you can sometimes simplify code by defining your own operators. It’s a technique to use sparingly.

Missing Data

In Chapter 3, I showed you ways to stop using null values for representing missing data. Either you can use Discriminated Unions to model cases where data items are present or absent, or you can use option types in situations where the possibilities are simply “value present” or “value not present” and where it’s obvious from context why these cases would occur.

I talked about the Option module , in particular the Option.bind, Option.map, and Option.defaultValue functions, which you can use to make your option type handling pipeline-friendly. There are also functions such as Option.ofObj and Option.ofNull, which can help you transition data between the nullable and the F# world.

Don’t forget that the advent of “nullable reference types” in C# – that is, the potential for reference types to be nonnullable by default – is slowly changing the landscape here. You’ll need to keep up with the latest language design thinking for both C# and F# to get the best from these changes.

Finally, I mentioned that you should avoid exposing F#-specific types, such as DUs and option types, in APIs that might be consumed by C# or other .NET languages.

Collection Functions

In Chapter 4, I showed how vital the fluent use of collection functions is to effective F# programming. I encouraged you to get familiar with the many functions in the Array, List, and Seq modules, such as map, iter, and filter. I pointed out the importance of choosing and interpreting functions, particularly collection functions, by looking at their type signatures. Remember there are handy visual tables in this chapter to help you with this (Tables 4-1 through 4-11).

Be aware of the significance of partial functions and learn to handle their failure cases gracefully. Often, this can be done by using a try... variant of a function, which returns an option type, or by writing such a variant yourself.

I pointed out the dangers of loops that use mutable values as counters and flags. There is almost always an easier way of achieving the same thing using a collection function. Learn to write neat, elegant pipelines of collection functions – but don’t let them get too long, or maintainers may find them difficult to interpret and debug.

Immutability and Mutation

In Chapter 5, I discussed how, though baffling at first, immutability by default is the key to the practical benefits of functional programming. Once you understand how to program in an immutable style, try to get in the habit of coding in that style first, only falling back on mutation for performance reasons, or because what you’re trying to do isn’t possible to express clearly in immutable terms. I realize, though, that it may take a while before this approach seems natural to you. Remember that using the collection functions is often the way to move away from loop-based, mutable programming to mutable style.

Pattern Matching

In Chapter 6, I showed how there’s more to pattern matching than match expressions. I urged you to practice recognizing and using pattern matching wherever it can occur. Use Table 6-2 as a guide both to what syntax features are available and how freely to use them. Understand active patterns and use them where appropriate, but not at the expense of obfuscating your code. Remember that you can pattern match on types, which is indispensable when dealing with class hierarchies, and on the literal null, which may sometimes be useful when dealing with nullable values.

Record Types

In Chapter 7, I discussed how to use record types as the first choice for representing small groups of named items. Be familiar with the design considerations that drive the choice of records over classes: records are to be preferred when there are no “moving parts,” and the external and internal representations of data can be the same.

When you need to “modify” a record type instance, reach for the with keyword rather than making the record instance or its fields mutable.

I discussed the difference between structural (content) equality, as implemented by default in record types, and reference equality, as implemented by default in classes.

You can add instance or static methods to records, but do so sparingly. Alternatives include placing the record type and closely related functions in a module, or – when behaviors need to be complex and closely coupled to the type – using a class instead.

Remember that you don’t have to declare a record type in advance. If the scope is small, it might be worth instantiating an anonymous record with {| Label = value; ... |}.

Finally, I think it’s worth understanding the implications of applying the [<Struct>] attribute to a record type.

Classes

In Chapter 8, I discussed F#’s approach to .NET classes, which allow you to represent combinations of private and public values and behaviors. I suggest you reach for classes rather than record types when you truly need asymmetric representation of data, or you need moving parts. Typically, having moving parts involves there being an internal mutable state, together with methods that indirectly let the caller change that state. Here, F# classes are the most natural fit. Also consider using F# classes when you need to participate in a C#-style class hierarchy.

Conversely, be aware of the costs of using classes. I showed how using them can lead to accidental complexity, which often starts because of a need to implement equality and/or comparison between class instances.

Remember that object expressions can sometimes let you provide inheritance-like behavior with minimal code.

Programming with Functions

In Chapter 9, I introduced the twin concepts of currying and partial application . Currying is the separation of function parameters into individual items, and partial application is providing just some of those parameters when using the function. Prefer curried style unless there is a special reason to tuple a function’s parameters together. Define curried parameters in an order that best allows the caller to partially apply when necessary. Use partial application if it makes your code clearer or eliminates repetition.

I showed how functions are first-class values, meaning you can create them, bind them to other values, pass or receive them as parameters, and return them as the result of other functions – all with little more effort than it takes to do the same for, say, an integer.

I explained how you can compose functions using the >> operator, if the two functions return and take the same types. Consider using this feature if it genuinely simplifies or clarifies your code. Be wise to the costs of function composition in terms of readability and the ability to step through code and inspect intermediate values.

Asynchronous and Parallel Programming

In Chapter 10, I illustrated asynchronous programming using the analogy of ordering pizza at a restaurant – one which gives you a pager to tell you when your meal is ready. To make your code asynchronous, identify places where your code is “ordering pizza,” typically requesting a disk or network operation. Use let!, use!, or match! bindings in an async {} block to perform such operations, freeing up the thread to do other work and ensuring that another thread picks up and processes the result when it becomes available. Return the (promise of a) result of an async {} block using the return keyword.

I pointed out the difference between the F# “cold task” model for asynchronous calls and C#’s “hot task” model – where the task is running as soon as it is defined. Be prepared to translate between the two worlds, for example, by using Async.StartAsTask to create a running, C#-style task. Use Async.RunSynchronously very sparingly to actually get the result of an asynchronous computation, remembering that doing so is equivalent to waiting at the restaurant counter for your pizza pager to go off. In an extended example, I took you through the process of implementing “async all the way down,” at each layer of a stack of functions.

Where it makes sense to run several asynchronous computations in parallel, consider doing so with Async.Parallel, remembering that this takes an optional parameter that allows you to limit the number of threads working simultaneously.

For computations that don’t need to be asynchronous but which can usefully be run in parallel, simply use Array.Parallel.map or one of the other functions in the Array.Parallel module.

Don’t forget that F# now has a native task {} computation expression, allowing you to work directly in terms of C#-style “hot tasks.”

Railway Oriented Programming

In Chapter 11, I took you through the ROP style. This is an approach centered around the use of the Result type, which allows you to represent the results of computations that might pass or fail. I recast the ROP metaphor in terms of machine tools in a widget factory. Each machine tool is placed into an “adapter housing” so that we can put together a production line of units, each of which can bypass failures and process successes from the previous step. In ROP, the basic functions generally take a “naked” type as input and return a result wrapped in a Result type. We use an adapter function (Result.bind) to convert each such function so that it both takes a Result type and returns a Result type. Functions thus adapted can be composed using >>. ROP also uses Result.map to adapt functions that can never fail so that they can also slot into the pipeline.

We can use a Discriminated Union to enumerate all the error possibilities, with some cases having payloads to convey further information about the error. Doing this means that errors that have occurred anywhere in the pipeline can be handled in a centralized way using Result.mapError.

I suggested that you use ROP judiciously. But even if you choose not to adopt it in your code, you should be sure to understand how it works, as doing so can yield insights that are applicable in your F# programming generally.

Performance

In Chapter 12, I encouraged you to code with mechanical sympathy , being aware of the performance characteristics of the data structures and algorithms that you employ. For example, don’t use indexed access into F# lists, and don’t create large numbers of objects unnecessary, particularly if they will persist beyond Generation 0 of the garbage collection cycle.

Use collection functions combined with the appropriate collection types to write performant code. For example, Array.map and Array.filter might in some circumstances be a better choice than List.map and List.filter. If you don’t want intermediate collections in a pipeline to be realized, consider using functions from the Seq module. Remember that comprehensions (code in seq {}, [||], or [], combined with the yield keyword) can be a great way to combine somewhat imperative code with functional concepts, all in a performant way.

Where performance is critical, create repeatable benchmarks. I gave an example of doing this using BenchmarkDotNet. First write correct code, typically supported by unit tests. Then refine performance progressively, all while keeping an eye both on the benchmark results and the unit test results.

I suggested that you shouldn’t optimize prematurely, nor microoptimize unnecessarily, especially if doing so compromises the clarity or reliability of your code or risks not getting the benefits of future compiler or platform improvements.

Layout and Naming

In Chapter 13, I encouraged you to treat layout and naming as key drivers of the quality of your code. I suggested that you choose concise names that reflect exactly what an item does or represents. Placing items in carefully named modules can help with this. It’s often useful to classify types and functions carefully, for example, separating generic from domain-specific types and functions and placing functions relating to each type into their own module.

I gave a few tips on layout. These boil down to organizing your code to help the eye pick out patterns (and exceptions to those patterns) using consistent line breaks and indentation. You can also help the reader by using triple-slash comments to document public functions, types, etc., as these comments will appear as tool tips in most editors.

I also pointed you to the official F# Style Guide, which contains a wealth of more detailed recommendations on topics such as naming, spacing, and indentation.

Onward!

You’re now equipped to write stylish, performant, maintainable code. But if you ever find yourself in doubt, try applying the principles we established in Chapter 1.
  • Semantic focus: When looking at a small piece of code, can the reader understand what is going on without needing a great deal of context from elsewhere in the codebase – or worse still, from outside it?

  • Revisability: Can the maintainer make changes to the code and be confident that there won’t be unexpected knock-on effects elsewhere?

  • Motivational transparency: Can the reader tell what the author of the code intended to achieve, and why each piece of code is as it is?

  • Mechanical sympathy: Does the code make best use of the facilities available in the platform and the language, for instance, by using the right data structure and accessing it in an efficient way?

Stick to these principles, learn what language features help you adhere to them, and you’ll have an enjoyable and productive time with F#. Have fun!

..................Content has been hidden....................

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