fpdart

repository·main·Indexed 20 days ago

https://github.com/sandromaglione/fpdart

A functional programming library for Dart and Flutter inspired by fp-ts, cats, and dartz. It provides core functional types such as Option for missing values, Either for error handling, and IO and Task for synchronous and asynchronous computations. The library is optimized for Dart 3, fully null-safe, and includes utility types like TaskEither, Reader, and State, as well as Do notation for cleaner function chaining.

Tokens
4.9K
Snippets
11
Records
16
Agent score
71%

What's inside fpdart

  1. What is fpdart and why use it?

    main

    fpdart is a functional programming library for Dart that provides core functional types to help developers manage complexity, handle errors, and manage asynchronous computations more effectively. It is designed to be accessible to developers without prior functional programming experience through extensive documentation and real-world examples.

    Key types provided include:

    • Option: For handling missing values without using null.
    • Either: For handling errors and error messages.
    • Task: For composable asynchronous computations.

    Unlike the older dartz package, fpdart is built specifically for Dart 3, is fully null-safe, and is based on the principles of fp-ts and cats.

  2. Compare fpdart with dartz

    main

    If you are migrating from or considering dartz, fpdart offers several advantages:

    Featurefpdartdartz
    DocumentationFully documented in codeOften lacks documentation
    Dart VersionOptimized for Dart 3Originally targeted Dart 1
    Null SafetyCompletely null-safeOlder implementation
    Higher-Kinded TypesImplemented via defunctionalizationLimited
    API RichnessRicher API with more types (e.g., Reader, TaskEither)Missing some types
    CollectionsDoes not provide immutable collectionsProvides ISet, IMap, etc.

    Note: fpdart does not provide implementations for immutable collections like ISet, IMap, IHashMap, or AVLTree.

  3. Use Do notation for cleaner function chaining

    main

    The Do notation (introduced in v0.6.0) makes chaining functions easier by avoiding deeply nested flatMap calls. It allows you to write code that looks more linear and simple.

    To use it, initialize the Do() constructor and use the $ function to extract and use values inside the context.

    Pitfalls to avoid:

    • Do not throw inside the Do() constructor.
    • Do not await without executing the $ function.
    • Do not use a nested Do() constructor inside another one.
    • Do not call the $ function inside another callback within the Do() constructor.
    /// Using the Do notation
    String goShoppingDo() => Option.Do(
          ($) {
            final market = $(goToShoppingCenter().alt(goToLocalMarket));
            final amount = $(market.buyAmount());
    
            final banana = $(market.buyBanana());
            final pear = $(market.buyPear());
    
            return 'Shopping: $banana, $apple, $pear';
          },
        ).getOrElse(
          () => 'I did not find 🍌 or 🍎 or 🍐, so I did not buy anything 🤷‍♂️',
        );
  4. Use immutable collection extensions

    main

    While fpdart does not provide its own immutable collections, it provides extension methods on native Dart Iterable, List, and Map to extend them with immutable-friendly methods. It is highly recommended to use the fast_immutable_collections package alongside fpdart.

    Key extension examples:

    • head: Returns the first element (returns None() for empty lists, unlike Dart's .first which throws).
    • mapValue: Returns a new map with transformed values (immutable equivalent to updateAll).
    /// Dart: `1`
    [1, 2, 3, 4].first;
    
    /// fpdart: `Some(1)`
    [1, 2, 3, 4].head;
    
    /// Dart: Throws a [StateError] ⚠️
    [].first;
    
    /// fpdart: `None()`
    [].head;
    
    final map = {'a': '1'};
    
    /// Dart: mutable ⚠️
    map.updateAll((key, value) => value + 10);
    
    /// fpdart: immutable equivalent 🤝
    final newMap = map.mapValue((value) => value + 10);
  5. Compose utility types

    main

    You can compose the core types (Option, Either, IO, Task) to create more complex functional structures:

    • IOOption: A synchronous function (IO) that may return a missing value (Option).
    • IOEither: A synchronous function (IO) that may fail (Either).
    • TaskOption: An asynchronous function (Task) that may return a missing value (Option).
    • TaskEither: An asynchronous function (Task) that may fail (Either).
    • Reader: Provides access to a context/dependency without explicit passing.
    • ReaderTask: Combines Reader (dependency) with Task (asynchronous).
    • ReaderTaskEither: Combines Reader (dependency), Task (asynchronous), and Either (error handling).
    • State: Used to store, update, and extract state functionally.
  6. Use fpdart types for API implementations

    main

    This example demonstrates how to refactor standard Dart asynchronous code into functional code using fpdart. It specifically shows how to replace Future and null-prone operations with functional types to handle errors and missing values more safely.

    Key type replacements used in the Open Meteo API example:

    • TaskEither: Replaces Future for asynchronous requests that can fail. It encapsulates both the asynchronous nature of the call and the possibility of an error.
    • Either: Used to validate API responses, representing a result that is either a failure (Left) or a valid value (Right).
    • Option: Used to handle values that may be missing:
      • Use lookup when retrieving a value from a Map to safely handle missing keys.
      • Use head when accessing the first element of a List to safely handle empty lists.
  7. Customize iOS Launch Screen Assets

    main

    To change the launch screen image for the iOS version of the PokeAPI example, you can use one of two methods:

    1. Direct File Replacement: Replace the existing image files directly within the examples/pokeapi_functional/ios/Runner/Assets.xcassets/LaunchImage.imageset/ directory with your own assets.
    2. Xcode Interface:
      • Open the iOS workspace using: open ios/Runner.xcworkspace.
      • In the Xcode Project Navigator, navigate to Runner/Assets.xcassets.
      • Drag and drop your desired images into the asset catalog to replace the existing ones.
    open ios/Runner.xcworkspace
  8. Resolve naming conflicts using an import shim

    main

    When using fpdart alongside other libraries (like Flutter), you may encounter naming conflicts because fpdart uses common class names like State.

    To resolve this, create an 'import shim' file (e.g., functional.dart) that imports fpdart and re-exports its members using prefixes or specific names. This allows you to rename conflicting classes at the source of your project's imports.

    Steps to implement a shim:

    1. Create a new file (e.g., functional.dart).
    2. Import fpdart.
    3. Use the show or as keywords to manage exports.
    4. Re-export the desired classes, renaming them if they conflict with existing framework classes (e.g., renaming State to FpState).
    5. In your application code, import functional.dart instead of fpdart directly.
  9. Use Task for asynchronous computations

    main

    The Task type is a wrapper around an asynchronous function (Future). It is the asynchronous equivalent of IO, allowing you to compose asynchronous functions that are assumed to never fail.

    Common operations:

    • Task.of(value): Create an instance from a value.
    • Task(() async => value): Create an instance from an async function.
    • map(f): Transform the value inside.
    • flatMap(f): Chain another Task based on the current value.
    • run(): Extract the value by awaiting the async function.
    /// Create instance of [Task] from a value
    final Task<int> task = Task.of(10);
    
    /// Create instance of [Task] from an async function
    final taskRun1 = Task(() async => 10);
    final taskRun2 = Task(() => Future.value(10));
    
    /// Map [int] to [String]
    final Task<String> map = task.map((a) => '$a');
    
    /// Extract the value inside [Task] by running its async function
    final int value = await task.run();
    
    /// Chain another [Task] based on the value of the current [Task]
    final flatMap = task.flatMap((a) => Task.of(a + 10));
  10. Construct `Option` using various factory methods

    main

    The Option type represents an optional value: Some(value) if the value is present, or None if it is absent. You can construct an Option using several patterns depending on your source data:

    • Direct values: Use some(value) or Option.of(value) for present values, and none() or Option.none() for empty ones.
    • Nullable values: Use optionOf(nullableValue) or Option.fromNullable(nullableValue) to automatically convert null to None and non-null values to Some.
    • Predicates: Use option(value, predicate) or Option.fromPredicate(value, predicate) to create a Some only if the value satisfies a specific condition.
    • Error handling: Use Option.tryCatch(() => expression) to wrap a computation that might throw an exception. If an exception is thrown, it returns None.
    // Direct
    some(banana);
    none();
    Option.of(banana);
    Option.none();
    
    // From nullable
    optionOf(null); // returns None
    optionOf(banana); // returns Some(banana)
    Option.fromNullable(null);
    
    // From predicate
    option(banana, (b) => b == banana); // returns Some(banana)
    option(banana, (b) => b == apple); // returns None
    Option.fromPredicate(banana, (b) => b == apple);
    
    // From computation
    Option.tryCatch(() => banana); // returns Some(banana)
    Option.tryCatch(() => exception); // returns None