30
First-Class Patterns John A. De Goes - @jdegoes Frontier Developers, February 26

First-Class Patterns

Embed Size (px)

DESCRIPTION

Some languages, like SML, Haskell, and Scala, have built-in support for pattern matching, which is a generic way of branching based on the structure of data. While not without its drawbacks, pattern matching can help eliminate a lot of boilerplate, and it's often cited as a reason why functional programming languages are so concise. In this talk, John A. De Goes talks about the differences between built-in patterns, and so-called first-class patterns (which are "do-it-yourself" patterns implemented using other language features). Unlike built-in patterns, first-class patterns aren't magical, so you can store them in variables and combine them in lots of interesting ways that aren't always possible with built-in patterns. In addition, almost every programming language can support first-class patterns (albeit with differing levels of effort and type-safety). During the talk, you'll watch as a mini-pattern matching library is developed, and have the opportunity to follow along and build your own pattern matching library in the language of your choice.

Citation preview

Page 1: First-Class Patterns

First-Class PatternsJohn A. De Goes - @jdegoes

Frontier Developers, February 26

Page 2: First-Class Patterns

Agenda● Intro● Pattern Matching 101● First-Class-ness 101● Magical Patterns● First-Class Patterns● Exercises

Page 3: First-Class Patterns

IntroPattern Matching

● Divides a (possibly infinite) set of values into a discrete number of cases, where each case can be handled in a uniform way

● “if” on steroids○ Sometimes strictly more powerful (e.g. Haskell)

Page 4: First-Class Patterns

Intro - Examples-- sign of a number

sign x | x > 0 = 1 | x == 0 = 0 | x < 0 = -1

-- take the first n elements from a list

take 0 _ = []take _ [] = []take n (x:xs) = x : take (n-1) xs

-- generate some javascript

valueToJs :: Options -> ModuleName -> Environment -> Value -> JSvalueToJs _ _ _ (NumericLiteral n) = JSNumericLiteral nvalueToJs _ _ _ (StringLiteral s) = JSStringLiteral svalueToJs _ _ _ (BooleanLiteral b) = JSBooleanLiteral b

...

Page 5: First-Class Patterns

Intro - Examplessealed trait Level

case object Level1 extends Level

case object Level2 extends Level

sealed trait Title

case object DBAdmin extends Title

case class SWEngineer(level: Level) extends Title

case class Employee(manager: Option[Employee], name: String, title: Title)

val employees = ???

val selfManagedLevel2Engineers = employees.collect {

case Employee(None, name, SWEngineer(Level2)) => name

}

Page 6: First-Class Patterns

Pattern Matching 101

Filter

Extract

Does it have the structure I want? [Yes/No]

If so, extract out the pieces that are relevant to me.

Page 7: First-Class Patterns

Pattern Matching 101-- take the first n elements from a list

take 0 _ = []take _ [] = []take n (x:xs) = x : take (n-1) xs

Filter - does it have non-empty list structure?

Extract - Give me ‘head’ and ‘tail’

Page 8: First-Class Patterns

Pattern Matching 101Products Sumscase class Employee(

manager: Option[Employee],

name: String,

title: Title

)

sealed trait Title

case object DBAdmin extends Title

case class SWEngineer(level: Level)

extends Title

class Account {

...

public Account(BigDecimal balance, User holder) {

...

}

}

interface Shape { }

class Rect extends Shape { … }

class Ellipse extends Shape { … }

class Pentagon extends Shape { … }

terms terms

Page 9: First-Class Patterns

First-Class-ness 101

data Maybe a = Nothing | Just a deriving (Eq, Ord) class Person {

public Person(String name, int age) {

...

}

}

Page 10: First-Class Patterns

First-Class-ness 101

Magic interferes with your ability to abstract and compose.

Page 11: First-Class Patterns

Magical PatternsOrdinary Duplicationsealed trait Provenanceobject Provenance { … case class Either(left: Provenance, right: Provenance) extends Provenance case class Both(left: Provenance, right: Provenance) extends Provenance

def allOf(xs: Seq[Provenance]): Provenance = { if (xs.length == 0) Unknown else if (xs.length == 1) xs.head else xs.reduce(Both.apply) }

def anyOf(xs: Seq[Provenance]): Provenance = { if (xs.length == 0) Unknown else if (xs.length == 1) xs.head else xs.reduce(Either.apply) }}

Page 12: First-Class Patterns

Magical PatternsFactoringsealed trait Provenanceobject Provenance { case object Unknown extends Provenance case object Value extends Provenance case class Relation(value: SqlRelation) extends Provenance case class Either(left: Provenance, right: Provenance) extends Provenance case class Both(left: Provenance, right: Provenance) extends Provenance

def allOf(xs: Seq[Provenance]): Provenance = reduce(xs)(Both.apply)

def anyOf(xs: Seq[Provenance]): Provenance = reduce(xs)(Either.apply)

private def reduce(xs: Seq[Provenance])(f: (Provenance, Provenance) => Provenance): Provenance = { if (xs.length == 0) Unknown else if (xs.length == 1) xs.head else xs.reduce(f) }}

Page 13: First-Class Patterns

Magical PatternsFactoringz sealed trait Provenance object Provenance { case object Unknown extends Provenance case object Value extends Provenance case class Relation(value: SqlRelation) extends Provenance case class Either(left: Provenance, right: Provenance) extends Provenance case class Both(left: Provenance, right: Provenance) extends Provenance

private def strict[A, B, C](f: (A, B) => C): (A, => B) => C = (a, b) => f(a, b)

val BothMonoid = Monoid.instance(strict[Provenance, Provenance, Provenance](Both.apply), Unknown) val EitherMonoid = Monoid.instance(strict[Provenance, Provenance, Provenance](Either.apply), Unknown)

def allOf(xs: List[Provenance]): Provenance = Foldable[List].foldMap(xs)(identity)(BothMonoid)

def anyOf(xs: List[Provenance]): Provenance = Foldable[List].foldMap(xs)(identity)(EitherMonoid) }

Page 14: First-Class Patterns

Magical PatternsToxic Duplication - Abstraction Fail val Add = Mapping("(+)", "Adds two numeric values", NumericDomain,

(partialTyper {

case Type.Const(Data.Number(v1)) :: v2 :: Nil if (v1.signum == 0) => v2

case v1 :: Type.Const(Data.Number(v2)) :: Nil if (v2.signum == 0) => v1

case Type.Const(Data.Int(v1)) :: Type.Const(Data.Int(v2)) :: Nil =>

Type.Const(Data.Int(v1 + v2))

case Type.Const(Data.Number(v1)) :: Type.Const(Data.Number(v2)) :: Nil =>

Type.Const(Data.Dec(v1 + v2))

}) ||| numericWidening

)

Page 15: First-Class Patterns

Magical PatternsPattern Combinators - Composition Fail

● Product & sum○ PartialFunction.orElse

● Negation● Defaults● Use-case specific

Page 16: First-Class Patterns

Magical Patterns

Magical patterns limit your ability to abstract over patterns and to

compose them together to create new patterns.

Page 17: First-Class Patterns

First-Class Patterns

First-class patterns are built using other language features so you can

abstract and compose them.

Page 18: First-Class Patterns

First-Class PatternsHaskell Exampleex4 :: Either (Int,Int) Int -> Int

ex4 a = match a $

(1+) <$> (left (pair var (cst 4)) ->> id

<|> right var ->> id)

<|> left (pair __ var) ->> id

http://hackage.haskell.org/package/first-class-patterns

Page 19: First-Class Patterns

First-Class Patterns“For the rest of us”

X match {

case Y => Z

}

type Pattern???

Page 20: First-Class Patterns

First-Class PatternsStructure

X match {

case Y => Z

}

type Pattern???

Input

Output

Extraction

Page 21: First-Class Patterns

First-Class PatternsOne ‘Option’

X match {

case Y => Z

}

type Pattern[X, Z] = X => Option[Z]

Input

Output

Extraction

Page 22: First-Class Patterns

First-Class PatternsBasic Patterns

def some[A, B](p: Pattern[A, B]): Pattern[Option[A], B] = _.flatMap(p)

def none[A, B]: Pattern[A, B] = Function.const( None)

def k[A](v0: A): Pattern[A, A] = v => if (v == v0) Some(v0) else None

def or[A, B](p1: Pattern[A, B], p2: Pattern[A, B]): Pattern[A, B] = v => p1(v).orElse(p2(v))

scala> or(none, some(k( 2)))(Some(2))res3: Option[Int] = Some(2)

https://gist.github.com/jdegoes/9240971

Page 23: First-Class Patterns

First-Class Patterns - JS

http://jsfiddle.net/AvL4V/

And Now in JavaScript….Because

Page 24: First-Class Patterns

First-Class PatternsLimitations

● Extractors cannot throw away information, leading to ‘dummy parameters’

● Negation not possible under any circumstances

● No partiality warnings or built-in catch all

Page 25: First-Class Patterns

Exercises1. Define a Pattern Combinator to Fix: val Add = Mapping("(+)", "Adds two numeric values", NumericDomain,

(partialTyper {

case Type.Const(Data.Number(v1)) :: v2 :: Nil if (v1.signum == 0) => v2

case v1 :: Type.Const(Data.Number(v2)) :: Nil if (v2.signum == 0) => v1

case Type.Const(Data.Int(v1)) :: Type.Const(Data.Int(v2)) :: Nil =>

Type.Const(Data.Int(v1 + v2))

case Type.Const(Data.Number(v1)) :: Type.Const(Data.Number(v2)) :: Nil =>

Type.Const(Data.Dec(v1 + v2))

}) ||| numericWidening

)

EASY

Page 26: First-Class Patterns

Exercises2. Define ‘Pattern’ and some core patterns in the language of your choice

EASY

Page 27: First-Class Patterns

Exercises3. Define an ‘And’ pattern that requires both inputs match

EASY

Page 28: First-Class Patterns

Exercises4. Define an alternate definition of pattern (along with a few core patterns) that permits pattern negation

4.b Optional: Use this to allow catch-alls

MODERATE

Page 29: First-Class Patterns

Exercises5. Define an alternate definition of pattern (along with a few core patterns) that permits extractors to throw away information

HARD

Page 30: First-Class Patterns

THANK YOUFirst-Class Patterns

John A. De Goes - @jdegoesFrontier Developers, February 26