Recommended Guidelines for F# Projects, Packages and Namespaces
Naming, Engineering and General Advice
Author
Don Syme, with advice from various contributors to F# Core Engineering discussions.
Originally drafted September 2014, with subsequent minor updates.
Contents
Introduction
One of the historic goals of the “F# Core Engineering” group is to help ensure best engineering practices in public-facing F# components and packages.
In this document we capture some recommended guidelines for naming and engineering for F# projects, packages and namespaces. This is based on how things stood in 2014, and we have continued to update these with minor modifications. We particularly focus on open projects such as those in the F# Community Projects list or packages in NuGet for an “FSharp” search.
These are recommendations - they are not hard-and-fast rules, and are offered for discussion amongst F# developers. If you would like to discuss these guidelines or suggest modifications, please edit this file on GitHub and submit a pull request.
In discussions, there has been agreement on the following:
-
FSharp.* projects should be high-quality or trending rapidly in that direction. Ideally any packages and code under the FSharp.* namespace should be of sufficient quality to be considered ready for production use by people in the wider F# community.
-
We need to avoid pollution of the FSharp.* namespace, particularly by unfinished projects.
-
We need to encourage some degree of incubation and experimentation using FSharp.* project, package and namespace names. Important F# components such as FSharp.Data have developed in this way.
-
Packages and projects using FSharp.* naming should generally be cross-platform, unless explicitly qualified by some platform-specific moniker (e.g. “Windows”, “Android”, “Azure”, “Gtk” or “Linux”). Code in the FSharp.* namespace should build, run and pass tests across multiple platforms.
-
We want to encourage and scale the positive and productive spirit with which the F# community operates by sharing information about how to make successful, long-lasting and broad-reach components in a collaborative way.
In light of these, we cover some specific recommendations with regard to naming, quality and general advice below. They are written to augment the existing F# Component Design Guidelines. Some of these recommendations may eventually be added to those guidelines.
Naming Guidelines
Guideline: Do make the purpose of your project or package clear in its name
When starting a project, it can be all-to-easy to choose a name that is too general, for example “FSharp.Helpers”. As you clarify what your project is about, make sure you adjust the name of your project accordingly.
For example, a project such as “FSharp.Cloud” is unclear in purpose. Is this for uploading music to Apple’s iCloud? Or for Amazon AWS? Or Microsoft Azure? Instead, use a name such as “FSharp.Azure.Scripting” or “FSharp.Amazon.Scripting” if your package is in fact a scripting library for a particularly cloud infrastructure provide.
Alternatively, you may end up dropping the use of an “FSharp” name for your project or package. see below, and choosing a more product-like name.
Guideline: Consider an FSharp.*
name for things intended primarily for F# developers
Many libraries, package or tools are designed explicitly for the use of F# programmers. These may be valid candidates for an “FSharp.*” name, if other criteria are met.
For example, “FSharp.Data” or “FSharp.Data.SqlCommandProvider” are in this category.
Guideline: Avoid an FSharp.*
name for things not intended exclusively for F# developers
Some projects, packages and tools use F# as an implementation or scripting language but are intended for broader use. If your audience is larger than the worldwide community of F# developers, then strongly consider using a name which doesn’t mention F#.
For example,
-
FAKE is a general build tool which happens to use F# as a scripting language. It has achieved widespread adoption, partly because the name is well chosen to appeal to a broad audience.
-
Deedle package is branded as “An exploratory data library for .NET” and includes a C#-facing API (implemented in F#). It was successfully renamed from “FSharp.Data.DataFrame” to broaden its reach, and at the time of writing a Google search for “C# DataFrame” brings Deedle as the first hit.
Likewise, some projects are product-like or are actual products, often with reach beyond the worldwide community of F# developers or enabling new development experiences. These should normally use a name that doesn’t begin with “FSharp”, though may mention F# if useful. Examples are MathDotNet, Akka.NET and WebSharper. Some of these are F#-specific, some are general .NET tools with an F# angle, some are products, some are open source, but all benefit from their own branding.
Guideline: Avoid using two word “FSharp.XYZ” names for projects and packages
We recommend that new public-facing projects do not use a two-word qualified FSharp.* name except in extremely rare circumstances.
For example, projects with names such as FSharp.Numerics, FSharp.Distribution, FSharp.BigData or FSharp.Web are named too broadly. We are uncomfortable with seeing anyone use unqualified two-word FSharp.* names for projects except in rare circumstances such as FSharp.Data.
If your project already uses such a name, then strongly consider renaming it. One exception is FSharp.Data which is sufficiently broad and canonical to use a two-word name.
Guideline: Do use existing second-level namespaces where appropriate
.NET and F# components are placed in second-level namespaces such as “FSharp.Control”, “FSharp.Data”, “FSharp.Net”, and so on.
Use the following prefixes where possible, rather than inventing new ones:
-
FSharp.Control
: Functionality related to control flow, such as asynchronous programming, message passing, event-based programming, reactive programming, and similar -
FSharp.Data
: Types related to data access, data schema, and similar -
FSharp.Text
: Text processing, formating, printing, or similar functionality -
FSharp.Azure
: Types related to cloud computing on the Azure plattform. -
FSharp.AWS
: Types related to cloud computing on the AWS plattform. -
FSharp.Compiler
: Functionality relating to compilation of F# -
FSharp.Core
: Use sparingly. Typically required for helper types required by incubation of compiler features
For example, a library like “FSharp.Actor” might be better renamed to “FSharp.Control.Actor”. Similarly, “FSharp.Reactive” is better renamed to “FSharp.Control.Reactive”. Likewise, a type provider for a data source or schema format XYZ should normally be placed in “FSharp.Data”, e.g. “FSharp.Data.XYZ”.
Guideline: Consider using “Experimental” in project/package names in early stages
We encourage incubation of candidates for the FSharp.* namespace. One way to do this is to allow some moderation, e.g. allow free use of names like
- FSharp.Data.Experimental.XYZ
- FSharp.Linq.Experimental.XYZ
- FSharp.Core.Experimental.XYZ
The use of “Experimental” is useful and recommended in early stages. Renaming projects and packages is fairly well supported by GitHub and existing package managers, combined with global-search-and-replace.
Guideline: Clearly label project status prior to release
Projects, packages and tools using the FSharp.* top level namespace should clearly and prominently label their status in all documentation during the project’s early phases of development. Projects should use version numbers below 1.0 to denote pre-release status, and clearly label their status as “Alpha” when the project is still lacking features or functionality required for widespread usage, and “Beta” prior to adequate usage and testing to be considered a mature package.
Guideline: Do seek review of your project, package and namespace names
The F# community love to help and give good advice. Seek advice via the #fsharp tag on Twitter or other community forums.
Transpiling Guidelines
Some F# libraries are candidates for cross-compilation using F# transpiler tools such as Fable and WebSharper. Other transpilers for other targets may also be developed over time.
The following are draft guidelines for adding support for transpilation, based on information available in 2019.
-
Consider supporting transpilation by one or more alternative toolchains. It is not required or obligatory.
-
Do ensure that the qualities of the library are not diminished significantly, e.g. API design, documentation and performance.
-
Do be positive to contributions which add support and testing for other transpilers. However there is no obligation to support any particular transpiler.
-
Do add CI testing for the transpilation.
-
Do use feature-specific switches
#if NO_FOO_FEATURE
rather than#if MYTRANSPILER
. -
Do assess the quality of the transpilation tool, and the degree of support for language and library.
-
Consider using an appropriate different namespace, e.g.
Fox.Collections.Rapid
for transpiled version of the (hypothetical)FSharp.Collections.Rapid
library. -
Consider the maintenance burden of the necessary
#if
when making your decisions, including the long-term maintenance by other contributors.
As an example, as of October 2019 the dotnet/fsharp
repository has decided not to support transpilation for the FSharp.Compiler.Service
library component. However, some transpilers maintain separate forks of that repository.
Engineering Guidelines
Guideline: Apply good software engineering practice before publicizing your project
Before you publicize an open, public-facing project, it pays to get all the basics in place to allow people to collaborate with you. These are, minimally:
-
Naming. Get the naming of your project right. Make its purpose clear.
-
README and Road Map. Add a clear, simple README to your project. Make its purpose clear. Add a road map too.
-
Packaging. Make a package for your library, component or tool. Often this will be a NuGet package, but if it is some other kind of tool then make and publish the appropriate package. For example, the Emacs mode for F# is published as a MELPA package. Fable is published as an NPM package. Document how the package gets published.
-
Testing. Ensure your project has tests that build, run and pass out-of-the-box.
-
Documentation. Ensure your project has good documentation. If on GitHub, consider publishing your documentation via GitHub pages. Consider using documentation generation tools and templates used by the F# Project Scaffolding
-
Tutorials. Ensure your project has tutorials as part of its documentation. Templates and examples are available in the F# Project Scaffolding
-
Videos. If you’ve given a talk on your project, add a link to the video. It’s a great way to introduce yourself to potential contributors.
-
Continuous Integration. Add continuous integration build and testing to your project, for example using AppVeyor or Travis. For AppVeyor, this is done by adding
appveyor.yml
(example) and registering your project. For Travis, you add a.travis.yml
(example) and register the project. Travis provides OSX and Linux machines - OSX machines are used if the language is set to “objective-c”. -
Reach a Known State. Document all known issues. Consider labeling some of them as “up-for-grabs”. Don’t leave undocumented minefields in your package or tool.
-
Cross-platform. Set up CI testing on both Windows and Linux/OSX.
-
.NET Standard if possible. If writing a library, make your component a .NET Standard 2.0 component where possible.
If you don’t do these things, people are unlikely to want to collaborate with you on your project.
Guideline: Evolving code and new features should be carried out in a PR or a feature branch, and merged in only when ready
Do not develop features in your “master” branch or published NuGet packages. Instead, use a PR, a feature branch, or a fork, or a new project, or a component marked Experimental.
General Guidelines
Guideline: Consider putting open incubation projects in The F# Community Incubation Space
The F# Core Engineering group manages The F# Community Incubation Space for community incubation projects. To get your project added or removed from this space, add an issue to the admin section of this space.
Guideline: Do recruit additional contributors and maintainers of your project.
Please contribute fixes and improvement to the project scaffolding to make it easier for others to use. If you add your project to The F# Community Incubation Space then the owners of the space will also automatically be able to perform some maintenance tasks on your repository, including accepting pull requests or curating issues.
Guideline: Do use a nice logo for your package or tool.
Many F# community projects include nice logos. If necessary, ask for help to design a new logo in a matching style. For example, see the FSharp.Data library.
Guideline: Do add your project to the F# Community Projects list
We recommend you consider adding your project the F# Community Projects list. You can do this by submitting a pull request on GitHub as described on the page.
Guideline: Do consider creating a Twitter account for your package or tool
A Twitter account for a project can increase its visibility, especially in early stages, and set it on a track to be an independent, long-lived technology with multiple contributors. For example, see the FSharp.Data or F# for Open Editors on Twitter.
If you tweet, use the #fsharp tag on Twitter.
Guideline: Consider making your project “independent” of you or your company
Projects, packages and tools are often branded in a way to make them less directly dependent on a particular person or company, relying on people who fill roles rather than being permanently tied to an individual.
For example, the Akka.NET project started life as a project called “Pigeon” in the GitHub account of one major contributor. It is now branded as an “independent” project.
Branding your project as independent (or indirectly-dependent) can increase confidence, help attract additional contributors, give new opportunities (for example to start a consulting company around the technology), and above all can help with transitions as people move on and off the project. Effectively, a layer of indirection is placed between the projects and the contributors. Behind the scenes, the same contributors may be involved, especially initially, but the long term advantages are very large.
This approach is frequently used for open source packages and tools initially started by individuals. Note that detaching yourself from your project in this way can be both challenging and liberating.
This approach can also be used for products or open-source projects where companies are major contributors. This can be a difficult choice: the company may (or may not) want its name attached to the open-source project in a strong way.
Guidelines for the cultural processing of strings in .NET generally, and F# specifically
Guideline: Explicitly state cultural intent, where applicable, in the conversion and comparison of strings
Default .NET methods for string conversions of number types and DateTimes (value-to-string; string-to-value) have a notable behaviour characteristic: they are ‘culturally sensitive’. The same applies to the default methods for upper and lower casing of strings (string-to-string conversions). This means that commonly-used methods such as:-
Double.ToString()
Double.Parse(s)
Double.TryParse(s, out d)
DateTime.ToString(format)
String.ToUpper()
/.ToLower()
- and many more
use the rules of the current thread’s current culture to perform their conversions. Since this culture can vary from machine to machine (and is very likely to do so when machines are located in different countries), use of these method overloads gives rise to many potential problems, including but not limited to:-
- failure to parse a string representing a floating-point number or a date (an exception is thrown)
- parsing a string that represents a floating-point number or a date to an incorrect value
- security issues
- subtle bugs
Similarly, default .NET methods that involve the comparison of strings (for equality testing and ordering) have similar problems. Methods such as:-
String.StartsWith(s)
/.EndsWith(s)
String.IndexOf(s)
String.Contains(s)
String.Compare(s, s1)
String.Equals(s)
- and more
either use the current thread’s current culture for a culturally sensitive comparison, or are fixed to use an Ordinal (non-cultural, case-sensitive) comparison. The former can give rise to bugs and poor security decisions where the comparison should have been performed non-culturally. The latter (Ordinal only) may be too restrictive: e.g. the caller may need the comparison to be case-insensitive.
Since .NET 2.0, released in 2005, all the tools have been in place (in terms of enums, method overloads, .NET types etc.) to allow .NET developers to process strings correctly at all times - especially with respect to performing culturally-varying vs fixed-culture conversions of strings, and cultural vs non-cultural comparisons of strings. These tools should be used at all times. To do so, a .NET developer should, in general, always use those method overloads that allow the caller to explicitly state his/her cultural intent.
An example, for conversions:-
open System.Globalization
let floatVal = 2001.345
let str1 = floatVal.ToString(CultureInfo.InvariantCulture) // 'string floatVal' does the same thing
let str2 = floatVal.ToString(CultureInfo.CurrentCulture)
let str3 = floatVal.ToString(new CultureInfo("en-US"))
An example, for comparisons:-
open System
let s1 = "<some string>"
let s2 = "<some other string>"
let eq1 = String.Equals(s1, s2, StringComparison.Ordinal) // 's1 = s2' does the same thing
let eq2 = String.Equals(s1, s2, StringComparison.OrdinalIgnoreCase)
Always explicitly stating cultural intent should ensure correct operation of the code, and - equally as important - shows that the developer is aware of the issues and has addressed them (calling Double.ToString()
, even when use of the current thread’s current culture is desired, gives no such re-assurance).
F# developers should be aware of the behaviour of F#’s core operators with respect to cultural sensitivity:-
- F# and
FSharp.Core
functions always use Ordinal (non-cultural, case-sensitive) string comparison by default for “compare”, “=”, “<>”, “<”, “>”, Seq.sort, Seq.sortBy, HashIdentity.Structural, ComparisonIdentity.Structural and so on - core operators
byte
,decimal
,float
,float32
,int
,int16
,int32
,int64
,sbyte
,uint16
,unit32
,uint64
convert strings using the invariant culture.
F# developers can and should rely on the behaviour of these operators, and the need to be explicit about Invariant Culture conversions and Ordinal comparisons is removed when using these operators.
Guideline: Always follow the long-established Best Practices for using strings in the .NET Framework
See Microsoft’s official Best Practices for Using Strings in the .NET Framework, as well as the specific guidance for F# developers that is given in this document.
Guideline: F# components should be tested to ensure that they behave correctly when operating under different cultures
Unit tests typically run in-process and on the same thread as the code-under-test. Therefore culture testing may be as simple as modifying the current thread’s current culture in the Act (or Setup) phase of the test; e.g.
// change the current thread's current culture to French - France
System.Threading.Thread.CurrentThread.CurrentCulture <- CultureInfo.CreateSpecificCulture("fr-FR")
Integration tests may need to go to greater lengths to manipulate the code-under-test’s culture, including modification of the test machine’s Region and Language control panel settings.
Guideline: Public-facing F# libraries should make sensible decisions about cultural string processing, and should strive for consistency
A public-facing library that might reasonably be expected to run on machines of different cultures should ensure that all of the code is written to process strings correctly, with respect to cultural / non-cultural behaviour. Contributors should be encouraged to follow rules and guidelines that are established at the outset, so that the code is consistent in this regard.
If library-specific functions are used to wrap .NET string conversion and comparison methods then:-
- these functions should be used by all contributors
- the functions should encourage callers to be explicit about cultural intent, and not hide such details
- the functions should wrap only those .NET method overloads that allow callers to be explicit about cultural intent
If library-specific operators are used to wrap .NET string conversion and comparison methods then their cultural operation should be explicitly documented and understood by all contributors.
On the whole it’s best to err on the side of caution and not introduce such library-specific functions and operators, since contributors may contribute to multiple libraries.
Where libraries introduce new data structures + associated operators and functions that use string-based identifiers (e.g. keys), the default string comparison for these identifiers should be Ordinal (non-cultural, case-sensitive). This should be noted in a library’s documentation.
About Us
The F# Core Engineering Group is a technical group associated with The F# Software Foundation.
We manage the cross-platform and open-source F# Compiler and Components repositories and the F# Community Project Incubation Space.
Visit our website and please continue all the great work on core F# engineering!
First Published: 19 September 2014
The F# Core Engineering group