Skip to content

Phase 0: Morphir.SDK F# Library Implementation #364

@DamianReeves

Description

@DamianReeves

Overview

Create the Morphir.SDK F# runtime library that provides Morphir SDK types and functions for generated F# code. This is Phase 0 and a critical dependency that blocks all subsequent F# backend implementation phases.

Strategic Decision: Following morphir-scala and morphir-jvm patterns, we create a runtime library rather than pure type mapping. This provides:

  • ✅ Consistent behavior across backends
  • ✅ Versioning of SDK semantics
  • ✅ Reusability from existing main-archive SDK code
  • ✅ Foundation for complex SDK types (Key, Aggregate, Rule)

Context for AI Agents

Repository: finos/morphir-dotnet
Epic: #363 F# Code Generation Backend
Phase: 0 (Foundation)
Priority: P0 (Critical - blocks #365, #366, #367, #368, #369, #370, #371, #372)

Key Resources:

Technology Stack:

  • .NET 10 / F# 9
  • Testing: TUnit (same as other projects in repo)
  • Package: NuGet (Morphir.SDK 0.4.0-alpha)

Acceptance Criteria

Project Setup

  • Create src/Morphir.SDK/Morphir.SDK.fsproj (F# class library)
  • Create tests/Morphir.SDK.Tests/Morphir.SDK.Tests.fsproj (TUnit)
  • Add both projects to Morphir.slnx solution
  • Configure NuGet package metadata (PackageId, Version, Authors, Description)
  • Set TargetFramework to net10.0

Core Modules (Phase 0.1 - Week 0.5)

Implement these modules with type aliases and function wrappers:

  • Basics.fs: Order type, comparison functions
  • Maybe.fs: Type alias for Option<'a> + helper functions
  • Result.fs: Type alias for Result<'v, 'e> + helper functions
  • List.fs: Extensions to F# List module
  • String.fs: Extensions to F# String type
  • Int.fs: Extensions to int type
  • Bool.fs: Minimal extensions (F# bool is sufficient)
  • Char.fs: Extensions to char type

Collections & Date/Time (Phase 0.2 - Week 0.5)

  • Dict.fs: Type alias for Map<'k, 'v> + extensions
  • Set.fs: Extensions to F# Set<'a>
  • Tuple.fs: Tuple helper functions
  • LocalDate.fs: Type alias for DateOnly (.NET 6+)
  • LocalTime.fs: Type alias for TimeOnly (.NET 6+)
  • Decimal.fs: Decimal extensions

Advanced Types (Phase 0.3 - Optional for alpha)

Can be deferred to beta/RC releases:

  • Instant.fs: DateTimeOffset alias
  • Month.fs: Month enumeration
  • UUID.fs: Guid alias
  • Regex.fs: Regex wrapper
  • Key.fs: Key<'a> discriminated union for lookups
  • Aggregate.fs: Aggregation functions

Testing Requirements

  • Unit tests for all modules (≥80% coverage)
  • Property-based tests using FsCheck for mathematical laws
    • Example: List.map id = id (functor identity law)
    • Example: List.map (f >> g) = List.map f >> List.map g (functor composition)
  • Compatibility tests comparing behavior with morphir-elm semantics
  • Executable examples for each module

Documentation Requirements

  • XML doc comments on all public APIs (visible in IntelliSense)
  • README.md in src/Morphir.SDK/ with:
    • Quick start guide
    • Usage examples for each module
    • Installation instructions
    • Differences from morphir-elm (if any)
  • API reference (auto-generated from XML docs)
  • Migration guide from main-archive SDK

NuGet Package

  • Build and pack: dotnet pack -c Release
  • Publish Morphir.SDK 0.4.0-alpha to NuGet.org
  • Validate package installation: dotnet add package Morphir.SDK --version 0.4.0-alpha
  • Test consumption in a sample F# project

Implementation Guidance

Step 1: Review Existing Code (main-archive)

The main-archive branch contains a previous SDK implementation:

git fetch origin main-archive
git checkout main-archive
# Review: src/Morphir.SDK.Core/

What to extract:

  • Basics.fs - Order type and comparison functions (reuse directly)
  • List.fs - Some list extensions (review for quality)
  • Test patterns and property tests

What to update:

  • Upgrade to .NET 10 / F# 9
  • Replace LightBDD with TUnit for testing
  • Ensure AOT compatibility (no reflection)

Step 2: Create Project Structure

# From repo root
dotnet new classlib -n Morphir.SDK -lang F# -o src/Morphir.SDK
dotnet new tunit -n Morphir.SDK.Tests -lang F# -o tests/Morphir.SDK.Tests

# Add to solution (uses .slnx format)
dotnet sln Morphir.slnx add src/Morphir.SDK/Morphir.SDK.fsproj
dotnet sln Morphir.slnx add tests/Morphir.SDK.Tests/Morphir.SDK.Tests.fsproj

# Add test project reference
cd tests/Morphir.SDK.Tests
dotnet add reference ../../src/Morphir.SDK/Morphir.SDK.fsproj
dotnet add package FsCheck --version 2.16.6

Step 3: Implement Core Modules

Example: Maybe.fs

namespace Morphir.SDK

/// <summary>
/// Morphir Maybe type - alias for F# Option.
/// Maintains semantic alignment with Morphir IR.
/// ADAPTED FROM: morphir-elm src/Morphir/SDK/Maybe.elm
/// </summary>
type Maybe<'a> = Option<'a>

module Maybe =
    /// <summary>Apply a function to a Maybe value</summary>
    let inline map f maybe = Option.map f maybe

    /// <summary>Chain Maybe operations (bind)</summary>
    let inline andThen f maybe = Option.bind f maybe

    /// <summary>Return the value or a default</summary>
    let inline withDefault defaultValue maybe =
        Option.defaultValue defaultValue maybe

    /// <summary>Convert Maybe to Result with error message</summary>
    let inline toResult error maybe =
        match maybe with
        | Some value -> Ok value
        | None -> Error error

Example: Basics.fs (from main-archive)

namespace Morphir.SDK

/// <summary>
/// Ordering enumeration for comparisons.
/// MIGRATED FROM: main-archive src/Morphir.SDK.Core/Basics.fs
/// </summary>
type Order =
    | LT  // Less than
    | EQ  // Equal
    | GT  // Greater than

module Basics =
    /// <summary>Compare two values</summary>
    let inline compare x y =
        match Comparer<_>.Default.Compare(x, y) with
        | n when n < 0 -> LT
        | 0 -> EQ
        | _ -> GT

    /// <summary>Minimum of two values</summary>
    let inline min x y = if x < y then x else y

    /// <summary>Maximum of two values</summary>
    let inline max x y = if x > y then x else y

Step 4: Write Tests

Example: MaybeTests.fs

module Morphir.SDK.Tests.MaybeTests

open TUnit.Core
open TUnit.Assertions
open Morphir.SDK

[<Test>]
let ``Maybe.map transforms Some values`` () =
    task {
        let result = Maybe.map ((*) 2) (Some 5)
        do! Assert.That(result).IsEqualTo(Some 10)
    }

[<Test>]
let ``Maybe.map returns None for None`` () =
    task {
        let result = Maybe.map ((*) 2) None
        do! Assert.That(result).IsEqualTo(None)
    }

[<Test>]
let ``Maybe.withDefault returns value for Some`` () =
    task {
        let result = Maybe.withDefault 0 (Some 42)
        do! Assert.That(result).IsEqualTo(42)
    }

[<Test>]
let ``Maybe.withDefault returns default for None`` () =
    task {
        let result = Maybe.withDefault 0 None
        do! Assert.That(result).IsEqualTo(0)
    }

Property-Based Tests with FsCheck

module Morphir.SDK.Tests.MaybeProperties

open FsCheck
open TUnit.Core
open Morphir.SDK

[<Test>]
let ``Maybe.map satisfies functor identity law`` () =
    task {
        let prop (x: int option) =
            Maybe.map id x = x

        let result = Check.Quick prop
        do! Assert.That(result.Succeeded).IsTrue()
    }

[<Test>]
let ``Maybe.map satisfies functor composition law`` () =
    task {
        let prop (x: int option) =
            let f = (*) 2
            let g = (+) 1
            Maybe.map (f >> g) x = (Maybe.map f >> Maybe.map g) x

        let result = Check.Quick prop
        do! Assert.That(result.Succeeded).IsTrue()
    }

Step 5: Configure NuGet Package

Edit src/Morphir.SDK/Morphir.SDK.fsproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>

    <!-- NuGet Package Metadata -->
    <PackageId>Morphir.SDK</PackageId>
    <Version>0.4.0-alpha</Version>
    <Authors>FINOS Morphir Team</Authors>
    <Company>FINOS</Company>
    <Description>F# runtime library for Morphir SDK types and functions</Description>
    <PackageTags>morphir;fsharp;sdk;functional-programming</PackageTags>
    <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
    <PackageProjectUrl>https://github.com/finos/morphir-dotnet</PackageProjectUrl>
    <RepositoryUrl>https://github.com/finos/morphir-dotnet</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>
</Project>

Step 6: Build, Test, and Publish

# Build
dotnet build src/Morphir.SDK/Morphir.SDK.fsproj

# Test (should have ≥80% coverage)
dotnet test tests/Morphir.SDK.Tests/Morphir.SDK.Tests.fsproj

# Pack
dotnet pack src/Morphir.SDK/Morphir.SDK.fsproj -c Release -o ./artifacts

# Publish to NuGet (requires API key)
dotnet nuget push ./artifacts/Morphir.SDK.0.4.0-alpha.nupkg \
  --source https://api.nuget.org/v3/index.json \
  --api-key $NUGET_API_KEY

# Test installation
mkdir /tmp/test-sdk
cd /tmp/test-sdk
dotnet new console -lang F#
dotnet add package Morphir.SDK --version 0.4.0-alpha

Migration from morphir-elm

Review the morphir-elm SDK modules for reference semantics:

morphir-elm Module F# Implementation Strategy
Maybe.elm Type alias to Option<'a>, delegate all functions to Option module
Result.elm Type alias to Result<'v, 'e>, delegate to Result module
List.elm Extensions to F# List module
Dict.elm Type alias to Map<'k, 'v> (F# has immutable maps built-in)
Set.elm Extensions to F# Set<'a>
String.elm Extensions to F# String type
Basics.elm Create Order type, comparison functions
LocalDate.elm Type alias to DateOnly (.NET 6+)
LocalTime.elm Type alias to TimeOnly (.NET 6+)

Key Principle: Maintain semantic compatibility with morphir-elm while leveraging F#/.NET's superior type system and standard library.


Success Criteria

  • Morphir.SDK NuGet package published to NuGet.org
  • All core modules (Phase 0.1 & 0.2) implemented with tests
  • Test coverage ≥ 80% (check with dotnet test --collect:"XPlat Code Coverage")
  • Package can be consumed by F# projects
  • Zero compiler warnings
  • AOT compatible (no reflection, no dynamic code generation)
  • Ready for F# backend Phase 1 (Phase 1: Foundation - Project Setup and Fabulous.AST Exploration #365) to begin

Definition of Done


Related Issues

Blocks:

Part of: #363 F# Code Generation Backend (Epic)


Questions or Blockers?

If you encounter any issues or have questions:

  1. Review the Morphir.SDK Library Plan
  2. Check the main-archive implementation
  3. Reference morphir-elm SDK for semantics
  4. Comment on this issue with specific questions

Status: Ready for Implementation
Estimated Effort: 1-2 weeks
Milestone: M0 - Foundation
Target Version: Morphir.SDK 0.4.0-alpha

Metadata

Metadata

Labels

featurefsharpF# language relatedphase-0Phase 0: Foundationpriority-p0Critical prioritysdkSDK library related

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions