liveBook · Manning

Smart constructor

As explained in Functional Design and Architecture: Examples in Haskell

A smart constructor is a specialized construction function that creates values while presenting a more convenient or constrained interface than the underlying data constructor.

A smart constructor is a function that creates a value without exposing the value’s underlying representation. Instead of letting callers assemble an ADT directly, it gives them a stable, intentional entry point. The function may be a thin wrapper, or it may perform validation, select internal details, serialize inputs, or convert errors before returning the value. section 3.2.4 section 13.1.3

The motivating problem is that public data constructors make internal structure part of the client contract. Callers can pattern-match on constructors and construct combinations that the domain should reject. Renaming a constructor or changing an ADT then breaks dependent code. Keep the type abstract in a module, hide its value constructors, and export smart constructors as the public interface. section 3.2.4 section 3.3.2

Protect a component definition

A component passport declares whether it belongs to Sensors or Controllers, while a component definition can be either Sensor or Controller. Direct construction can accidentally create a Controller with a sensor passport. A sensor smart constructor always builds a Sensors passport; a controller smart constructor always builds a Controllers passport. This cannot make input data correct, but it prevents callers from assembling that invalid state through the supported interface. section 3.3.2

Listing 3.11 Smart constructors
module Andromeda.Hardware.Hdl
  ( module X
  , ComponentDef                                  #1
  , ComponentIndex
  , sensor                                        #2
  , controller
  ) where
  
import Andromeda.Hardware.Common as X
  (PhysicalGuid, Parameter, Vendor, ComponentIndex, ComponentName)
  
import Andromeda.Hardware.Hdl.Internal
  (ComponentPassport (..), ComponentClass (..))
  
sensor                                            #3
  :: PhysicalGuid -> Vendor -> ComponentName
  -> ComponentIndex -> Parameter
  -> ComponentDef
sensor guid vendor name idx param =
  Sensor (ComponentPassport Sensors name guid vendor) idx param
  
controller
  :: PhysicalGuid -> Vendor -> ComponentName
  -> ComponentIndex
  -> ComponentDef
controller guid vendor name idx =
  Controller (ComponentPassport Controllers name guid vendor) idx

Compare the exported sensor and controller functions with the hidden ComponentPassport and ComponentClass constructors. Each function fixes the class required by its resulting ComponentDef. section 3.3.2

Hiding constructors is a deliberate boundary, not an absolute prohibition. Tests or tightly controlled implementation code may need direct access to opaque internals. One option is an Internal module that remains public but signals that its API is unstable and should not be re-exported. Another is to expose abstract types together with operations for using them without revealing their structure. section 3.3.2

Smart constructors also improve usability when a raw constructor is technically valid but inconvenient. In a free-monad language, each operation must wrap an algebra instruction with Free and supply Pure as its continuation placeholder. Named functions such as startNewSandwich, addComponent, and finishSandwich hide that boilerplate, so scripts read as domain operations rather than manual tree construction. section 6.3.1 section 6.3.2

Hide service-specific field encoding

A structured logging interface represents fields as Text key-value pairs, but a Sentry timestamp originates as a Timestamp. sentryTimestamp is a smart constructor that produces the required Field while keeping JSON serialization and byte/text conversions inside the Sentry-specific module. Logging code supplies the timestamp without depending on that representation work. section 10.1.3

The same boundary can translate implementation failures. A filesystem language may have one constructor returning a native exception result and another returning an application-level FSError result. Exposing only the latter keeps business logic focused on explicit value-based errors while the smart constructor performs the conversion. This is useful when an interface should conceal native library details without pretending they do not exist. section 13.1.3

Use a smart constructor when construction needs a contract, a friendlier vocabulary, or insulation from representation changes. A trivial wrapper can still be worthwhile when it is the only exported way to create an abstract value. The trade-off is API design: decide which construction and inspection operations clients genuinely need, and keep direct internal access clearly bounded. section 3.2.4 section 3.3.2

How Functional Design and Architecture: Examples in Haskell covers it

How deep it goes
Explains wrappers, opaque exports, invariant enforcement, serialization, error conversion, and Free/Pure wrapping, while deferring native exception mechanics.
Examples
Uses Value wrappers, sensor/controller definitions, sandwich scripts, Sentry timestamp fields, and filesystem writes.
What it assumes
Assumes familiarity with functions and ADTs, then introduces smart constructors through concrete module and domain examples.
What you can do after
Apply it by exposing construction functions that preserve selected invariants and hide representation or integration details.
How it connects
Connects abstract data types, modules, Free monads, GADTs, structured logging, dependency inversion, and explicit errors.

Also explained in

Compare how 3 books explain smart constructor →

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.