fsharp-env-capabilities
Skill23 starsChanged 32 days agoMITView on GitHub
Pass dependencies in F# as one generic env value whose capabilities are small IProvideX interfaces, so the compiler infers each function's dependency set and tests build only what a function needs. Also binds IConfiguration settings to F# types, validates them with Result at startup, and exposes the validated type as a capability instead of IOptions<T>. Use when an F# function list of dependency parameters grows, when wiring IServiceProvider or ASP.NET Core DI into F# functions, when binding or validating appsettings.json or IOptions settings in F#, when a test must substitute one dependency, or when the user says env, capability, IProvide, or environment pattern.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- fsharp-async-compositionSkill
- fsharp-binary-compatibilitySkill
- fsharp-failure-pathsSkill
- fsharp-idiom-reviewSkill
- fsharp-iwsam-errorsSkill
- fsharp-library-api-designSkill
- fsharp-native-aotSkill
- fsharp-nullness-interopSkill
- fsharp-performance-typesSkill
- fsharp-serializationSkill
- fsharp-signature-filesSkill
- fsharp-verify-apiSkill
- optimize-fsharp-typecheck-graphSkill
- parse-dont-validateSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 0 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 28
Keep task and taskResult blocks.
- Very specific: line 48
Write leaves with (env: #IProvideX).
- Very specific: line 55
Write composites with env unannotated and pass env to each call:
- Very specific: line 68
Add a breaks/ script that passes an env with one capability missing.
- Very specific: line 91
Stop the app before it does work.
- Very specific: line 99
Run scripts/run.ps1. It runs Environment.fsx, FlatEnv.fsx, and Settings.fsx, and prints the inferred signatures.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
F# environment capabilities
Each function takes env as its first parameter. A capability is an interface with one property
that exposes a worker interface. A function names only the capabilities it uses, through a
flexible type #IProvideX. A composite that calls several functions gets the union of their
capabilities by inference, with no new parameter. The caller passes one object that implements
every capability the call tree names. Sources: Bartosz Sypytkowski,
Dealing with complex dependency injection in F#,
and the bridge to ASP.NET Core DI, Fsharp-DI-Bridge.
A runnable proof with a test env, a container env, and two compile-time break tests lives in
scripts/. Read references/FINDINGS.md for output you can
compare against.
Rules
- Two interfaces per capability. The worker holds the operations, for example
ILogger. The accessor holds one property, for exampleIProvideLoggerwithabstract Logger: ILogger. Name accessorsIProvideX. The env implements accessors only. references/ENV-PATTERNS.md gives the reasons. - The accessor exposes a worker interface or a plain value, never a concrete class such as
RandomorHttpClient. A test must control what the worker returns. Wrap the class:IRandomwithNextBytes: length: int -> byte[], implemented once bySystemRandomand in tests byFixedRandom. - One module per capability. Each function takes
(env: #IProvideX)first and readsenv.X. - A composite leaves
envunannotated. Inference supplies the union. - A function that declares explicit type parameters must name
'envamong them. SeeFS0064below. - A bundle interface that inherits several
IProvideXis for implementers: the production env, a test requirements type. Inference never names it. - Keep
taskandtaskResultblocks. The Reader-monad variant in the article drops async composition. The article itself rates it less useful.
Steps
- List the external things a function touches: logging, database, clock, random, configuration, cache. Each is one capability. Done when every I/O call in the function maps to one capability and each worker is an interface a test can fake.
Declare the worker and accessor pair and the module:
type ILogger = abstract Info: string -> unit type IProvideLogger = abstract Logger: ILogger module Log = let info (env: #IProvideLogger) fmt = Printf.kprintf env.Logger.Info fmtDone when
dotnet fsiprintsval info: env: #IProvideLogger -> ....Write leaves with
(env: #IProvideX). When a leaf also declares'efor an IWSAM error, declare'envexplicitly:let fetchUser<'env, 'e when 'env :> IProvideDatabase and UserError<'e>> (env: 'env) (id: int) : Task<Result<User, 'e>> =Done when
dotnet fsiprints nowarning FS0064for the leaf.Write composites with
envunannotated and passenvto each call:let changePassword env (request: ChangePasswordRequest) = taskResult { let! user = Db.fetchUser env request.UserId do! Db.updateUser env { user with Salt = Random.salt env 16 } Log.info env "password changed for %d" user.Id }Done when the printed signature shows the union, for example
when 'a :> IProvideDatabase and 'a :> IProvideLogger and 'a :> IProvideRandom.- Build the production env and the test env from references/ENV-PATTERNS.md. Done when the same composite runs with both and the test env implements only the capabilities the composite names.
- Add a
breaks/script that passes an env with one capability missing. Done when it fails withFS0193.
Compiler signals
| Signal | Meaning | Action |
FS0193: The type 'LogOnly' is not compatible with the type 'IProvideClock' | The env misses a capability the call tree names. | Implement IProvideClock on the env. |
warning FS0064: ... The type variable implied by the use of a '#' ... has been constrained to be type 'FirstEnv' | A function with explicit type parameters used #IProvideX. The compiler did not generalize env. The first caller fixed it. A second caller then fails with FS0193: The type 'SecondEnv' is not compatible with the type 'FirstEnv'. | Name 'env in the explicit list: <'env, 'e when 'env :> IProvideX and ...>. |
Signature prints IProvideDatabase -> ... instead of 'a -> ... when 'a :> IProvideDatabase | The function uses one capability. The compiler fixed env to that interface. | No action. Any env that implements it still passes. A composite that calls it still infers the full union. |
FS0039: The type parameter 'e is not defined | A composite named an IWSAM member. | Move the check into a leaf. See the fsharp-iwsam-errors skill. |
Settings
Settings are one more capability. Bind configuration to a DTO, parse the DTO into a validated type at startup, and expose that type through an accessor. references/SETTINGS.md has the full example and the binder table. scripts/Settings.fsx checks each rule below.
- Bind to a
[<CLIMutable>]DTO that only the composition root reads. A missing key gives no error: anintbinds as0and astringasnull. - In the DTO, use
stringfor a number and convert it in the parser. Then text that does not convert is one more error. ANullable<int>orintfield throws in the binder on such text, before the parser runs. Usestring[]for a list. Astring listfield throws when the key is present. Astring optionfield isNoneeven when the key has a value. section.Get<Dto>()returnsnullfor a missing section.AddOptions<Dto>().Bind(section)gives an instance with default fields. A missing nested record binds asnullin both forms.- Parse the DTO with
parse: Dto -> Result<Settings, string list>. Usevalidation { let! ... and! ... }so that the result holds every error. Name the configuration key in each error. For the parsed type, use theparse-dont-validateskill. - Stop the app before it does work. Either parse in the composition root and stop on
Error, or register anIValidateOptions<Dto>that calls the parser and add.ValidateOnStart(). ValidateOnStartruns only when a host starts. WithValidateOnStartand a bareServiceCollection, callprovider.GetRequiredService<IStartupValidator>().Validate(). Otherwise the error comes at the first read.Validate()throwsOptionsValidationExceptionfor one bad section andAggregateExceptionfor two or more. Catch both, or the error list is lost.- Expose the parsed type through one accessor for each section:
IProvideMailSettingswithabstract MailSettings: MailSettings. No function outside the composition root takesIOptions<T>orIConfiguration. - A test env returns a settings record literal. It needs no configuration, no container, and no
Options.Create.
Verify
Run scripts/run.ps1. It runs Environment.fsx, FlatEnv.fsx, and Settings.fsx,
and prints the inferred signatures. It fails when a Settings.fsx check does not match. It also checks that each script under scripts/breaks/ fails with the error
quoted above. In your own project, print the signature of every composite with dotnet fsi and
read the constraint list. A composite whose signature names a concrete env type has lost its
genericity. Find the leaf that pinned it.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

