Skip to content

Commit f28beea

Browse files
xperiandriclaude
andcommitted
Adopted voption and span guidance from the dotnet/fsharp instructions
Bring over the option/voption and string slice rules from the F# compiler's `FSharp.instructions.md`: unwrap an `option` from an API directly, convert to `option` once and last, pipe the value in before `_.Member` shorthand, pass an explicit `StringComparison` (also on span overloads), inspect slices through `ReadOnlySpan<char>` and store them as `ReadOnlyMemory<char>`. GraphQL names are called out as case-sensitive, so `OrdinalIgnoreCase` must never be used to compare them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 862cb8e commit f28beea

1 file changed

Lines changed: 8 additions & 2 deletions

File tree

‎.github/copilot-instructions.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ Agents discover servers from #file:'.mcp.json'; these are only hints on when to
8989

9090
### Values and Collections
9191

92-
* Prefer `voption` over `option`.
92+
* Prefer `voption` (`ValueSome`/`ValueNone`) over `option`. Fields, members, parameters and values shared between threads included: none of them is a reason to pick `option`. Exception: when an API hands you `'T option` and has no `voption` counterpart, unwrap it with `Option.defaultValue`/`Option.defaultWith` directly – do not insert `ValueOption.ofOption` just to switch modules.
93+
* The mirror case, an API that *takes* `'T option` (an optional argument `?name = …`, a field typed `'T option`): stay in `ValueOption` through the whole chain and convert once, last – `x |> ValueOption.bind _.Value |> ValueOption.toOption`, never `x |> ValueOption.toOption |> Option.bind _.Value`.
9394
* Prefer `struct ('T1 * 'T2)` over reference tuples, and anonymous struct records (`struct {| ... |}`) over tuples for return types of public functions and methods.
9495
* Never group with `Seq.groupBy` – use `ToLookup` from `System.Linq`. It groups once into an `ILookup<'Key, 'T>` instead of re-grouping on every enumeration and does not allocate a tuple per group. Pass a lambda (`xs.ToLookup (fun x -> keyOf x)`), not a bare function value.
9596
* When casting sequence items use `Seq.cast<TargetType>` instead of `Seq.map (fun item -> item :> TargetType)`.
@@ -98,9 +99,14 @@ Agents discover servers from #file:'.mcp.json'; these are only hints on when to
9899

99100
### Functions, Lambdas and Strings
100101

101-
* Prefer underscore lambda syntax like `Seq.map _.Name` over `Seq.map (fun x -> x.Name)`, but only when the expression is a simple member access. Complex expressions like `Seq.where (fun x -> x.Name = name)` or `Seq.map (fun x -> x.Field1, x.Field2)` cannot be simplified. Never write a space in `_.MethodCall()` – it breaks parsing.
102+
* Prefer underscore lambda syntax like `Seq.map _.Name` over `Seq.map (fun x -> x.Name)`, but only when the expression is a simple member access. Complex expressions like `Seq.where (fun x -> x.Name = name)` or `Seq.map (fun x -> x.Field1, x.Field2)` cannot be simplified. A member chain ending in a method call is not complex: `_.Changed.Subscribe(handler)`. The shorthand needs its input type known, so pipe the value in first – `value |> ValueOption.map _.Id`, not `ValueOption.map _.Id value` (FS0072). Never write a space in `_.MethodCall()` – it breaks parsing.
102103
* Simplify `Seq.map (fun x -> someFunction x)` to `Seq.map someFunction`.
103104
* Prefer interpolated strings over `printf` functions for string formatting. Format specifiers like `$"%s{value}"` are valid in interpolated strings and help type inference.
105+
* Pass an explicit `StringComparison` to every `Equals`, `StartsWith`, `EndsWith`, `IndexOf`, `Contains` and `Compare`, and an explicit comparer to every `HashSet<string>` and `Dictionary<string, _>`. `Ordinal` by default; culture-sensitive comparison is a decision, never a default. Use `OrdinalIgnoreCase` only where the thing compared really is case-insensitive – never for GraphQL names (types, fields, arguments, directives, enum values), which are case-sensitive.
106+
* A slice of a string that is only inspected – compared, trimmed, scanned, matched against a prefix – is a `ReadOnlySpan<char>` (`text.AsSpan (start, length)`), not a `Substring`: the substring allocates a copy per call, the span does not. Materialize with `Substring`/`ToString ()` only for the value that leaves the function or is stored.
107+
* The `StringComparison` rule applies to spans unchanged: `MemoryExtensions` has `StringComparison` overloads of `StartsWith`, `EndsWith`, `Equals`, `CompareTo`, `IndexOf` and `Contains` for `ReadOnlySpan<char>` – `s.AsSpan().TrimStart(' ').StartsWith("//", StringComparison.Ordinal)`. The overloads without one are the generic element-wise `ReadOnlySpan<'T>` ones – ordinal for `char` by accident, not by statement.
108+
* A slice that must outlive the stack frame – kept in a record, captured by a closure (including a `task` CE), returned from a member – is `ReadOnlyMemory<char>` (`text.AsMemory (...)`), never a `ReadOnlySpan<char>`: a byref-like value cannot be stored.
109+
* To compare two slices without building either, use `String.CompareOrdinal (a, aIndex, b, bIndex, length)` or `spanA.Equals (spanB, StringComparison.Ordinal)`.
104110
* Use descriptive function names that indicate transformation direction.
105111

106112
### Nullable Reference Types

0 commit comments

Comments
 (0)