F#+ is an F# base library intended for production use, so the design of this library should be adjusted to the following guidelines:
-
Does it belong to this F#+ or to a separate library, dependent or not on F#+? Most likely if it's something technology specific it will not have a place here, whereas if it's something related to a language extension this might be a good place. There are situations that fall in the middle. Parsing is one example, at the moment we consider Json or other format parsing not part of this library but we're dealing with F# values parsing.
-
Should we add experimental, exotic code to this library? This is a trick question. On one side, this library is aimed at production code, but it should also be taken into account that we rely on users understanding what they are putting into production. Code qualities like performance, readability and scalability are not being enforced here by hiding functions. This in conjunction with the fact that the concept of "production code" is a bit vague in terms of different scenarios, for instance there are production code scenarios where performance is not a goal, nor scalability.
- General F# guidelines and naming conventions apply here.
- For new functions or operators, try to first match existing de-facto conventions in F# (ie:
foldBack
instead offoldRight
). - If there is no possibility to relate the name with an existing F# function name, we look in other languages, prefereably FP first languages.
- Generic functions should be named the same as module specific ones, ie:
map
, notfmap
.
- In contrast to F# core, we don't apply the principle of encouraging good practices by hiding away stuff, namely useful functions that are evil at the same time, ie:
curry
. The end user is responsible for good use of the functions. - Generic functions should ideally have a corresponding non-generic counterparts, ie
map3
==>Option.map3
-Result.map3
. - Generic functions should ideally relate to rules, not necessarily Category Theory.
- Remember to design related functions in a consistent way across different types, even if that might lead to a function that is not as efficient. Performance is a priority but we don't assure performance by hiding away stuff.
- Functions should be as referentially transparent as practicable, namely things like culture should be either neutral or explicit.
- Generic operators and functions should go into the
FSharpPlus.Operators
module unless they conflict with each other or with existing F# functionality, in which case they should go in a module that requires explicit opening. - New types/collections should be added in a separate file, ideally having a module with let-bound functions as it's in general more F# idiomatic. Some static members, required to satistfy static constraints for generic abstractions should be added when the type implements that abstraction, but if it exposes a let-bound function with the same functionality, the static member can be hidden from the tooling with an attribute.
- Unary/binary operators: Adding an operator to the auto opened
FSharpPlus.Operators
module should be careful considered and justified since there are a limited number of operators and because of the increasing chance to clash with another library's operator. Adding operators to manually opened modules is the alternative.
-
Default overloads (fallback mechanism) are and should be available for the end user of the library but internally we should avoid relying on them. It's better to duplicate code or eventually to factor out some portions than relying on default implementations. This provides less complicated type inference (more control over the code) and faster compile time. At the same time this decision doesn't affect the end user as long as we don't forget any overload (which would be considered a bug) and in most cases it provides an additional benefit of improving performance, by having specialized functions.
-
Care must be taken when using interfaces, there are scenarios where it is desired to provide overloads for an explicit interface (ie:
seq<_>
) but not to implicit types implementing that interface, which will otherwise become a default overload. As an example an explicitseq<_>
instance has an overload for>>=
but it's not good that all types that implementseq<_>
default to that overload (not allIEnumerables<_>
are monads), while for methods likeskip
it's just fine to haveseq<_>
as a default. -
New abstractions should consider using well known operators as static members, especially if a generic global operator exists for it. As an example
>>=
exists as a global operator, so it's better to use it - also as convention for the monadic bind operation by requiring a>>=
(instead of a namedbind
) static member. This has 2 advantages: It allows the operator on the type to be used without this library as a non-generic one, and it also increases the chance of finding 3rd party types that have that operator already defined.