Well-architected libraries for functional programming are at once immensely beautiful and practical. They are simple but extraordinarily powerful, helping users solve their problems by snapping together Lego-like building blocks, each of which has just one purpose. Yet, there is a surprising dearth of material on how developers can construct their own well-architected functional code. Many functional programming tutorials talk discuss type safety and making illegal states unrepresentable, but few speak on the subject of good functional interface design.
In this presentation, John A. De Goes takes to the stage to discuss a nebulous and underrated tool in the arsenal of every functional programmer. Called *orthogonality*, this tool allows programmers to craft the building blocks of their functional code at "right angles", so so they can be reasoned about simply and composed predictably to solve complex problems. John introduces the concept of orthogonality, looking at its geometric and algebraic origins, presents a way to measure orthogonality, and then walks through a number of interface examples, comparing non-orthogonal designs with orthogonal ones.
By the end of the session, attendees should have a newfound appreciation for how important orthogonality is to constructing good functional interfaces, and they should develop the early stages of an intuition about how to slice up a complex problem into core, single-purpose, composable building blocks.
7. Two Keys to Bliss
Orthogonality + Composability
8. Two Keys to Bliss
Composability makes functional
code powerful, and
orthogonality makes it
beautiful*
*i.e. modular and uncluttered by irrelevant details.
9. Two Keys to Bliss
Composable, orthogonal bases
tend to be powerful, but small
and simple to reason about,
permitting flexible, modular
solutions to many problems.
21. Orthogonality
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws
InterruptedException, ExecutionException, TimeoutException;
}
Is this orthogonal?
22. Orthogonality
public interface Future<V> {
boolean cancel(
boolean mayInterruptIfRunning);
boolean isDone();
V get() throws
InterruptedException,
ExecutionException;
V get(long timeout, TimeUnit unit)
throws
InterruptedException,
ExecutionException,
TimeoutException;
}
Is this orthogonal?
Timeout
Get
Timeout + Get
23. Orthogonality
The cardinal sin of
non-orthogonality is the
tangling of separate concerns,
which infects the code base to
destroy modularity.
31. Worked Examples
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException,ExecutionException,
TimeoutException;
}
Is this orthogonal?
32. Worked Examples
The curse of non-orthogonality!
// Would like to write:
<A> Future<A> retryUntilSuccess(Future<A> future, Interval spacing) {
// ???
}
// Forced to write:
A retryUntilSuccess(Future<A> future, Interval spacing,
long timeout, TimeUnit unit) {
// ...
}
33. Worked Examples
public interface Future<V> {
Future<Tuple2<Future<V>, Function<Boolean, Future<Unit>>>> fork();
V unsafePerformGet() throws InterruptedException, ExecutionException;
Future<V> timeout(long timeout, TimeUnit unit);
}
Future 2.0: Detangle ‘get’ and ‘timeout’
34. Worked Examples
Is this orthogonal?
static int compare(
String str1,
String str2,
boolean nullIsLess)
35. Worked Examples
Is this orthogonal?
NullIsLess +
Comparison
NullIsLess
Comparison
static int compare(
String str1,
String str2,
boolean nullIsLess)
36. Worked Examples
The curse of non-orthogonality!
List<String> myAlgorithm(List<String> list, boolean nullIsFirst) {
// ...
int c = compare(first, second, nullIsFirst);
// ...
}
37. enum Ordering { LT, EQ, GT }
interface Ord<A> {
Ordering compare(A l, A r);
}
Worked Examples
1. Define a unit of composition
38. class StringOrd extends Ord<String> {
public Ordering compare(String l, String r) {
// ...
}
}
Worked Examples
2. Define one dimension (string comparison)
39. class NullIsLess<A> {
private final Ord<A> ord;
public NullIsLess(Ord<A> ord) {
this.ord = ord;
}
public Ordering compare(A l, A r) {
if (l == null) { if (r == null) return EQ; else return LT; }
else if (r == null) return GT;
else return ord.compare(l, r);
}
}
Worked Examples
3. Define another dimension (null is first)
40. List<String> myAlgorithm(List<String> list, Ord<String> ord) {
// ...
Ordering c = ord.compare(first, second); // <- Beautiful!!!
// ...
}
Ord<String> ord = new NullIsLess<String>(new StringOrd());
List<String> list2 = myAlgorithm(list, ord);
Worked Examples
4. Compose orthogonal components
41. Worked Examples
Is this orthogonal?*
data MVar a
putMVar :: MVar a -> a -> IO ()
takeMVar :: MVar a -> IO a
*Thanks to Fabio the fabulous for this example.
42. Worked Examples
Is this orthogonal?
data MVar a
putMVar :: MVar a -> a -> IO ()
takeMVar :: MVar a -> IO a
Synchronization +
Concurrency
Synchronization
Concurrency
43. Worked Examples
data IORef a
newtype Expect a = Expect a
modify ::
IORef a -> (a -> a) -> IO Bool
data Promise a
newPromise :: IO (Promise a, a -> IO ())
awaitPromise :: Promise a -> IO a
Synchronization
(IORef)
Concurrency
(Promise)
46. Worked Examples
Is this orthogonal?
data Parser a = Parser (String -> Either String (String, a))
char :: Parser Char
fail :: String -> Parser a
alt :: Parser a -> Parser a -> Parser a
seq :: Parser a -> (a -> Parser b) -> Parser b
pure :: a -> Parser a
map :: (a -> b) -> Parser a -> Parser b
47. Worked Examples
Is this orthogonal?*
data Parser a = Parser (String -> Either String (String, a))
char :: Parser Char
fail :: String -> Parser a
alt :: Parser a -> Parser a -> Parser a
seq :: Parser a -> (a -> Parser b) -> Parser b
pure :: a -> Parser a
map :: (a -> b) -> Parser a -> Parser b
*Trick question — or is it?
48. Worked Examples
Is this orthogonal?
data Parser a = Parser (String -> ...
char :: Parser Char
fail :: String -> Parser a
alt :: Parser a -> Parser a -> Parser a
seq :: Parser a -> (a -> Parser b) -> Parser b
pure :: a -> Parser a
map :: (a -> b) -> Parser a -> Parser b
SequencingMapping
Flattening
49. Worked Examples
Is this orthogonal?
data Parser a = Parser (String -> ...
char :: Parser Char
fail :: String -> Parser a
alt :: Parser a -> Parser a -> Parser a
join :: Parser (Parser a) -> Parser a
pure :: a -> Parser a
map :: (a -> b) -> Parser a -> Parser b
Mapping
Flattening
51. Summary
1. Functional code is composable and orthogonal, allowing a small set of
principled building blocks to snap together to solve complex problems in
a predictable, reasonable way.
2. Composability measures the combinability of values.
3. Orthogonality measures the singular focus of primitive operations.
4. Refactor to orthogonality to obtain modular clode, uncluttered by
irrelevant details.
52. Thank You!
Special thanks to Reid, Cameron, Emily, & the wonderful Knoxville FP
community, and the generous sponsor ResultStack!
John A. De Goes — @jdegoes
http://degoes.net