Compare commits

...

4 Commits
main ... drafts

  1. 22
      ci/pipeline.yaml
  2. 19
      ci/tasks/build-site.yaml
  3. 167
      site/_drafts/functors-applicative-and-monads.md

22
ci/pipeline.yaml

@ -0,0 +1,22 @@
resources:
- name: site-drafts
type: git
source:
uri: https://bitsof.thisfieldwas.green/keywordsalad/thisfieldwas.green.git
branch: drafts
- name: haskell-image
type: registry-image
source:
repository: haskell
tag: '8.10.7'
jobs:
- name: build-site-job
plan:
- get: site-drafts
params: { depth: 1 }
trigger: true
- get: haskell-image
- task: build-site-task
image: haskell-image
file: ./site-drafts/ci/tasks/build-site.yaml

19
ci/tasks/build-site.yaml

@ -0,0 +1,19 @@
platform: linux
inputs:
- name: site-drafts
outputs:
- name: site-output
run:
path: sh
args:
- -cx
- |
set -eux
cd site-drafts
./go build
./go test
cd ..
zip -r build.zip site-drafts
mkdir site-output
mv build.zip site-output

167
site/_drafts/functors-applicative-and-monads.md

@ -0,0 +1,167 @@
---
title: Functors, Applicatives, and Monads
author: Logan McGrath
comments: false
date: 2022-01-12T08:13:05-0800
tags: functional programming, functors, applicatives, monads, scala
layout: post
---
During my time that I have worked with Functors, Applicatives, and Monads I have accumulated some amount of mnemonics and metaphors so that I can better retain their concepts and understand how they work. I like to call these "personal metaphors" as I have mostly kept them to myself out of some measure of embarrassment, but in this post I am going to share what these metaphors are as I hope someone may find some value in them.
Allow me to describe Functors, Applicatives, and Monads using my own words to support correct terminology.
## Conventions
1. I will be using poorly drawn cartoon illustrations to illuminate concepts in a relatable manner.
2. Where concepts need to be more concrete, I will provide code in the form of pseudo-Scala which may or may not compile.
3. Terminology that I will be using may touch as high as academia but will remain more broadly within the scope of applied functional programming.
4. I will be applying my personal mnemonics to terms that I have struggled to retain. These mnemonics will be provided in the form of word choice and accompanied by illustration to aid in making these concepts accessible.
5. Where there is conceptual overlap with terminology used in object oriented programming, I will leverage those terms to drive the intentions behind functional programming abstractions.
## Programs as functions
In functional programming, the unit of computation is any function `f: A => B`.
A program can be thought of as a _function in the large_, and once implementation details are factored out it can also be reduced in representation as if it were a function `f: A => B`.
Functions may be composed from smaller functions. Taking two functions, `f: A => B` and `g: B => C`, it follows that a third function `h: A => C` exists such that it is composed as `h: g ∘ f` or defined as `h(x) = g(f(x))`. Programs themselves are compositions of numerous smaller functions to produce larger units of more complex behavior.
As behavior becomes complex, functions may become complex as well. This can lead to _tight coupling_ of behavior and implementation, which leads to brittle code. Imagine in the extreme case: code that has to interact with a database connection. Business logic in the context of a database-driven application is orthogonal to the type of database used, as well as to the fact that a database is used at all. Ideally, that a database is used should be an implementation detail that is abstracted away from the programmer so that the business logic remains the primary focus of their work.
There is a simple way to abstract these two concerns, and that is by reasoning about the database connection as a _context_.
Illustration
: Business logic as an embedded function
: Database connection as a context, with the function embedded within
## Contexts and Effects
What is a context? A context is a setting within which some atom of type `A` might be produced. Atoms may be of any type uniquely identified by a letter or word, `A` beind a common case. Contexts likewise may be nicely represented as a letter and brackets such as `F[_]` when the type of the atom is unknown, and `F[A]` when the type of the atom is known to be `A`. Other letters work nicely of course, as do words.
Each kind of context can be described as having a unique set of effects. These effects burden themselves upon a context like gremlins pulling at levers that _effect_ how an instance of an atom may be produced. Names of contexts may hint at the baggage imposed on them by these gremlins. For example:
`List[A]`
: Nondeterminism of sort, cardinality, and size of the instances of `A`
`Either[X, A]`
: Presence of `A` if valid
`Option[A]`
: Presence or absense of `A`
`IO[A]`
: Acquiring an `A` triggers side effects outside of the function
`Future[A]`
: Temporal nondeterminism of the acquisition of `A`
: Acquiring an `A` triggers side effects outside of the function
There is a shape that manifests itself across the contexts listed above. By applying a rule borrowed from object oriented programming, _"abstract what changes"_, then the shape of these structures reduces very neatly into a simple representation by `F[A]`.
Illustration
: Concrete representations of contexts
: Contexts as a box accompanied by gremlins
This same rule applies to the unique sets of effects for each of these contexts, and a remarkable thing happens: the effects merely become acknowledgeable as an implementation detail. They may also be abstracted away, and to such an extreme that they are very much ignored.
Illustration
: Contexts with gremlins
: A sole context without gremlins
By reducing any context to the simplest _shape_ of `F[A]` and shedding its effects, it would appear that there isn't much left to work with, however atoms still need to be consumed in order for them to be useful. How is a context opened up so that their atoms may be consumed?
## Motivating Functors
Given a `List[A]`, each instance of the atom `A` may be consumed by iterating over the list. Likewise, an `Option[A]` provides a straightforward `get()` operation to consume the atom. Accessing the atoms held within `Future[A]` and `IO[A]` isn't nearly as straightforward, however. Their side effects are destructive, might block execution of the program, and there's no guarantee that an instance of an atom will materialize. It would appear that there's no general operation across these types through which their atoms may be consumed!
There is however an abstraction that solves this very problem. This abstraction is called a `Functor` and it provides a single function called `map()`:
```scala
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
object Functor {
def apply[F: Functor]: Functor[F] = implicitly[Functor[F]]
}
```
How does this enable consumption of atoms produced by a context? A `Functor` containing an atom of type `A`, when given a function `A => B`, will _lift_ the function into its context using the `map()` function and apply its atom to the function. A new context containing the results is returned. Here are some examples of what using this operation looks like:
```scala
Functor.map(List(1, 2, 3))(x => x * 2)
// => List(1, 4, 6)
Functor.map(Option("hello"))(s => s + " world")
// => Some("hello world")
Functor.map(Either.asRight[Unit](42))(x => x / 2)
// => Right(21)
```
What would happen if the context carried no instances of the atom?
```scala
Functor.map(List())(x => x * 2)
// => List()
Functor.map(Option())(s => s + " world")
// => None
Functor.map(Either.asLeft[Int](()))(x => x / 2)
// => Left(())
```
If there is no instance of the atom, then is follows that the function lifted into the `Functor` wouldn't be applied to anything. The `map()` function thus returns a context that simply has _nothing here_, and the lifted function isn't used. This behavior is referred to as _short circuiting_ and is used to model error handling, as contexts that are in error states contain no data to operate against.
For `List`, `Option`, and `Either` their respective implementations of `Functor` may look like this:
```scala
object Functors {
implicit val listFunctor: Functor[List] =
new Functor[List] {
def map[A, B](fa: List[A])(f: F => B): List[A] =
fa match {
case x :: xs => f(x) :: map(xs)(f)
case Nil => Nil // nothing here
}
}
implicit val optionFunctor: Functor[Option] =
new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] =
fa match {
case Some(x) => Some(f(x))
case None => None // nothing here
}
}
implicit def eitherFunctor[X]: Functor[Either[X, _]] =
new Functor[Either[X, _]] {
def map[A, B](fa: Either[X, A])(f: A => B): Either[X, B] =
fa match {
case Right(x) => Right(f(x))
case l@Left(_) => l // nothing here
}
}
}
```
Attention is called to above cases where there is _nothing here_: each context contains no instances of `A` for those cases. Additionally each context is burdened by some nuance imposed by their effects:
`List`
: The `map()` function applies `f` to each element within the `List` and returns a new list containing the results.
: The `map()` function is recursively defined with _nothing here_ as the base case. This means that empty lists do not apply `f` and `map()` thus returns an empty list.
`Option`
: As an `Option` is strictly presence or absence of an instance of an atom, `f` will apply if the atom is present only.
`Either`
: An `Either` is a special case of `Option` where if a `Right` atom is not present, then an alternate `Left` atom will be present instead.
: The `Left` atom is the _nothing here_ case, but unlike a `None` it is able to carry data. This allows an `Either` to embed error handling information which may be extracted later.
For all of these types, using `Functor` with `map()` allows a complete abstraction away from the specific type of the context. Using `Functor` also shows that logic occurs strictly within the context, as the function `f` applied to `map()` is called by the context implementation and its results handled by the implementation as well. The only space allowed for variance is the specific handling for the individual atom with the scope of `f`.
This abstraction works for individual atoms within a context, but what if the atoms from two insances of a context needed to be operated with together?
## Motivating Applicatives
Loading…
Cancel
Save