F#+ (FSharpPlus) Documentation

repository·master·Indexed 21 days ago

https://github.com/fsprojects/fsharpplus

A functional programming enhancement library for F# that leverages generic programming to reduce boilerplate and provide advanced functional abstractions. It extends FSharp.Core and the BCL with non-intrusive patterns, including the Control namespace's Invokables for representing generic functions and the FSharpPlus.TypeLevel project for debugging type providers.

Tokens
1.5K
Snippets
1
Records
7
Agent score
76%

What's inside F#+

  1. Overview of F#+

    master
    F#+ (FSharpPlus) is a base library designed to enhance F# by providing advanced functional programming capabilities. It uses generic programming techniques to reduce boilerplate code while maintaining compatibility with existing F# patterns through consistent naming conventions and signatures. It is designed to 'enhance' rather than 'replace' standard F# patterns, allowing developers to adopt it incrementally.
  2. How overloaded static members and interfaces work in F#+

    master

    F#+ uses static members and interface implementations to provide a seamless experience for end-users, particularly regarding generic abstractions:

    • Default Overloads: The library provides default overloads (fallback mechanisms) to ensure end-users can use functions easily. While the library may internally duplicate code to improve compile times and type inference, the end-user sees a complete set of specialized and fallback overloads.
    • Monadic Bind Convention: When creating new abstractions, F#+ follows the convention of using well-known operators as static members. For example, instead of a named bind method, types implement the >>= static member. This allows the operator to be used even without the F#+ library and increases compatibility with other 3rd-party types.
    • Interface Precision: The library is careful with interface overloads. It distinguishes between providing an overload for an explicit type (like seq<_>) versus all types implementing an interface (like IEnumerable<_>) to avoid incorrect default behaviors (e.g., not treating every IEnumerable as a Monad).
  3. How F#+ handles generic functions and modules

    master

    F#+ follows specific patterns for organizing generic functions and their type-specific counterparts to ensure consistency and ease of use:

    • Generic vs. Specific Naming: Generic functions use standard F# naming conventions (e.g., map instead of fmap). For every generic function, there should ideally be non-generic counterparts in specific modules (e.g., Option.map3 and Result.map3 corresponding to a generic map3).
    • Module Organization:
      • Generic operators and functions are placed in the FSharpPlus.Operators module for easy access.
      • If a function or operator might conflict with existing F# functionality or other libraries, it is placed in a module that requires an explicit open statement.
      • New types or collections are typically defined in their own files, using modules with let-bound functions to remain idiomatic.
    • Operator Usage: Unary and binary operators are kept to a minimum in the auto-opened FSharpPlus.Operators module to prevent namespace clashes. Users can also find these operators in manually opened modules.
  4. Understand F#+ design philosophy and extension patterns

    master

    F#+ is designed to be a non-intrusive, non-opinionated extension of FSharp.Core and the BCL. When using the library, keep the following principles in mind:

    • Non-intrusive: F#+ avoids changing existing F# language or FSharp.Core functionality. New functionality is typically isolated in specific modules/namespaces that require an open statement.
    • Consistency over naming collisions: Because F# lacks type classes, F#+ uses creative naming to maintain consistency while avoiding collisions with built-in functions. For example:
      • map is used for generic mapping, but for Dictionaries, use mapValues.
      • minimum is used for collections to avoid collision with the built-in min (which compares two values).
    • Zip and Lift behaviors:
      • map2 and zip in collections typically act pairwise.
      • lift2 is provided for applicative behavior (cross-product), which is the generic counterpart for non-collection types.
      • F#+ zip functions are designed to be safer than F# core's zip, often matching the behavior of .zipShortest to avoid errors when collection lengths differ.
  5. How Invokables and Control abstractions work

    master

    The Control namespace uses a pattern called Invokables to represent generic functions and abstractions (similar to type classes or traits in other languages).

    Mental Model

    Since F# (targeting CIL) does not have first-class support for Higher Kinds, F#+ uses generic type parameters as labels to communicate intent. For example, a signature like ('T -'U) -> 'Functor<'T> -> 'Functor<'U> uses the label Functor to indicate that the types must satisfy the Functor abstraction.

    Implementation Details

    An abstraction (like Monoid) is implemented via concrete types (Invokables) such as Plus and Zero. These types provide:

    1. Concrete overloads: Static methods for specific BCL or FSharp.Core types (e.g., string concatenation, list concatenation).
    2. An inline invokable method: Uses SRTP (Static Resolution via Type Parameters) to dispatch to the correct overload.
    3. Default implementations: Provided as inline to reduce boilerplate for the end-user. For example, if you implement Bind and Return for a Monad, F#+ can provide Join automatically via default implementations.

    Usage Tip

    Invokers are designed to have signatures similar to the generic functions they represent. You can often use an Invoker in place of a function to write more generic code by passing a type instead of a specific function.

  6. Use FSharpPlus.TypeLevel for debugging type providers

    master

    The FSharpPlus.TypeLevel project is designed to speed up the development cycle when debugging type providers. Instead of rebuilding a large project every time a change is made, you can use this small, dedicated project to isolate and test type-level logic.

    To run type-level tests, you must compile the project with the compiler constant TYPELEVEL_DEBUG enabled. Note that these tests are only active during development and are excluded from the final package.