From 1ac2adfea10ed648ab5bfb1697a021a03de90714 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 5 Oct 2026 02:12:28 +0200 Subject: [PATCH] fix(rxquery)!: stop seeding `sumBy` with `Unchecked.defaultof` The fold started from `Unchecked.defaultof`, so a reference type that defines `(+)` received `null` as the first operand and faulted. The first element now seeds the sum; an empty source still yields the default value, so numeric and `TimeSpan` sums keep returning zero. BREAKING CHANGE: `sumBy` adds values of the projected type, so it requires `(+) : 'Value * 'Value -> 'Value`. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ src/FSharp.Control.R3/Observable.fs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df56a03..ec0ff78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Breaking:** `AwaitOperationConfiguration` cases are prefixed with `Await` (`AwaitSequential`, `AwaitParallel 4`, ...) so they no longer collide with `System.Threading.Tasks.Parallel` +- **Breaking:** `rxquery` `sumBy` adds values of the projected type: it requires `(+) : 'Value * 'Value -> 'Value` - `mapAsync` and `iterAsync` validate the concurrency limit of the options when they are called ### Fixed - `rxquery` emitted its elements on the thread pool, out of order and after the source had moved on; `yield` and `zero` are now synchronous +- `rxquery` `sumBy` passed `null` to the `(+)` of reference types ## [0.3.1] - 2026-01-28 diff --git a/src/FSharp.Control.R3/Observable.fs b/src/FSharp.Control.R3/Observable.fs index 984f385..8fb14bd 100644 --- a/src/FSharp.Control.R3/Observable.fs +++ b/src/FSharp.Control.R3/Observable.fs @@ -163,10 +163,20 @@ module Builders = member _.MinBy (s : Observable<'a>, [] valueSelector : 'a -> 'b) = s.MinByAsync (new Func<'a, 'b> (valueSelector)) [] - member inline _.SumBy (s : Observable<_>, [] valueSelector : _ -> _) = + member inline _.SumBy (s : Observable<_>, [] valueSelector : _ -> 'Value) = + // The first element seeds the sum: seeding with Unchecked.defaultof passed null to the (+) of reference types, + // while requiring a Zero member would reject types such as TimeSpan whose zero is a field s |> _.Select(valueSelector) - |> _.AggregateAsync(Unchecked.defaultof<_>, new Func<_, _, _> (fun a b -> a + b)) + |> _.AggregateAsync( + ValueNone, + new Func<_, _, _> (fun sum value -> + match sum with + | ValueNone -> ValueSome value + | ValueSome sum -> ValueSome (sum + value) + ), + new Func<_, _> (ValueOption.defaultValue Unchecked.defaultof<'Value>) + ) [] member _.Zip (s1 : Observable<_>, s2 : Observable<_>, [] resultSelector : _ -> _) =