Dependency rejection keeps internal functions pure by passing them simple data instead of functions, database access, logging, or other effects. External work stays at the boundary, where a caller performs it and supplies the resulting values. section 15.3.4
The pattern is useful when a function contains domain decisions but also needs external services. Separating those concerns makes the decision logic easier to reason about, while accepting that the orchestration layer may become harder to test. section 15.2.3 section 15.3.4
A practical dependency-rejection design usually follows these moves:
- Load external data in the caller rather than inside the validation function. section 15.3.4
- Pass domain values such as an order and customer into the pure function. section 15.3.4
- Return log messages and results as data, then perform logging separately. section 15.3.4
- Keep conditional effects at the orchestration boundary, because they can make the design complex quickly. section 15.3.4
Pure validation around database loading
The caller obtains a connection string, loads a customer, and passes that customer to validateOrderPure. The validator returns messages and a validation result; the caller logs the messages afterward. The validator is simple and pure, while processOrder retains the external dependencies and is inherently untestable. section 15.3.4
Listing 15.5 Composing with pure functions only
let describeCustomer customer = #1 let output = System.Text.StringBuilder () output.AppendLine $"Welcome, {customer.Name}!" |> ignore if customer.Balance < 0M then output.AppendLine $"Customer {customer.Name} owes {System.Math.Abs customer.Balance}" |> ignore else output.AppendLine $"{customer.Name} has a balance of {customer.Balance}." |> ignore output.ToString () let describeToConsole customer = #2 customer |> describeCustomer |> Printers.console #1 A pure version of the greeting function #2 A function that composes the greeting function with a specific printer
Compare a pure customer-description function with a separate console-printing composition step. section 15.2.3
Listing 15.8 Dependency rejection with computation expressions
let validateOrderPure order (customer:Customer) = #1
[
"Checking customer balance..."
"Validated order!"
], Ok (ValidatedOrder order) #2
let processOrder (order:Order) = result {
let authToken =
System.Environment.GetEnvironmentVariable "AUTH_TOKEN"
|> AuthToken
let connectionString =
System.Environment.GetEnvironmentVariable "CONNECTION_STRING"
|> SqlConnectionString
let logger = printfn "%s"
logger "Loading customer from DB..." #3
let! customer = IO.loadCustomer connectionString order.CustomerId
let (messages, validationResult) =
validateOrderPure order customer #4
for message in messages do logger message #3
let! validatedOrder = validationResult
return! IO.dispatchOrder authToken validatedOrder
}
Inspect dependency rejection applied to order validation with result computation and deferred logging. section 15.3.4
