This guide defines the design principles for clean, composable, and robust F# scripting with ESAPI. It applies to all scripts within this solution to ensure maintainability, clarity, and safe error handling.
- All operations that can fail return
Result<'T, string>orValidation<'T, string list> - This enables safe, predictable composition
- Failures are never hidden or thrown unless explicitly handled
let trySetPrescription (dose: float) (plan: PlanSetup) =
try
plan.SetPrescription(1, DoseValue(dose, "Gy"), 1)
Ok plan
with ex ->
Error $"Failed to set prescription: {ex.Message}"Avoid:
- Throwing exceptions from core logic
- Returning raw exceptions
- Relying on null or option types for failure signaling
- Compose multiple dependent steps clearly
- Short-circuits on first failure
- Makes function flow linear and easy to read
result {
let! patient = tryGetPatient ctx
let! course = tryGetCourse ctx
let! plan = tryFindPlanByIdPattern course "HH"
return plan
}Avoid:
- Deep match nesting
- Manual branching over Result
- Functions take explicit inputs and return outputs
- Avoid context-bound behavior in inner logic
- All non-IO logic is testable and referentially transparent
let createModifiedPlan course originalPlan imagePlan =
result {
let! copied = copyPlanToNewImage course originalPlan imagePlan
let! modified =
copied
|> trySetPrescription 2.0
|> Result.bind (trySetCalculationModel "AcurosXB")
return modified
}Avoid:
- Using mutable state
- Writing to UI or file system in logic functions
- Use immutable values by default
- Only mutate when the external API (e.g., ESAPI) requires it
- Limit the scope of modifications explicitly
let! patient = tryGetPatient ctx
do patient.BeginModifications()Avoid:
- Mutable bindings unless required
- In-place updates across shared state
Scripts should follow this logical layering:
Main.fs // Entry point: binds execution, shows results
Workflow.fs // Core workflow logic, composed of smaller units
Other Modules // Supporting layers, e.g. Utilities, DomainTypes, PlanOps
- Main.fs: Only constructs context and calls workflow functions
- Workflow.fs: Defines the domain-specific flow for the script
- Lower modules: Hold reusable operations or helpers
Avoid:
- Putting logic directly in Main.fs
- Mixing context acquisition and processing in the same function
- Errors are labeled with identifying information (IDs, names)
- Enables direct traceability to source of failure
Error $"[Plan {plan.Id}] Failed to copy to new image: {ex.Message}"Avoid:
- Empty or generic messages like "Error occurred" or "Something failed"
- Multiple variants (e.g., single vs batch) are kept in the code for demonstration or testing
- Toggled via comments — clear, explicit, and isolated
// --- SINGLE PLAN VERSION ---
// let! plan = createModifiedPlan course originalPlan (List.head imagePlans)
// do showMessageBox $"Created plan: {plan.Id}"
// --- MULTI PLAN VERSION ---
match createModifiedPlansFromDailyImages course originalPlan imagePlans with
| Validation.Ok plans -> ...Avoid:
- Duplicating logic between versions
- Removing working versions used for demos or reference
- Function names clearly describe their purpose and behavior
- Prefer explicit prefixes:
- tryGetX: returns Result
- createX: builds a new instance
- checkX: validates or asserts
- showX: triggers side effects
let tryFindPlanByIdPattern (course: Course) (pattern: string) = ...Avoid:
- Vague names like runStep, processIt, handleX
- Omitting verbs in function names