liveBook · Manning

Dependency rejection

As explained in F# in Action: A Revised Edition of Get Programming with F#

The dependency rejection pattern is a design approach that keeps internal functions independent of external dependencies by passing in simple data rather than functions or effects.

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

How F# in Action: A Revised Edition of Get Programming with F# covers it

How deep it goes
Explains data-only validation, returned messages, caller-owned loading, and conditional IO tradeoffs; stops short of advanced treatment.
Examples
Uses customer descriptions, console printing, order validation, database loading, result values, and returned log-message lists.
What it assumes
Assumes basic functional composition and effects, while emphasizing discipline in identifying functions that should remain pure.
What you can do after
Apply it via pure functions, move effects into callers, and reason about validation independently from external services.
How it connects
Connects pure functions, functional composition, computation expressions, testing, logging, and conditional IO.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of F# in Action: A Revised Edition of Get Programming with F#. The section links point to the parts of the book it draws on.