liveBook · Manning

Functional interface

As explained in Functional Design and Architecture: Examples in Haskell

A functional interface is an abstraction defining communication between subsystems through functional representations rather than object-oriented method dispatch.

A functional interface defines how one subsystem communicates with another without exposing the subsystem’s implementation. It commonly combines abstract or opaque types with functions that operate on them. Client code depends on this small public surface rather than on constructors, internal data structures, native APIs, or device-specific details. section 3.3.3 section 4.3

Use a functional interface when a subsystem must remain replaceable, testable, and independent of its callers. The interface captures the important domain operations; an implementation supplies the operational details. A hardware-facing module, for example, can export Device, blankDevice, addSensor, and addController while hiding DeviceImpl and its internal map of parts. section 3.3.3 section 1.3

Listing 3.12 Hardware runtime
module Andromeda.Hardware.Device ( 
    Device,                                      #1
    DevicePart,
    makeDevice,                                  #2
    blankDevice,
    addSensor,
    addController,
    getDevicePart,
    updateDevicePart
 ) where
  
import Andromeda.Hardware.Common
import Andromeda.Hardware.Hdl
  
data DevicePart                                  #3
  = SensorImpl     ComponentPassport
  | ControllerImpl ComponentPassport
  
type DeviceParts = Map ComponentIndex DevicePart #4
  
data Device = DeviceImpl DeviceParts             #5
  
makeDevice :: Hdl -> Device                      #6
blankDevice :: Device
addSensor :: ComponentIndex -> Parameter 
          -> ComponentPassport -> Device -> Device 
addController :: ComponentIndex -> ComponentPassport
              -> Device -> Device 
getDevicePart :: ComponentIndex -> Device
             -> Maybe DevicePart 
updateDevicePart :: ComponentIndex -> DevicePart
                 -> Device -> Maybe Device

The exported module boundary shows the central move: reveal the types and operations clients need, while keeping the concrete Device representation private. section 3.3.3

This is an architectural boundary, not one fixed encoding. A functional interface can be an algebraic data type plus interpreter, a state machine, a monadic representation, or type-class-based code. DSLs layered over a domain model, subsystem, or service can likewise act as interfaces by describing allowed operations independently of execution. section 1.3.3 section 2.2

A useful interface keeps responsibilities separated:

  • The interface exposes only essential operations and domain-relevant types, not implementation details. section 2.2.3
  • An interpreter or implementation performs the real work and handles concrete dependencies. section 2.2.3
  • Business logic uses the interface and can remain unaware of selected implementations. section 4.2.2
  • Tests can inject mocks or fakes instead of requiring real external subsystems. section 4.3

A Service Handle is the simplest form when first-class functions are available: a data value holds operations as fields, and callers receive that value as a dependency. Different handle values can provide different behavior, including explicitly impure behavior, without changing the calling business logic. This is dependency injection through a functional interface. section 4.2.2 section 4.3

Injecting a greetings service

A GreetingsHandle groups a pure greeting function and an IO-producing greeting function. helloUser accepts the handle rather than choosing an implementation itself, so the same client routine can run with either supplied handle value. The pattern makes the dependency visible at the function boundary and separates client behavior from service behavior. section 4.2.2

For richer domain languages, free monads provide an interface whose scripts are declarative, abstract, monadic, interpretable, and first-class values. A script describes sequential actions but does not run until an interpreter traverses it. This supports scenarios, nested languages, and independently selectable interpreters, while preserving a boundary comparable in purpose to an OOP interface. section 2.2.3 section 6.5

Functional interfaces support dependency inversion by letting business functions depend on reporting, storage, hardware, or other capabilities instead of concrete channels and frameworks. Configuration can select a real implementation at application startup, while higher-level code remains details-agnostic. Keep the boundary concise and avoid bypassing it; abstraction helps only when it reduces coupling without adding unjustified complexity. section 13.2.1 section 4.3 section 1.3

How Functional Design and Architecture: Examples in Haskell covers it

How deep it goes
Defines abstract type-and-function boundaries, then develops Service Handles and free-monad interfaces with interpreters.
Examples
Uses hidden Device internals, SensorAPI and ControllerAPI records, greetings handles, and meteor reporting.
What it assumes
Assumes familiarity with subsystems and functions, while introducing interfaces through records, abstract types, and interpreters.
What you can do after
Apply it via Service Handles or free monads to isolate clients from replaceable subsystem implementations.
How it connects
Connects dependency injection, dependency inversion, free monads, DSLs, interpreters, APIs, and mocking.

Also explained in

Compare how 3 books explain functional interface →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Design and Architecture: Examples in Haskell. The section links point to the parts of the book it draws on.