- Esoteric?
- Using lots of parentheses?
- Insanely generic?
A: Programming with Functions
A: Aggressively programming with functions.
- Separation of Data, and Behaviour (algorithms).
- Ensure it is impossible to represent an illegal state.
sealed trait Chain
final case class Link(next: Chain) extends Chain
case object End extends Chain
@tailrec def length(l: Chain, acc: Int = 0): Int = l match {
case Link(n) => length(n, acc + 1)
case End => acc
}
// length(Link(Link(Link(End)))) -> 3
public class PositiveInteger {
private int _value;
public PositiveInteger(final int value) {
// WHAT DID YOU EXPECT!?!
if (value < 0) System.exit(1);
_value = value;
}
public int getValue() { return _value; }
}
NOTE: Analogy of writing this slideshow, and learning FP
- Mapping from SOLID to FP.
- An Overview of Esoteric Nomenclature
- The M-Word
- Writing Higher-Kinded Interfaces
- Property Based Testing
The Laws of software design trivially map to FP
Only one potential change in the application should affect the specification of a thing.
Functions
Things should be open for extension, but closed for modification.
Functions
Things should be replaceable with instances of their subtypes without altering the correctness of the program.
Functions
Many client-specific interfaces are better than one general-purpose interface.
Functions
One should depend upon abstractions, not concrete implementations.
Functions
Term | Description |
---|---|
Function | Maps one type, to another. |
Type Class | Generic Interface. |
Kind | The type of a Type. What happens when types level-up. |
Category | A math graph; hard to visualise. |
Functor | Read the docs |
Monad | When used correctly people will get off your lawn. |
val f: Int => Int = _ + 1
Function<Integer, Integer> f = (int x -> x + 1)
f = lambda x: x + 1
f :: Integer -> Integer
f x = x + 1
trait Show[T] { def show(t: T): String }
public interface Show<T> {
public String show(final T t);
}
def show(t): return str(t)
class Show t where
show :: t -> String
import scala.language.higherKinds
type Type // 0-th order kind. a regular type
type Constructor[T] // 1-st order kind.
type Factory[F[_]] // 2-nd ...
type Industry[F[_[_]]] // yes, this is a thing.
// Doesn't work in java.
# No idea how this would be done in python. Meta-classes?, probably meta-classes.
*
* -> *
* -> * -> *
CanBuildFrom[-From, -Elem, +To] // A base trait for builder factories.
Functor[F[_]] // Functors between categories form a category.
// Allows us to apply a function within a context `F`.
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Allows us to wrap a value in a context `A`.
trait Applicative[A[_]] extends Functor[A] {
def apply[X](a: X): A[X]
def ap[X, Y](ax: A[X])(f: A[X => Y]): A[Y]
}
// Allows us to 'merge' nested contexts together.
trait Monad[M[_]] extends Applicative[M] {
def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
def join[A](mma: M[M[A]]): M[A]
}
The ultimate abstraction:
final case class Scored[T](item: T, score: Int)
type Scorer[T, F[_]] = T => F[Scored[T]]
type Selector[T] = Set[Scored[T]]] => Option[T]
type Auctioneer[T, F[_]] = Seq[T] => F[Option[T]]
object Auctioneer {
def apply[T, F[_] : Applicative](
scorer: Scorer[T, F],
selector: Selector[T]
): Auctioneer[T, F] = { ts =>
val scored: Seq[F[Scored[T]]] = ts.map(scorer)(breakOut)
Applicative.sequence(scored).map(selector)
}
}
The above code doesn't work, but instead forces us into using 'package' objects to export/use the types. This is either a bug or a feature; but forces a slightly different (header-oriented) style:
package object awesome {
type Scorer[T, F[_]] = T => F[Scored[T]]
type Selector[T] = Set[Scored[T]] => Option[T]
type Auctioneer[T, F[_]] = Set[T] => F[Option[T]]
}
Lets imagine we have an injection type class:
// LAW:
// reverse.compose(forward)(x) must be Some(x) forAll x in A.
final case class Injection[A, B](
forward: A => B,
reverse: B => Option[A]
)
Seems legit ...
val IntMagic = Injection[Int, Int](x => x * 4, x => Some(x / 4))
We could even test it:
// useless test that I see everywhere....
"IntMagic should obey the injection law" in {
import IntMagic._
reverse.compose(forward)(4) shouldBe (Some(4))
}
There is a terrible problem here, because Integer division is not invertible. Lets just test all possible inputs (it is so very easy).
import org.scalacheck.Arbitrary._
"IntMagic should obey the injection law" in {
import IntMagic._
val roundTrip = reverse.compose(forward)
forAll { x: Int =>
roundTrip(x) shouldBe (Some(x))
}
}
We can/should abstract this Law into a generic Property test!
import org.scalacheck.{ Arbitrary, Prop }
object InjectionLaw {
// This is SO EASY IT HURTS, seriously PLEASE DO THIS
def apply[A : Arbitrary, B](inj: Injection[A, B]): Prop = {
import inj._
val roundTrip = reverse.compose(forward)
Prop.forAll { a: A => roundTrip(a) == Some(a) }
}
}
// Elsewhere!
"IntMagic should obey the injection law" in {
InjectionLaw(IntMagic)
}
- Everything I've said is unambiguously correct
- Type away your problems (get it?!)
- Properties are easy to test, so please do.