There is a meme that every (OOP) GoF design pattern can just be replaced with a function.
If you think about it, it’s not that funny. Sure, there is some truth to that, and some patterns are already obsolete. But we also don’t just use functions in a vacuum — we write, compose, and manipulate them.
Plus, jumping to those kinds of extremes is not helping anyone. We can build connections and grow on top of existing knowledge.
So, let’s go through each of the 23 GoF patterns and see which one I actually see being used and what the functional alternatives are (if any). Most techniques and concepts will be language agnostic, with a few specific examples. We’ll see snippets in Haskell, Scala, PureScript, and Unison.
Creational Patterns
- Abstract Factory
- Builder
- Factory Method
- Prototype
- Singleton
Disclaimer: There are no OOP classes outside of OOP, so making instances of classes doesn’t make sense — but we still can talk and abstract over creating instances of things. Instead of classes, we'll be thinking in terms of modules, bundles of functionality, encapsulated APIs, or something along those lines...
Abstract Factory
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
I remember it as a toy factory that, for example, can make bears, airplanes, and cars out of different materials (e.g., wood or plastic).
In the oop world, we can use this to abstract over things like database connections and commands while generalizing over the database. So, we can write code that creates a database connection, executes some queries, and runs it with one database. Later, if we want to, we can swap the database by swapping the factory (and configuration).
In FP, we don’t use abstract factories — we utilize functions and polymorphism (polymorphic types, and, in some cases, type classes). Let’s take a look at two Haskell libraries and how they address this.
In beam, we write abstract (backend/database-agnostic) code in terms of MonadBeam with polymorphic backend and generic functions (e.g, select).
query1 :: (MonadBeam be m) => m [Product]
query1 = do
let allProducts = all_ (warehouseDb.product)
runSelectReturningList (select allProducts)
Don't worry about the function signature for now. The important part is that it's abstract in terms of the database.
We can then execute for a specific database using one of the backends; for example, runBeamPostgres for postgres.
executeQuery :: Connection -> IO ()
executeQuery connection = runBeamPostgres connection do
result1 <- query1
liftIO $ putStrLn ("Query 1: " <> show result1)
Note: We can also use the specific backend directly to access the full power of the database.
See usage example and docs.
In persistent, it’s somewhat similar; we use functions that are abstract over the backend type. (for sql databases, it’s SqlBackend). The library uses type classes (such as PersistStoreRead, and PersistStoreWrite) to define database operations.
insertStuff :: (MonadIO m) => ReaderT SqlBackend m ()
insertStuff = do
newId <- insert (Product "Wood Screw Kit 1" 245)
liftIO $ putStrLn ("Insert 1: " <> show newId)
ReaderT SqlBackend m ()means we can access database configuration (e.g., database connection), can perform arbitrary IO operations, and don’t return anything meaningful (void).
And once again, we tie everything together with a specific backend, for example, using runSqlite or withPostgresqlPool.
See usage example and docs.
Factory Method
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
In the simplest form, we want to hide the concrete implementation, so it can be created, returned, and used behind an abstraction/interface. A somewhat simple and common use case.
Imagine we have some payment processing system and need to support multiple providers — for now, stripe and paypal.
trait PaymentProcessor[F[_]] {
def processPayment(payment: Payment): F[PaymentResult]
def refund(transactionId: String): F[PaymentResult]
}
class PayPalProcessor[F[_]: Sync] extends PaymentProcessor[F] {
def processPayment(payment: Payment): F[PaymentResult] = ???
def refund(transactionId: String): F[PaymentResult] = ???
}
class StripeProcessor[F[_]: Sync] extends PaymentProcessor[F] {
def processPayment(payment: Payment): F[PaymentResult] = ???
def refund(transactionId: String): F[PaymentResult] = ???
}
This is an example of tagless final — a popular approach to structuring code in Scala. We have an abstract interface PaymentProcessor (some generic F) and somewhat abstract implementations (F with Sync) that still don't commit to the final implementation, it could be IO, IO with explicit error handling, IO with tracing, etc.
If we delegate the creation of the specific implementation to the PaymentProcessorFactory — for example, based on the config — we get ourselves a simple factory.
case class PaymentConfig(paymentProvider: String)
object PaymentProcessorFactory {
def createProcessor(config: PaymentConfig): F[PaymentProcessor[F]] = {
config.paymentProvider.toLowerCase match {
case "paypal" => PayPalProcessor[F].pure[F]
case "stripe" => StripeProcessor[F].pure[F]
case unknown => IllegalArgumentException("Unknown provider: $unknown").raiseError[F]
}
}
}
Note that we can always go a level deeper and abstract the abstractions:
trait PaymentProcessorFactory[F[_]] {
def createProcessor(provider: String): F[PaymentProcessor[F]]
}
class DefaultPaymentProcessorFactory[F[_]: Sync](config: PaymentConfig)
extends PaymentProcessorFactory[F] {
def createProcessor(): F[PaymentProcessor[F]] = {
config.paymentProvider.toLowerCase match {
???
}
}
}
class ExperimentBasedPaymentProcessorFactory[F[_]]
extends PaymentProcessorFactory[F] { ... }
class CountryBasedPaymentProcessorFactory[F[_]]
extends PaymentProcessorFactory[F] { ... }
But let's not get wild here. Instead, let's look at a primitive usage example (note that there is nothing about specific implementations):
class PaymentService[F[_]: Sync](factory: PaymentProcessorFactory[F]) {
def makePayment(provider: String, payment: Payment): F[PaymentResult] = {
for {
processor <- factory.createProcessor(provider)
result <- processor.processPayment(payment)
} yield result
}
def refundPayment(provider: String, transactionId: String): F[PaymentResult] = {
for {
processor <- factory.createProcessor(provider)
result <- processor.refund(transactionId)
} yield result
}
}
Factory methods are still used in different shapes and forms, especially in the scala world. As soon as you have a tagless final, mtl, service pattern, or whatever, factories sprout out naturally when we actually have multiple implementations.
Singleton
Ensure a class only has one instance, and provide a global point of access to it.
In the presence of immutability and other things, singletons aren’t really needed. Most of the time, we create something once and then pass it around, knowing that nobody can modify it. For example, a db connection:
Simple.withConnect connectionInfo $ \connection -> do
cleanUp connection
insertStuff connection
queryData connection
insertWithTransaction connection
queryWithJoins connection
errors connection
If we need to modify something and access it concurrently, we use appropriate concurrency primitives; for example, Ref (aka Ref, IORef, …, you can find ref’s siblings all over).
If we are in scala land and do need the simplest form of singleton (one instance of a class, not the double-thread-safe protected version, there are also singleton objects modules.
Builder
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
Now that we talk about scala, Builders are also quite common. It is one of the patterns that the community inherited and carried over from java.
For example, here is a request definition using the sttp library:
val request = basicRequest
.cookie("login", "me")
.body("This is a test")
.post(uri"http://endpoint.com/secret")
It’s typical for fp libraries (and apis) to provide a basic/initial/default way to create data and build on top of.
Here is a unison example
We see something similar in Haskell libraries and apis: defaultRecord that can be updated via setters or via record update syntax. For example, to create Options for json codecs using
defaultOptions { omitNothingFields = True }
💡 Note: Sometimes it comes in the form of a default record and monoid instance.
Wait. Is this Prototype, not Builder? Hm. Let’s revisit it in a second.
To conclude with builders, I’d say that they are somewhat common. Maybe not the way GoF envisioned, but whatever, builders are builders.
Otherwise, in the fp world, we talk more about smart constructors and not builders. The focus is on making functions and APIs for creating only valid or validated values that can be safely passed around afterwards. For example, if we have a smart builder for an album, it should make sure that we can't create an album with an invalid year or an empty artist name.
Prototype
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
It’s quite a functional pattern. Even if I can’t draw a clear line between builders and prototypes.
We saw this example of creating Options for json codecs:
defaultOptions { omitNothingFields = True }
There is some record with defaults, we copy it, and update only what's needed.
Copying data is bread and butter in FP. Some have it baked in. The above is the Haskell syntax for record updates. Scala case classes have a copy method:
case class Foo(from: String, amount: Long)
val foo = Foo("foo", 20)
val bar = foo.copy(from = "bar")
Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Structural design patterns are about organizing (structuring and re-structuring) objects and classes. And once again, a disclaimer. Because we don't have those outside of OOP, we’ll have to be more abstract and think in slightly different terms. I think we should focus on APIs and gluing code together. And to keep it interesting, we'll mix in various approaches.
If you can recall, when we started with Abstract Factory, we already saw a couple of interesting-looking functions (function signatures):
query :: (MonadBeam be m) => m [Product]query :: (MonadIO m) => ReaderT SqlBackend m ()
In Haskell, there is plain IO, mtl and transformers, custom monads (on top of those), IO + Reader(T), RIO, unliftio, services and handle patterns, free monads, freer-simple, extensible-effects, fused-effects, eff, effectful, cleff, polysemy, bluefin, and other effect libraries (that I forgot to mention).
“Remember this when someone says that it’s just functions in FP.”
Adapter
Convert the interface of a class into another interface that clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
In the simplest case, an interface could imply the data we move around. Imagine you have a module responsible for payments, you need to update to the latest stripe API (with breaking changes), but you don't want (or are not allowed) to change the sdk module. You can make an adapter.
If you have a module with V1 data with a function like this:
getInvoices :: UserId -> InvoiceStatus -> IO [InvoiceDataV1]
You can adapt it to V2 data like this:
adaptInvoice :: InvoiceDataV1 -> InvoiceDataV2
-- adapt from v1 to v2
getInvoicesV2 :: UserId -> InvoiceStatus -> IO [InvoiceDataV2]
getInvoicesV2 user status = do
v1Response <- getInvoicesV1 user status
v2Response = map adaptInvoice v1Response
pure v2Response
I'd say this is an adapter, alas, a boring one. A more interesting interpretation of interfaces is the library and internal APIs ‒ APIs that we use to code with. It's a perfect opportunity to demonstrate what I meant in the introduction.
There is no one way to write Haskell code. Even in the same code base, it's quite common to have a mix of approaches. For example, you just started a new service with a new fancy framework, and you need to integrate a library (internal or external) that uses something different.
Let's start simple. You have a library that uses plain IO:
getInvoices :: UserId -> InvoiceStatus -> IO [InvoiceData]
And you need to integrate it with your service that uses monad transformers, for example, ResourceT. We need an adapter!
getInvoicesLifted :: UserId -> InvoiceStatus -> ResourceT Env [InvoiceData]
💡
ResourceTprovides the ability to pass and read env. There are no global variables.
You can do the transformation manually (not going to show it), but you can also use a function just for this! In this case, liftIO:
getInvoicesLifted :: UserId -> InvoiceStatus -> ResourceT Env [InvoiceData]
getInvoicesLifted user status = liftIO (getInvoices user status)
This is simplified. Depending on the context, complexity, and your team, this could be handled differently and kept more abstract. For instance, some people would prefer not to use an intermediate function like getInvoicesLifted and use liftIO at the call site.
Some prefer to use alternative abstractions and utilities, such as unliftio. But it all depends... Remember to check the documentation, recommendations, or your instincts for each particular case.
For example, if you look at the docs of the Effectufl library, you can see a whole section on integration with various cases.
Bridge
Decouple an abstraction from its implementation so that the two can vary independently
I think the idea is to use composition instead of inheritance to decouple the high-level interface that clients interact with from the concrete implementation details. The inheritance is not a problem outside of OOP, and you can decouple the public interface from the private in many different ways.
For example, if we talk about libraries and apis, one common approach in Haskell and PureScript library design is using the Internal module.
For example, let's look at the bytestring library: the public API is Data.ByteString, but the lower-level API is in Data.ByteString.Internal. The Internal API is not stable and not meant for casual everyday usage; it's meant to provide flexibility for advanced users. For instance, see the disclaimer in the containers internals
Not everyone's favorite approach, and it doesn't solve all the problems, just one example.
Composite
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Trees and recursions are native to FP. For instance, there are design patterns for traversing and recursing over nested data structures, called recursion schemes. But I think it might be too specific to "represent" Composite; we can get away with something simpler. We can use type classes to model Composite.
Let's ignore the parts where we need to add and remove components, and focus on having a uniform interface for components and compositions of components. Imagine we need to have a Printable interface.
(Don't think about oop classes, think about rust traits)
class Printable a where
print :: a -> String
We can implement a Printable instance for anything that needs to be printed. Let's rely on the default PureScript show for a number and return a string in quotes:
instance Printable Number where
print = show
instance Printable String where
print s = "\"" <> s <> "\""
If we have an array of printable things, we can print them all:
instance Printable a => Printable (Array a) where
print arr = "[" <> intercalate ", " (map print arr) <> "]"
Note that we rely on a Functor instance of Array, a map function. Another type class. Just imagine that we apply a function to every element of the list.
To wrap up this example, let's look at printable json (with a mix of printable and not yet printable types):
data JSON
= JNull
| JBool Boolean
| JNumber Number
| JString String
| JArray (Array JSON)
| JObject (Array (Tuple String JSON))
instance Printable JSON where
print JNull = "null"
print (JBool true) = "true"
print (JBool false) = "false"
print (JNumber n) = print n
print (JString s) = print s
print (JArray arr) = print arr
print (JObject pairs) = "{" <> intercalate ", " (map printPair pairs) <> "}"
printPair (Tuple k v) = print k <> ": " <> print v
There might be too many quotes, and I'm too lazy to test it. However, see how the json arrays and objects are handled. Composite!
Decorator
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
My favorite use of decorator is to separate business logic from production-needed bells and whistles (things that help observability and performance, but can swallow the core logic if not tamed well).
Let's revisit the tagless-final processor that we used a while back.
trait PaymentProcessor[F[_]] {
def processPayment(payment: Payment): F[PaymentResult]
def refund(transactionId: String): F[PaymentResult]
}
class PayPalProcessor[F[_]: Sync] extends PaymentProcessor[F] {
def processPayment(payment: Payment): F[PaymentResult] = ???
def refund(transactionId: String): F[PaymentResult] = ???
}
class StripeProcessor[F[_]: Sync] extends PaymentProcessor[F] {
def processPayment(payment: Payment): F[PaymentResult] = ???
def refund(transactionId: String): F[PaymentResult] = ???
}
If we want to add metrics without duplication and polluting the implementation, we can add a new implementation of the payment processor to enhance the functionality:
class MeteredProcessor[F[_]](underlying: PaymentProcessor, metrics: Metrics)
extends PaymentProcessor[F] {
def processPayment(payment: Payment): F[PaymentResult] =
metrics.measure(underlying.processPayment(payment))
def refund(transactionId: String): F[PaymentResult] =
metrics.measure(underlying.refund(transactionId))
}
// Checks the cache first, before calling the api
class CachedProcessor[F[_]](underlying: PaymentProcessor, cache: Cache)
extends PaymentProcessor[F] {
def processPayment(payment: Payment): F[PaymentResult] = ???
def refund(transactionId: String): F[PaymentResult] = ???
}
And then decorate the original processors:
val payPalProcessor = MeteredProcessor(PayPalProcessor(...)) // no cache
val stripeProcessor = MeteredProcessor(CachedProcessor(StripeProcessor(...)))
Facade
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
I don’t think it’s a real design pattern; it’s just good design (good manners). I don’t think it’s worth talking about here.
Flyweight
Use sharing to support large numbers of fine-grained objects efficiently.
This one is also a normal phenomenon. In my naive and optimistic view, in the presence of immutability, we often get this for free.
data Expense = Expense Int
deriving (Eq, Show, Num)
makeExpensiveValue :: Int -> Expense
makeExpensiveValue n =
trace ("Creating an expensive value: " ++ show n) (ExpensiveValue n)
If we create an instance of something expensive, it makes sense that it's created once:
main = do
let x = makeExpensiveValue 3
print x
-- Creating an expensive value: 3
-- Expense 3
And if we want to create a whole list of those, it will still create one instance of Expense 3:
main = do
let xs = replicate 5000 (makeExpensiveValue 3)
let res = sum xs
print res
-- Creating an expensive value: 3
-- Expense 15000
This still prints Creating an expensive value once. The initialization happens once.
Proxy
Provide a surrogate or placeholder for another object to control access to it.
I think using Proxy is structurally similar to Decorator ‒ instead of providing an additional functionality, it restricts access, sometimes adds lazy initialization, and so on. Maybe the way the clients use it is different, doesn't make a huge difference. Let's just move on.
Behavioral Patterns
Finally, a set of patterns that have nothing to do with classes and objects, right?
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Chain of Responsibility
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
When I think about chains and fallbacks, I think of Options:
val first: Option[Payment] = stripePayments.headOption
val second: Option[Payment] = payPalPayments.headOption
first.orElse(second).getOrElse(fallback)
We chain a few things until we get some payment. This isn't much of a chain of handlers, so let's look at something more interesting. Imagine we need to extract a version out of the string, but we are dealing with questionable vendors (some return "v1.1", others "1.1.0", "2025-10-10", and so on):
We can go through the list of parser functions until one of them returns some result, or we reach the end:
val parseSemVer : String => Option[Version] = ???
val parseCalVer : String => Option[Version] = ???
val parseSoloVer: String => Option[Version] = ???
val parseChained: String => Option[Version] = { str =>
List(parseSemVer, parseCalVer, parseSoloVer).collectFirstSome(_(str))
}
parseChained("2025-10-10") // Some (CalVer ...)
parseChained("non-sense") // None
Alternatively, we can use an explicit chain:
val parseChained: String => Option[Version] = { str =>
parseSemVer(str) <+> parseCalVer(str) <+> parseSoloVer(str)
}
The <+> operator comes from the SemigroupK type class. Doesn't matter where it comes from. It's common. We won't go too deep into parser combinators, but this would be a fitting detour. They don't use Options (because we need meaningful errors), but do use plenty of typeclasses and operators to chain parsers together. If you are interested, consider exploring any Haskell library (such as megaparsec) or tutorial (e.g., Learn Haskell by building a blog generator). Note the use of the <|> operator (it's related to the other one).
parseTest (try (string "let") <|> string "lexical") "lexical"`
If we return to our examples and want more than "just" parsing -- if we want arbitrary handlers of requests -- we can use IO along with Option:
val fetchStripePayment: IO[Option[Payment]] = ???
val fetchPayPalPayment: IO[Option[Payment]] = ???
val createFallback: IO[Option[Payment]] = ???
fetchStripePayment.orElseF(fetchPayPalPayment).orElseF(createFallback)
That is syntactic sugar. Those chains are more commonly seen as an OptionT monad transformer:
(OptionT(fetchStripePayment) <+> OptionT(fetchPayPalPayment) <+> OptionT(createFallback)).value
The syntactic sugar in the previous snippet is to hide OptionT from innocent eyes.
And to nail the point. One of the Scala HTTP libraries uses something similar to chain services (actual request handlers) together:
val services = tweetService <+> helloWorldService
Command
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
This is IO, or Task, or Effect, or whatever your language or library calls it — some data type to describe and pass around (effectful) computations. Let's use Effect and PureScript for examples, and start simple.
Let's start with a function that takes an arbitrary command and repeats it N times. We can log and differentiate the stages via the console.
repeat :: forall a. Int -> Effect a -> Effect a
repeat n task =
-- todo: errors
if (n == 1) then do
log "done"
task
else do
log ("doing " <> show n)
_ <- task
repeat (n-1) task
Even though we can pass any kind of action, I couldn't come up with anything better than passing another log repeat 3 (log "- Action"). We can still see the repetition in action:
doing 3
- Action
doing 2
- Action
done
- Action
If we return to the payments domain and sketch out a more complex client, we can see the pattern more clearly:
type Request =
{ fetchPayments :: UserId -> Effect Payments
, prepareInvoice :: Payments -> DateTime -> Effect Invoice
}
client :: UserId -> Request -> Effect (Array Invoice)
client user { fetchPayments, prepareInvoice } = do
payments <- fetchPayments user
let dates = [yesterday, today, tomorrow]
invoices <- for dates (prepareInvoice payments)
pure invoices
In this example, there is no exception handling or retries, but this could be the next logical step. This is not really Command, but it covers 80% use cases (if not more). We can pass different implementations of tasks/actions to the client, and the client decides in what order to execute those, how many times, and whatever else.
This is a service pattern -- it's not very documented but not very restrictive. If you are coming from oop world, it's just an interface bundled together. And concrete implementations are also bundles of concrete functions.
Anyway. If IO and friends are not enough to express the Command, it could be integrated with the techniques used to replace the Interpreter.
Interpreter
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
I'm pretty sure this is the most beloved design pattern among fp devs. For instance, EDSLs (embedded domain-specific languages) are very popular and one of the go-to examples for selling fp; the overlap between the patterns and the worlds is significant.
We've already seen a few interpreters in the previous examples. But let's start with an alphabet: expressions.
// operations
sealed trait Expr
case class Add(l: Expr, r: Expr) extends Expr
case class Lit(i: Int) extends Expr
// evaluators/interpreters
def eval(e: Expr): Int = e match {
case Lit(i) => i
case Add (l, r) => eval(l) + eval(r)
}
def pprint(e: Expr): String = e match {
case Lit(i) => i.toString
case Add (l, r) => print(l) + "+" + print(r)
}
Which allows us to represent and interpret sentences like this one:
eval(Add(Lit(1), Add(Lit(2), Lit(3))))
It's not a production-ready example; there are plenty of issues (it's hard to extend and add new operators, to support other types of literals, and so on), but this should be a good starting point.
Now, let's hop a few learning steps ahead and look at real-world examples. When we are talking about free monads or tagless final, we are talking about interpreters. It's especially common in scala.
One of the popular Scala libraries -- doobie (for sql/persistence) uses free monads and "provides a functional way to construct programs (and higher-level libraries) that use JDBC"
For example, we can construct a program using a primitive sql statement that returns an int:
val program: doobie.ConnectionIO[Int] = sql"select 42".query[Int].unique
But it won't do anything unless we run or interpret it:
val xa: Transactor[IO] = Transactor.fromDriverManager[IO](???)
program
.transact(xa) // Interpret the doobie program into IO
.unsafeRunSync() // Execute IO
Technically, doobie provides an interpreter from its free monads to something more generic than IO, but ... I hope the point about interpreters is clear.
While we're here, let's look at a visitor.
Visitor
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
It's quite common to compare the Visitor pattern to pattern matching.
// Visitor interface
trait PaymentVisitor {
def visitPayPal(payment: PayPalPayment): Unit
def visitStripe(payment: StripePayment): Unit
def visitBankTransfer(payment: BankTransferPayment): Unit
}
vs.
def process(payment: Payment) = payment match {
case PayPalPayment(email, amount) => ???
case StripePayment(token, amount) => ???
case BankTransferPayment(iban, amount) => ???
}
And then you are supposed to laugh because a design pattern was invented to compensate for a lack of a language feature. But the thing is, life is not that simple. Not every functional language supports sum types and pattern matching. It's not a requirement for being functional.
Those languages can get away with Church encoding / Böhm-Berarducci encoding (depending on whether they are typed or not).
For example, instead of pattern matching on an optional value, it's enough to have a function that allows dealing with both cases when you have and do not have a value:
// with pattern matching
Option(3) match {
case Some(int) => print("You have an int")
case None => print("You have none")
}
// with Böhm-Berarducci encoding:
Option(3).fold(ifEmpty = print("You have none") // None case
)(f = (int: Int) => print("You have an int")) // Some case
Also, even though Scala has pattern matching, sometimes it can be more efficient to use a Visitor. See doobie internals:
// Interface ... encoded via the visitor pattern.
// This approach is much more efficient than pattern-matching for large algebras.
trait Visitor[F[_]] ...
Also, Java 21 comes with pattern matching. So, we'll see if it's time for Visitor to retire or not.
Speaking of folds
Iterator
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Multiple things here. Depends on what kind of aggregation we're talking about.
First, we have Foldable and Traversable type classes. Let's start with Foldable :
let numbers = [1, 2, 3, 4, 5]
length numbers -- 5
elem 3 numbers -- True
foldl (-) 0 numbers -- -10
sum numbers -- 15
Usage isn't limited to just lists; we can find Foldable instances for all sorts of things; we can even derive those for free:
data Tree a
= Empty
| Leaf a
| Node (Tree a) a (Tree a)
deriving Foldable
Traversable extends Foldable and abstracts away even more work for us:
let strings = ["1", "2", "3", "4"]
traverse readMaybe strings :: Maybe [Int] -- Just [1,2,3,4]
let badStrings = ["1", "2", "bad", "4"]
traverse readMaybe badStrings :: Maybe [Int] -- Nothing
fetchUserData :: Int -> IO String
apiExample :: IO ()
apiExample = do
let userIds = [1, 2, 3]
-- Fetch all users (sequential)
traverse fetchUserData userIds
Those two play a vital role in iterating over things. But I wouldn't stop there, because we also have Optics. They allow us to deal with pretty much any sort of aggregation and complexity. A lot to cover here. But here is my favorite example.
Imagine we have warehouses with stocks, and our task is to update all existing shoe stocks.
[
{
"inventory": {
"shoes": [
{
"name": "CHUCK TAYLOR ALL STAR LIFT",
"brand": "Converse",
"stock": 4
},
{
"brand": "Morrison"
}
]
},
"location": "London"
},
{
"location": "Berlin"
}
]
Something that we have to keep in mind: inventory is optional, name and stock are also optional.
newtype Response = Response {_warehouses :: [Warehouse]}
data Warehouse = Warehouse
{ _location :: Text
, _inventory :: Maybe Inventory
}
newtype Inventory = Inventory {_shoes :: [Shoes]}
data Shoes = Shoes
{ _name :: Maybe ShoesName
, _brand :: Brand
, _stock :: Maybe Stock
}
-- We omit optic derivation/generation for simplicity (see else where)
We start with composing optics to access what we need (existing stocks across warehouses and inventories):
_allShoes = warehouses . traverse . inventory . _Just . shoes . traverse
_existingStock = _allShoes . stock . _Just
And then we use it to update existing stocks:
over _existingStock (subtract 1) warehouseResponse
Technically, in this example, we are working with "underlying representation", but if we think of those snippets as library internals that expose only _allShoes or _existingStock, then we do provide a way to access the elements per pattern definition.
And then if the internal representation changes (for instance, there are multiple inventories per warehouse, inventory becomes inventories), nothing changes from the user's perspective.
_allShoes = warehouses . traverse . inventories . traverse . shoes . traverse
_existingStock = _allShoes . stock . _Just
-- No changes
over _existingStock (subtract 1) warehouseResponse
Mediator
Define an object that encapsulates how a set of objects interacts. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
Similar to Facade; for me, not a real design pattern. We shouldn't write spaghetti code.
Memento
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
In the presence of immutability, the state can't be modified, so there is no encapsulation to violate. History can be a list of (previous) states.
I used to work on a graph editor with PureScript, and we needed to keep a history of states to support undo. It was pretty much this:
type Editor =
{ current :: EditorState
, history :: Array EditorState -- "Memento"
}
undo :: Editor -> Editor
Well, it was more complicated but more ergonomic. We also wanted to support undo and redo (go back and forth in history), so we used Zipper. I don't remember if we used a library or wrote something custom, but it provides an api like this:
current :: forall a. ZipperArray a -> a
prev :: forall a. ZipperArray a -> Maybe a
next :: forall a. ZipperArray a -> Maybe a
Observer
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
This makes me think of functional reactive programming. I don't have much experience with it, so I'm unable to come up with an interesting illustration.
But I do have experience with simpler functional streams, like fs2, that provide primitives like Topic and Signal to implement the publish-subscribe pattern, and other ways to define dependencies between objects:
Topic[IO, String].flatMap { topic =>
val publisher = fooStream.through(topic.publish)
val subscriber = topic.subscribe(10).take(1)
subscriber.concurrently(publisher)
}
The next thing that comes to mind is event handling with api like this:
onEvent :: (a -> Effect Unit) -> Effect Unit
event :: a -> Effect Unit
Consider web events, such as button clicks, form updates, and key presses.
For example, this is an event handler from purescript-react-basic:
handler :: forall a. EventFn SyntheticEvent a -> (a -> Effect Unit) -> EventHandler
-- for simplicity, can be read as
handler :: SyntheticEvent -> (SyntheticEvent -> Effect Unit) -> EventHandler
Let's use it to update the credentials when someone types into the password form:
input
{ id: "password"
, type: "password"
, placeholder: "P4$$W0RD"
, required: true
, value: credentials.password
, onChange: handler targetValue (\value -> setPassword value)
}
And if you don't think that either fits as Observer, wait till you see what I have for the next one.
State
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Probably, the closest kinda-functional kinda-backend kinda-version of this is (Finite) State Machines.
┌─────────┐
│ Draft │
└────┬────┘
│ Send
▼
┌─────────┐
│ Sent │──────┐
└────┬────┘ │
│ Pay │ Cancel
▼ ▼
┌─────────┐ ┌───────────┐
│ Paid │ │ Cancelled │
└─────────┘ └───────────┘
sealed trait State
case object Draft, Sent, Paid, Cancelled extends State
sealed trait Event
case object Send, Pay, Cancel extends Event
(state, event) match {
case (Draft, Send) => ???
case (Sent, Pay) => ???
case (Sent, Cancel) => ???
case (Draft, Cancel) => error("Invalid state")
...
}
This is a very straightforward example, and there are many ways to implement a state machine, depending on the language, use case, and the rest of the code. There are libraries for this as well. For instance, there was a time when it was popular to use Akka FSM.
Let's take a step back and think of the State more abstractly. There are no objects in fp, so if we are talking about altering an object's behavior, we should be talking about altering the behavior of functions. In theory, we can make the behavior dependent on input parameters. Different parameters -- different behavior. However, as we discussed in the context of Structural patterns, at some point, we have to start using higher-level concepts and ways to organize code.
This is where the concept (or ability or whatever you want to call it) of State comes in. There are different implementations and approaches, but they all have the same interface:
-- Return the state (from internals of `m`)
get :: m s
-- Replace the state (inside the `m`)
put :: s -> m ()
There is the State data type in the standard library. There are various States in each effect library (for example, State in bluefin). Let's look at MonadState from the mtl library. I think it is the simplest one to use as an example, but the ideas apply to other libraries and implementations.
pay :: MonadState Invoice m => m ()
pay = do
invoiceState <- get
-- behavior depends on the internal state
if invoiceState.status == Sent
then put (invoiceState { status = Paid })
else pure () -- noop
We have a pay function that uses get and put state functions. This function is quite abstract -- we say that we don't care who or how is going to call it, as long as they implement the MonadState type class (get and put). They can store state in a single-threaded mutable variable, an atomic reference, or even on a disk; we don't care.
And note that we can also chain those operations:
-- modify is get + put in one operations
send :: MonadState Invoice m => m ()
send = modify (\(Invoice _) -> Invoice Sent)
cancel :: MonadState Invoice m => m ()
cancel = modify (\(Invoice Sent) -> Invoice Cancelled)
program = do
send
if someCondition
then pay
else cancel
I also want to look at the State from the frontend perspective. We might be getting even more off track, but this State makes me think of Elm architecture:
- Model — the state of your application
- View — a way to turn your state into HTML
- Update — a way to update your state based on messages
If you squint, it does alter its behavior when its internal state changes. And even though the basic description of the pattern doesn't mention how the state changes, here the state is changed via messages, which is very oop.
You can see something similar in lustre, a framework for building web apps in Gleam:
... the state of the application is stored in a single, immutable data structure called the model, and updated as messages are dispatched to the runtime.
I've mostly used it via useAffReducer hook purescript-react-basic-hooks :
useAffReducer ::
forall state action.
state ->
AffReducer state action ->
Hook (UseAffReducer state action) (state /\ (action -> Effect Unit))
The most interesting part is at the end: state + (action -> Effect Unit)
Provide an initial state and a reducer function
Strategy
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Finally, a design pattern that can be replaced by plain functions.
val processPaymentStripe = (payment: Payment) => { ??? }
val processPaymentPayPal = (payment: Payment) => { ??? }
If we need to pass a payment processor, we can pass a function. That's it.
Template Method
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
Either I'm getting tired, or this could be similarly replaced by plain functions. We can pass "steps" via functions. And if we need something more complicated, we can use one of the many approaches we discussed before.
val processPaymentStripe = (payment: Payment) => { ??? }
val processPaymentPayPal = (payment: Payment) => { ??? }
.
Top comments (1)
Wonderful. The intro is true. The rest is exhaustive. Bravo!