Skip to content

stdlib: give Option and Result their combining half - #48

Merged
simontreanor merged 1 commit into
mainfrom
feat/stdlib-option-result-sweep
Jul 31, 2026
Merged

stdlib: give Option and Result their combining half#48
simontreanor merged 1 commit into
mainfrom
feat/stdlib-option-result-sweep

Conversation

@simontreanor

Copy link
Copy Markdown
Owner

Dogfooding finding #5, PR 5 of 5 (see #34) — the last sweep. Branched from main, independent of #46 (Map/Set) and #47 (String); all three can merge in any order.

Both types had map/bind/withDefault and the predicates, which handle one value at a time. Neither could combine two, or cross into another type without a match. Ten members: map2 orElse flatten iter toList exists for Option; map2 orElse iter toList for Result.

map2 is the one that earns its place. Two independent lookups that both have to succeed otherwise means nesting two matches:

Option.map2 (fun row col -> Square { row = row, col = col })
            (Map.tryFind r rows)
            (Map.tryFind c cols)

Option.map2 is Some only when both are. Result.map2 is Ok only when both are, and otherwise reports the first Error, so the earliest failure surfaces. There's a lowering test asserting a is checked before b, rather than trusting the reading.

orElse takes the fallback first, matching withDefault, so it reads as primary |> Option.orElse fallback under the pipe. Worth review: getting the order backwards would be silently wrong rather than a type error, since both arguments have the same type. That's why the argument order is stated on the member's own documentation line, not just here.

One test premise of mine was wrong, and the correction is instructive. I expected Result.map2 f (Ok 1) (Error 2) to be a type error, but it checks: Ok 1 leaves its error type free, so it unifies with anything. Two concrete error types are what conflict, and the test pins that instead.

Tests: 7 typecheck cases (combining, the orElse order, one-level flatten, the shared error type, effect-carrying iter), 1 lowering assertion (first-error ordering), and 2 end-to-end programs covering all ten members. Full suite, clippy and fmt clean.

With this the sweep is complete: 94 members across List, Seq, Set, Map, String, Option, Result, plus fst/snd, all documented with their complexity. Next is the FSharp.Core audit pass, then finding #7's import diagnostic.

Both had map, bind, withDefault and the predicates, which handle one value
at a time. Neither could combine two, or cross into another type without a
match. That is ten members: map2, orElse, flatten, iter, toList and exists
for Option; map2, orElse, iter and toList for Result.

map2 is the one that earns its place. Two independent lookups that both
have to succeed otherwise means nesting two matches, and this is the shape
that removes them. Option.map2 is Some only when both are; Result.map2 is
Ok only when both are and otherwise reports the *first* Error, so the
earliest failure is the one that surfaces. A lowering test pins that
ordering rather than trusting the reading.

orElse takes the fallback first, matching withDefault, so it reads as
`primary |> Option.orElse fallback` under the pipe. Getting that backwards
would be silently wrong rather than a type error, since both arguments have
the same type, which is why the argument order is documented on the member
itself.

One test premise of mine was wrong and is worth recording: `Result.map2 f
(Ok 1) (Error 2)` type-checks, because `Ok 1` leaves its error type free.
Two *concrete* error types are what conflict, so the test pins that instead.

Every member of both modules is O(1), which is why none of them state a
cost: they hold one value, so there is nothing to traverse.
@simontreanor
simontreanor force-pushed the feat/stdlib-option-result-sweep branch from 1a14f43 to 9f2687c Compare July 31, 2026 16:41
@simontreanor
simontreanor merged commit 54d157e into main Jul 31, 2026
11 checks passed
@simontreanor
simontreanor deleted the feat/stdlib-option-result-sweep branch July 31, 2026 16:45
@simontreanor simontreanor mentioned this pull request Jul 31, 2026
simontreanor added a commit that referenced this pull request Jul 31, 2026
Two dogfooding reports from real programs, and the standard-library sweep
they triggered.

Language:

* a `type` declaration can name an imported type, bare or module-qualified
  (#36) — the one gap that changed a program's architecture rather than its
  phrasing, forcing two modules into one file
* field access resolves from the base's type when it is known, so two
  records may share a field name without prefixes (#37)
* parameters destructure: tuples (#38), records (#40), and `_`
* a direct self tail call lowers to a loop, so an interactive turn loop no
  longer walks the stack (#39, #41)

Standard library — about 115 new members, taking every module to the F#
core set: List (#42), Seq (#44), Set and Map (#46), String (#47), Option
and Result (#48), then a member-by-member FSharp.Core audit (#51). Every
built-in member now carries a one-line description and its complexity in
hover and completion (#43, #49), enforced by tests.

Fixes:

* `pyfun run` on a single file gives the program its own stdin, so an
  interactive program is runnable by the command whose job is running
  programs (#35)
* a partially applied lambda closes over its argument instead of being
  wrapped, so `List.map ((+) 2)` emits `lambda b: 2 + b` (#52)
* every multi-argument callback's scheme put the effect variable on the
  wrong arrows, so `List.fold` could never accept an effectful folder (#51)
* `Seq.empty` lowered to a bare `iter()`, a TypeError (#51)

One source-incompatible change, which is why this is 0.4.0 and not 0.3.1:
a dotted `extern` target whose module prefix cannot be decided from the
text is now a compile error naming the `extern import` to add (#50).
`sys.stdout.flush` used to emit `import sys.stdout` and fail at runtime;
declaring `extern import sys` fixes it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant