CHAPTER 20

Images

Designing F# Libraries

This book deals with F#, a language situated in the context of .NET-based software construction and engineering. As an expert F# programmer, you need more than knowledge of the F# language; you also need to use a range of software-engineering tools and methodologies wisely to build software that is truly valuable for the situation for which it’s deployed. The previous chapter touched on some important tools. This final chapter looks at some of the methodological issues related to F# library design, including:

  • Designing vanilla .NET libraries that minimize the use of F#-specific constructs, according to existing .NET design conventions
  • Elements of functional programming design methodology, which offers important and deep insights into programming, but doesn’t address several important aspects of the library or component design problems
  • Specific suggestions for designing .NET and F# libraries, including naming conventions, how to design types and modules, and guidelines for using exceptions

F# is often seen as a functional language, but as this book emphasizes, it’s really a multiparadigm language. The OO, functional, imperative, and language-manipulation paradigms are all well supported. That is, F# is a function-oriented language, so many of the defaults are set up to encourage functional programming, but programming in the other paradigms is effective and efficient, and a combination is often best of all. Nonetheless, a multiparadigm language brings challenges for library designs and coding conventions.

It’s a common misconception that the functional and OO programming methodologies compete; in fact, they’re largely orthogonal. It’s important to note, however, that functional programming doesn’t directly solve many of the practical and prosaic issues associated with library design—for solutions to these problems, you must look elsewhere. In the context of .NET programming, this means turning first to the .NET Library Design Guidelines, published online by Microsoft and as a book by Addison-Wesley.

In the official documents, the .NET library design is described in terms of conventions and guidelines for the use of the following constructs in public framework libraries:

  • Assemblies, namespaces, and types (see Chapters 6 and 7 of this book)
  • Classes and objects, containing properties, methods, and events (see Chapter 6)
  • Interfaces (in other words, object interface types; see Chapter 6)
  • .NET delegate types (mentioned briefly in Chapters 5 and 6)
  • Enumerations (that is, enums from languages such as C#, mentioned briefly in Chapter 6)
  • Constants (that is, constant literals from languages such as C#)
  • Type parameters (that is, generic parameters; see Chapter 5)

From the perspective of F# programming, you must also consider the following constructs:

Framework library design is always nontrivial and often underestimated. F# framework and library design methodology is inevitably strongly rooted in the context of .NET OO programming. This chapter gives our opinions about how you can approach library design in the context of F# programming. These opinions are neither proscriptive nor official. More official guidelines may be developed by the F# team and community at some future point, although ultimately the final choices lie with F# programmers and software architects.

images Note: Some F# programmers choose to use library and coding conventions much more closely associated with OCaml, Python, or a particular application domain, such as hardware verification. For example, OCaml coding uses underscores in names extensively, a practice avoided by the .NET Framework guidelines but used in places by the F# library. Some also choose to adjust coding conventions to their personal or team tastes.

Designing Vanilla .NET Libraries

One way to approach library design with F# is to design libraries according to the .NET Library Design Guidelines. This implicitly can mean avoiding or minimizing the use of F#-specific or F#-related constructs in the public API. We call these libraries vanilla .NET libraries, as opposed to libraries that use F# constructs without restriction and are mostly intended for use by F# applications.

Designing vanilla .NET libraries means adopting the following rules:

  • Apply the .NET Library Design Guidelines to the public API of your code. Your internal implementation can use any techniques you want.
  • Restrict the constructs you use in your public APIs to those that are most easily used and recognized by .NET programmers. This means avoiding the use of some F# idioms in the public API.
  • Use the Microsoft FxCop quality-assurance tool to check the public interface of your assembly for compliance. Use FxCop exemptions where you deem necessary.

At the time of writing, here are some specific recommendations from the authors of this book:

  • Avoid using F# list types 'T list in vanilla .NET APIs. Use seq<'T> (i.e. IEnumerable<'T>) or arrays instead of lists.
  • Avoid using F# function types in vanilla .NET APIs. F# function values tend to be a little difficult to create from other .NET languages. Instead, consider using .NET delegate types, such as the overloaded System.Func<...> types available from .NET 3.5 onward.
  • Avoid using F#-specific language constructs, such as discriminated unions and optional arguments, in vanilla .NET APIs.

For example, consider the code in Listing 20-1, which shows some F# code that you intend to adjust to be suitable for use as part of a .NET API.

Listing 20-1. An F# Type Prior to Adjustment for Use as Part of a Vanilla .NET API

namespace global

open System

type APoint(angle, radius) =
    member x.Angle = angle
    member x.Radius = radius
    member x.Stretch(l) = APoint(angle = x.Angle, radius = x.Radius * l)
    member x.Warp(f) = APoint(angle = f(x.Angle), radius = x.Radius)

    static member Circle(n) =
        [ for i in 1..n -> APoint(angle = 2.0 * Math.PI / float(n), radius = 1.0) ]

    new() = APoint(angle = 0.0, radius = 0.0)

The inferred F# type of this class is:


type APoint =
    class
        new : unit -> APoint
        new : angle:float * radius:float -> APoint
        member Stretch : l:float -> APoint
        member Warp : f:(float -> float) -> APoint
        member Angle : float
        member Radius : float
        static member Circle : n:int -> APoint list
    end

Let’s look at how this F# type appears to a programmer using C# or another .NET library. The approximate C# signature is:


// C# signature for the unadjusted APoint class of Listing 20-1

[Serializable]
public class APoint
{
    public APoint();
    public APoint(double angle, double radius);

    public double Angle { get; }
    public double Radius { get; }

    public static Microsoft.FSharp.Collections.FSharpList<APoint> Circle(int n);
    public APoint Stretch(double l);
    public APoint Warp(Microsoft.FSharp.Core.FSharpFunc<double, double> f);
}

There are some important points to notice about how F# has chosen to represent constructs here. For example:

  • Metadata, such as argument names, has been preserved.
  • F# methods that take tupled arguments become C# methods that take multiple arguments.
  • Functions and lists become references to corresponding types in the F# library.

The full rules for how F# types, modules, and members are represented in the .NET Common Intermediary Language are explained in the F# language reference on the F# web site.

To make a .NET component, place it in a file component.fs and compile this code into a strong-name signed DLL using the techniques from Chapter 7:


C:fsharp> sn -k component.snk
C:fsharp> fsc --target:library --keyfile:component.snk  component.fs  

Figure 20-1 shows the results of applying the Microsoft FxCop tool to check this assembly for compliance with the .NET Framework Design Guidelines. This reveals a number of problems with the assembly. For example, the .NET Framework Design Guidelines require:

  • Types must be placed in namespaces.
  • Public identifiers must be spelled correctly.
  • Additional attributes must be added to assemblies related to .NET Security and Common Language Specification (CLS) compliance.
images

Figure 20-1. Running FxCop on the code from Listing 20-1

Listing 20-2 shows how to adjust this code to take these things into account.

Listing 20-2. An F# Type After Adjustment for Use As Part of a Vanilla.NET API

namespace ExpertFSharp.Types

open System

module AssemblyAttributes =
    [<assembly : System.Runtime.InteropServices.ComVisible(false);
      assembly : System.CLSCompliant(true)>]
    do()

type RadialPoint(angle, radius) =
    member x.Angle = angle
    member x.Radius = radius
    member x.Stretch(factor) = RadialPoint(angle = x.Angle, radius = x.Radius * factor)
    member x.Warp(transform : Converter<_, _>) =
        RadialPoint(angle = transform.Invoke(x.Angle), radius = x.Radius)
    static member Circle(count) =
        seq { for i in 1..count ->
                RadialPoint(angle = 2.0 * Math.PI / float(count), radius = 1.0)}
    new() = RadialPoint(angle = 0.0, radius = 0.0)

The inferred F# type of the code in Listing 20-2 is:


type RadialPoint =
  class
    new : unit -> RadialPoint
    new : angle:float * radius:float -> RadialPoint
    member Stretch : factor:float -> RadialPoint
    member Warp : transform:System.Converter<float,float> -> RadialPoint
    member Angle : float
    member Radius : float
    static member Circle : count:int -> seq<RadialPoint>
  end

The C# signature is now:


// C# signature for the unadjusted RadialPoint class of Listing 20-2
[Serializable]
public class RadialPoint
{
    public RadialPoint();
    public RadialPoint(double angle, double radius);

    public double Angle { get; }
    public double Radius { get; }

    public static IEnumerable<RadialPoint> Circle(int count);
    public RadialPoint Stretch(double factor);
    public RadialPoint Warp(Converter<double, double> transform);
}

The fixes you make to prepare this type for use as part of a vanilla .NET library are:

  • Add several attributes as directed by the FxCop tool. You can find the meaning of these attributes in the MSDN documentation referenced by the FxCop warning messages.
  • Adjust several names: APoint, n, l, and f to become RadialPoint, count, factor, and transform, respectively.
  • Use a return type of seq<RadialPoint> instead of RadialPoint list by changing a list construction using [ ... ] to a sequence construction using seq { ... }. An alternative option would be to use an explicit upcast ([ ... ] :> seq<_>).
  • Use the .NET delegate type System.Converter instead of an F# function type.

After applying these, the last remaining FxCop warning tells you that namespaces with two to three types aren’t recommended.

The last two previous points aren’t essential, but, as mentioned, delegate types and sequence types tend to be easier for C# programmers to use than F# function and list types (F# function types aren’t compiled to .NET delegate types, partly for performance reasons). Note that you can use FxCop exemptions to opt out of any of the FxCop rules, either by adding an exemption entry to FxCop itself or by attaching attributes to your source code.

images Tip: If you’re designing libraries for use from any .NET language, there’s no substitute for actually doing some experimental C# and Visual Basic programming to ensure that uses of your libraries look good from these languages. You can also use tools such as .NET Reflector and the Visual Studio Object Browser to ensure that libraries and their documentation appear as expected to developers. If necessary, enlist the help of a C# programmer and ask her what she thinks.

Understanding Functional Design Methodology

So far, this chapter has looked at how to do vanilla .NET library design with F#. Frequently, however, F# programmers design libraries that are free to make more sophisticated use of F#, and they more or less assume that client users are using F# as well. To make the best use of F# in this situation, it’s helpful to use functional programming design techniques as part of the library design process. For this reason, this section covers what functional programming brings to the table with regard to design methodology.

Understanding Where Functional Programming Comes From

Let’s recap the origins of the major programming paradigms from a design perspective:

  • Procedural programming arises from the fundamentally imperative nature of processing devices: microprocessors are state machines that manipulate data using side effects.
  • Object-oriented programming arises from the need to encapsulate and reuse large objects and associated behaviors, such as those used for GUI applications.
  • Functional programming differs in that it arises from one view of the mathematics of computation. That is, functional programming, in its purest form, is a way of describing computations using constructs that have useful mathematical properties, independent of their implementations.

For example, the functional programming design methodology places great importance on constructs that are compositional. For example, in F#, you can map a function over a list of lists as:

let map2 f inp = List.map (List.map f) inp

This is a simple example of the inherent compositionality of generic functions: the expression List.map f produces a new function that can in turn be used as the argument to List.map. Understanding compositionality is the key to understanding much of what goes by the name of functional programming. For example, functional programmers aren’t interested in a lack of side effects just for the sake of it—instead, they like programs that don’t use side effects, because such programs tend to be more compositional than those that do.

Functional programming often goes further by emphasizing transformations that preserve behavior. For example, you expect to be able to make the following refactorings to your code regardless of the function f or of the values inp, x, or rest:

List.hd (x :: rest) Images  x

List.concat (List.map (List.filter f) inp) Images  List.filter f (List.concat inp)

Equations such as these can be a source of useful documentation and test cases, and in some situations, they can even be used to specify entire programs. Furthermore, good programmers routinely manipulate and optimize programs in ways that effectively assume these transformations are valid. If these transformations are not valid, it’s easy to accidentally insert bugs when you’re working with code. That said, many important transformation equations aren’t guaranteed to always be valid—they typically hold only if additional assumptions are made. As in the first example, the expression rest shouldn’t have side effects.

Transformational reasoning works well for some kinds of code and badly for others. Table 20-1 lists some of the F# and .NET constructs that are highly compositional and for which transformational reasoning tends to work well in practice.

Images

Understanding Functional Design Methodology

Functional design methodology is rooted in compositionality and reasoning. In practice, it’s largely about these steps:

  1. Deciding what values you’re interested in representing. These values may range from simple integers to more sophisticated objects, such as expression trees, from Chapter 9, or the asynchronous tasks from Chapter 13.
  2. Deciding what operations are required to build these values, extracting information from them, and combining them and transforming them.
  3. Deciding what equations and other algebraic properties should hold between these values, and assessing whether these properties hold for the implementation.

Steps 1 and 2 explain why functional programmers often prefer to define operations separately from types. As a result, functional programmers often find OO programming strange, because it emphasizes operations on single values, whereas functional programming emphasizes operations that combine values. This carries over to library implementation in functional programming, in which you often see types defined first and then modules containing operations on those types.

Because of this, one pattern that is common in the F# library is:

  • The type is defined first.
  • Then, a module defines the functions to work over the type.
  • Finally, a with augmentation adds the most common functions as members. Chapter 6 describes augmentations.

One simple example of functional programming methodology in this book appears in Chapter 12, where you saw how a representation for propositional logic is defined using a type:

type Var = string

type Prop =
    | And of Prop * Prop
    | Var of Var
    | Not of Prop
    | Exists of Var * Prop
    | False

Operations were then defined to combine and analyze values of type Prop. It wouldn’t make sense to define all of these operations as intrinsic to the Prop type, an approach often taken in OO design. In that same chapter, you saw another representation of propositional logic formulae where two logically identical formulae were normalized to the same representations. This is an example of step 3 of the functional design methodology: the process of designing a type involves specifying the equations that should hold for values of that type.

You’ve seen many examples in this book of how OO programming and functional programming can work well together. For example, F# objects are often immutable, but use OO features to group together some functionality working on the same data. Also, F# object interface types are often used as a convenient notation for collections of functions.

However, some tensions exist between functional programming and OO design methodology. For example, when you define operations independently of data (that is, the functional style), it’s simple to add a new operation, but modifying the type is more difficult. In OO programming using abstract and virtual methods, it’s easy to add a new inherited type, but adding new operations (that is, new virtual methods) is difficult.

Similarly, functional programming emphasizes simple but compositional types: for example, functions and tuples. OO programming tends to involve creating many (often large and complex) types with considerable amounts of additional metadata. These are often less compositional, but sometimes, more self-documenting.

Finally, although functional programming doesn’t provide a complete software design methodology, it’s beautiful and powerful when it works, creating constructs that can be wielded with amazing expressivity and a low bug rate. However, not all constructs in software design are amenable to compositional descriptions and implementations, and an over-reliance on pure programming can leave you bewildered and abandoned when the paradigm doesn’t offer useful solutions that scale in practice. This is the primary reason why F# is a multiparadigm language: to ensure that functional techniques can be combined with other techniques where appropriate.

images Note Some functional languages such as Haskell place strong emphasis on equational reasoning principles. In F#, equational reasoning is slightly less important; however, it still forms an essential part of understanding what functional programming brings to the arena of design methodology.

Applying the .NET Library Design Guidelines to F#

This section presents some additional recommendations for applying the .NET Library Design Guidelines to F# programming. We do this by making a series of recommendations that can be read as extensions to these guidelines.

Recommendation: Use the .NET Naming and Capitalization Conventions Where Possible

Table 20-2 summarizes the .NET guidelines for naming and capitalization in code. We’ve added our own recommendations for how these should be adjusted for some F# constructs. This table refers to the following categories of names:

  • PascalCase: LeftButton and TopRight, for example
  • camelCase: leftButton and topRight, for example
  • Verb: A verb or verb phrase; performAction or SetValue, for example
  • Noun: A noun or noun phrase; cost or ValueAfterDepreciation, for example
  • Adjective: An adjective or adjectival phrase; Comparable or Disposable, for example

In general, the .NET guidelines strongly discourage the use of abbreviations (for example, “use OnButtonClick rather than OnBtnClick”). Common abbreviations such as Async for Asynchronous are tolerated. This guideline has historically been broken by functional programming; for example, List.iter uses an abbreviation for iterate. For this reason, using abbreviations tends to be tolerated to a greater degree in F# programming, although we discourage using additional abbreviations beyond those found in existing F# libraries.

Acronyms such as XML aren’t abbreviations and are widely used in .NET libraries, although in uncapitalized form (Xml). Only well-known, widely recognized acronyms should be used.

The .NET guidelines say that casing can’t be used to avoid name collisions and that you must assume some client languages are case insensitive. For example, Visual Basic is case insensitive.

Images

We generally recommend using lowercase for variable names, unless you’re designing a library:

• let x = 1
• let now = System.DateTime.Now

We recommend using lowercase for all variable names bound in pattern matches, function definitions, and anonymous inner functions. Functions may also use uppercase:

· let add I J = I + J
• let add i j = i + j

Use uppercase when the natural convention is to do so, as in the case of matrices, proper nouns, and common abbreviations such as I for the identity function:

• let f (A: matrix) (B: matrix) = A + B
• let Monday = 1
• let I x = x

We recommend using camelCase for other values, including:

  • Ad hoc functions in scripts
  • Values making up the internal implementation of a module
  • Locally bound values in functions
• let emailMyBossTheLatestResults = ...
• let doSomething () =
       let firstResult = ...
       let secondResult = ...

Recommendation: Avoid Using Underscores in Names

In some older F# code, you see the frequent use of underscores to qualify some names. For example:

  • Suffixes such as _left and _right
  • Prefix verbs such as add_, remove_, try_, and is_, do_
  • Prefix connectives such as to_, of_, from_, and for_

We recommend avoiding this style because it clashes with .NET naming conventions.

images Note No rules are hard and fast. Some F# programmers ignore this advice and use underscores heavily, partly because functional programmers often dislike extensive capitalization. Furthermore, OCaml code uses underscores everywhere. But be aware that the style is often disliked by others who have a choice about whether to use it. It has the advantage that abbreviations can be used in identifiers without them being run together.

Recommendation: Follow the .NET Guidelines for Exceptions

The .NET Framework Design Guidelines give good advice about the use of exceptions in the context of all .NET programming. Some of these guidelines are as follows:

  • Don’t return error codes. Exceptions are the main way of reporting errors in frameworks.

    Don’t use exceptions for normal flow of control. Although this technique is often used in languages such as OCaml, it’s bug-prone and slow on .NET. Instead, consider returning a None option value to indicate failure.

  • Document all exceptions thrown by your code when a function is used incorrectly.
  • Where possible, throw existing exceptions in the System namespaces.
  • Use invalidOp and invalidArg to throw exceptions where possible

images Note Other exception-related topics covered by the .NET guidelines include advice on designing custom exceptions, wrapping exceptions, choosing exception messages, and special exceptions to avoid throwing (OutOfMemoryException, ExecutionEngineException, COMException, SEHException, StackOverflowException, NullReferenceException, AccessViolationException, and InvalidCastException).

Recommendation: Consider Using Option Values for Return Types Instead of Raising Exceptions

The .NET approach to exceptions is that they should be exceptional. That is, they should occur relatively infrequently. However, some operations (for example, searching a table) may fail frequently. F# option values are an excellent way to represent the return types of these operations.

Recommendation: Follow the .NET Guidelines for Value Types

The .NET guidelines give good guidance about when to use .NET value types (that is, structs, introduced in Chapter 6). In particular, they recommend using a struct in a public API only when the following are all true:

  • A type logically represents a single value similar to a primitive type.
  • It has an instance size smaller than 16 bytes.
  • It’s immutable.
  • It won’t have to be boxed frequently (that is, converted to/from the type System.Object).

Some programmers are much stricter and almost never use structs in public APIs.

Recommendation: Consider Using Explicit Signature Files for Your Framework

Chapter 7 describes explicit signature files. Using explicit signatures files for framework code ensures that you know the full public surface of your API and can cleanly separate public documentation from internal implementation details.

Recommendation: Consider Avoiding the Use of Implementation Inheritance for Extensibility

Chapter 6 describes implementation inheritance. In general, the .NET guidelines are agnostic with regard to the use of implementation inheritance. In F#, implementation inheritance is used more rarely than in other .NET languages. The main rationale for this is given in Chapter 6, which also presents many alternative techniques for designing and implementing OO types using F#. However, implementation inheritance is used heavily in GUI frameworks.

images Note Other OO extensibility topics discussed in the .NET guidelines include events and callbacks, virtual members, abstract types and inheritance, and limiting extensibility by sealing classes.

Recommendation: Use Properties and Methods for Attributes and Operations Essential to a Type

Here’s an example:

• type HardwareDevice with
      ...
      member ID: string
      member SupportedProtocols: seq<Protocol>

Consider using methods for the intrinsic operations essential to a type:

• type HashTable<'Key,'Value> with
      ...
      member Add           : 'Key * 'Value -> unit
      member ContainsKey   : 'Key -> bool
      member ContainsValue : 'Value -> bool

Consider using static methods to hold a Create function instead of revealing object constructors:

• type HashTable<'Key,'Value> with
       static member Create : IHashProvider<'Key> -> HashTable<'Key,'Value>

Recommendation: Avoid Revealing Concrete Data Representations Such as Records

Where possible, avoid revealing concrete representations such as records, fields, and implementation inheritance hierarchies in framework APIs.

The rationale is that one of the overriding aims of library design is to avoid revealing concrete representations of objects, for the obvious reason that you may want to change the implementation later. For example, the concrete representation of System.DateTime values isn’t revealed by the external, public API of the .NET library design. At runtime, the Common Language Runtime knows the committed implementation that will be used throughout execution. However, compiled code doesn’t pick up dependencies on the concrete representation.

Recommendation: Use Active Patterns to Hide the Implementations of Discriminated Unions

Where possible, avoid using large discriminated unions in framework APIs, especially if you expect there is a chance that the representation of information in the discriminated union will undergo revision and change. For frameworks, you should typically hide the type or use active patterns to reveal the ability to pattern match over language constructs. Chapter 9 describes active patterns.

This doesn’t apply to the use of discriminated unions internal to an assembly or to an application. Likewise, it doesn’t apply if the only likely future change is the addition of further cases, and you’re willing to require that client code be revised for these cases. Finally, active patterns can incur a performance overhead, and this should be measured and tested, although their benefits frequently outweigh this cost.

images Note Using large, volatile discriminated unions freely in APIs encourages people to use pattern-matching against these discriminated union values. This is appropriate for unions that don’t change. However, if you reveal discriminated unions indiscriminately, you may find it very hard to version your library without breaking user code.

Recommendation: Use Object Interface Types Instead of Tuples or Records of Functions

In Chapter 5, you saw various ways to represent a dictionary of operations explicitly, such as using tuples of functions or records of functions. In general, we recommend that you use object interface types for this purpose, because the syntax associated with implementing them is generally more convenient.

Recommendation: Understand When Currying Is Useful in Functional Programming APIs

Currying is the name used when functions take arguments in the iterated form—that is, when the functions can be partially applied. For example, the following function is curried:

let f x y z = x + y + z

This isn’t curried:

let f (x, y, z) = x + y + z

Here are some of our guidelines for when to use currying and when not to use it:

  • Use currying freely for rapid prototyping and scripting. Saving keystrokes can be very useful in these situations.
  • Use currying when partial application of the function is highly likely to give a useful residual function (see Chapter 3).
  • Use currying when partial application of the function is necessary to permit useful precomputation (see Chapter 8).
  • Avoid using currying in vanilla .NET APIs or APIs to be used from other .NET languages.

When using currying, place arguments in order from the least varying to the most varying. Doing so makes partial application of the function more useful and leads to more compact code. For example, List.map is curried with the function argument first because a typical program usually applies List.map to a handful of known function values but many different concrete list values. Likewise, you saw in Chapters 8 and 9 how recursive functions can be used to traverse tree structures. These traversals often carry an environment. The environment changes relatively rarely—only when you traverse the subtrees of structures that bind variables. For this reason, the environment is the first argument.

When you use currying, consider the importance of the pipelining operator; for example, place function arguments first and object arguments last.

F# also uses currying for let-bound binary operators and combinators:

• let divmod n m = ...
• let map f x = ...
• let fold f z x = ...

However, see Chapters 6 and 8 for how to define operators as static members in types, which aren’t curried.

Recommendation: Use Tuples for Return Values, Arguments, and Intermediate Values

Here is an example of using a tuple in a return type:

• val divmod : n:int -> m:int -> int * int

Some Recommended Coding Idioms

This section looks at a small number of recommendations for writing implementation code, as opposed to library designs. We don’t give many recommendations on formatting, because formatting code is relatively simple for #light indentation-aware code. We do make a couple of formatting recommendations that early readers of this book asked about.

Recommendation: Use the Standard Operators

The following operators are defined in the F# standard library and should be used wherever possible instead of defining equivalents. Using these operators tends to make code much easier to read, so we strongly recommend it. This is spelled out explicitly because other languages similar to F# don’t support all of these operators, and thus some F# programmers aren’t aware that these operators exist:

f >> g   -- forward composition
g << f   -- reverse composition
x |> f   -- forward pipeline
f <| x   -- reverse pipeline

x |> ignore   -- throwing away a value

x + y    -- overloaded addition (including string concatenation)
x - y    -- overloaded subtraction
x * y    -- overloaded multiplication
x / y    -- overloaded division
x % y    -- overloaded modulus

x <<< y  -- bitwise left shift
x >>> y  -- bitwise right shift
x ||| y  -- bitwise or, also for working with enumeration flags
x &&& y  -- bitwise and, also for working with enumeration flags
x ^^^ y  -- bitwise exclusive or, also for working with enumeration flags

x && y   -- lazy/short-circuit and
x || y   -- lazy/short-circuit or

Recommendation: Place the Pipeline Operator |> at the Start of a Line

People often ask how to format pipelines. We recommend this style:

let methods =
    System.AppDomain.CurrentDomain.GetAssemblies()
    |> List.ofArray
    |> List.map (fun assem -> assem.GetTypes())
    |> Array.concat

Recommendation: Format Object Expressions Using the member Syntax

People often ask how to format object expressions. We recommend this style:

open System

[<AbstractClass>]
type Organization() =
    abstract member Chief : string
    abstract member Underlings : string list

let thePlayers = {
    new Organization() with
        member x.Chief = "Peter Quince"
        member x.Underlings =
            ["Francis Flute"; "Robin Starveling"; "Tom Snout"; "Snug"; "Nick Bottom"]
    interface IDisposable with
        member x.Dispose() = ()}

images Note The discussion of F# design and engineering issues in Chapters 18 and 19 is necessarily limited. In particular, we haven’t covered topics such as aspect-oriented programming, design and modeling methodologies, software quality assurance, or software metrics, all of which are outside the scope of this book.

Summary

This chapter covered some of the rules you may apply to library design in F#, particularly taking into account the idioms and traditions of .NET. It also considered some of the elements of the functional programming design methodology, which offers many important and deep insights. Finally, we gave some specific suggestions for use when you’re designing .NET and F# libraries.

That concludes our tour of F#. We hope you enjoy a long and productive career working with the language.

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

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