Applying stuff from PL and type theory
Benefits of types
!
Correctness
Types enforce logical correctness
// ages have to be numbers
// your age cannot be "carrot"
val age: Int = 34
// in un-typed languages your age can be "carrot"
// that sucks
Your compiler reasons about your code. Instead of just you
This is the obvious one
Documentation
Types tell you what your code does
If you change your "documentation", your code won't compile until it matches!
// what does this function do?
func concat(x: String, x: String): String
// what does this function do?
func concat(x: [Int], y: [Int]): [Int]
If you change code attached to a comment,
the compiler won't stop you
/**
* Concat two strings to make a larger one
*/
var z = x + y
Whenever I find myself writing a comment I always think, can I express this clearly with better types or another function?
Reuse + Testability
Types help you write composable code
// after
func prefetch(photo: Photo, /*...*/): Eventually<PrefetchedPhoto>
In Roll, the we have a step where we prefetch photos for the cards in the feed.
This is what the code used to look like:
Handling the prefetch finishing wasn't exposed in the type signature.
We wanted to reuse this logic for getting photos for notifications:
// before
func prefetch(photo: Photo, /*...*/)
Now we can do whatever we want with the result!
Performance
How do untyped languages work?
var x = 4 // computer: x is a number
var y = x + 1 // computer: is x a number? good. y is a number
println(fib(y)) // computer: is fib a function? does fib return something I can print? good
They're actually uni-typed of type
"you can ask me what I really am"
Types let the computer solve these problems at compile time
Reading type theory
it's a science!
The Rules of Rules + Guide
(above the line) implies (below the line)
Γ = context (other things you proved)
Γ,(Γ1) = extend your context with Γ1
⊢ = entailment
stuff right of ⊢ = judgement
These are common axioms:
if J is in your context, then J is true
if J is true, then you can weaken your context (add judgements) and J is still true
if K is true, and if you show J with K, you can treat GAMMA as having K later
Non-recursive types
basically primitives
Zero
(type theory void)
// Swiftish
func f(x: Int) -> Void {
while (true) { }
}
aka void, bottom, or ⊥
T -> Void functions diverge
One
(the void in Java)
// in Kotlin `Unit` is both a type and a value
val x: Unit = Unit
// in Swift `Void` or `()` is the type of Unit, `()` is the value
let x: () = ()
aka unit, 1
Functions where you omit a return statement and that don't diverge are actually return unit
// f : Int -> Unit
func f(x: Int) {
println("You passed in ${x}")
}
Everything else
(Bool (or 2), Int, String, etc.)
val x: Int = 0
val s: String = ""
// Bools are also known as 2 because there are two choices
val y: Bool = false
These types exist obviously but they usually don't matter unless you talk about something extremely specific
Product types
(tuples / structs / classes)
Product types
// kotlin
val oneTwo: Pair<Int, Int> = 1 to 2 // intro
println(oneTwo.first) // elim
data class Person(name: String, age: Int)
// swift
struct Person { name: String, age: Int }
let p: Person = Person(name: "Doug", age: 34) // intro
println(p.name) // elim
Why product?
How many values exist of type Bool x Unit?
What about Bool x Bool?
What about Bool x (Bool x Unit)
(T1 x T2)
intro
elim
Sum types
(tagged unions / good enums)
Sum types
// swift
enum ViewCell {
case HeaderCell(title: String, childrenCount: Int),
case ContactCell(name: String, phoneNumber: String),
/* etc */
}
This is what I call a "good" enum
let v: ViewCell = HeaderCell("Suggested Invites", 9) // intro
switch v { // elim
case HeaderCell(let title, _): println("This is ${title}")
case ContactCell(_, let num): println("Contact me at ${num}")
}
intro
elim
(T1 + T2)
Sum types
// kotlin
enum Color(val rgb: Int) {
Red(0xF00),
Green(0x0F0),
Blue(0x00F)
}
This is what I call a "bad" enum
Notice that I didn't give the parameters in the cases names. The rgb value MUST be included for all cases.
You can only declare types of the form p1 + p1 not p1 + p2 for any products p1,p2
Sum types
// kotlin
object ViewCell {
abstract class Sum{
fun match<T>(
headerCell: (HeaderCell) -> T
contactCell: (ContactCell) -> T
): T {
when (this) {
is HeaderCell -> return headerCell(this)
is ContactCell -> return contactCell(this)
else -> throw MatchException()
}
}
}
data class HeaderCell(val title: String, val children: Int): Sum()
data class ContactCell(val name: String, val phoneNumber: String): Sum()
}
Hacking in "good" enums
val x: ViewCell.Sum = HeaderCell("Suggested Invites", 9) // intro
// elim
viewCell.match(
headerCell = { h -> println("My title is ${h.title}") }
contactCell = { c -> println("My name is ${c.name}") }
)
Option type
// swift/kotlin (we write Unit + Int as Int?)
// and we use null or nil to get the left projection
// and some value that's not null or nil to get the right projection
val imNotHere: Int? = nil
val imHere: Int? = 3
(Unit + T1)
Option types are sum types where the left projection is Unit !
Sloppy Products
struct SloppyCar {
hasRoof: Bool
roofColor: Color?
/* other stuff */
}
Why is this bad? (contrived example but whatever)
struct Roof {
color: Color
}
struct GoodCar {
roof: Roof?
}
Specifically, sloppy car permits
SloppyCar{hasRoof: false, roofColor: Red}
Good car does not!
Sloppy Products
struct SloppyViewCell {
headerCell: HeaderCell?
contactCell: ContactCell?
}
Why is this bad? (contrived example but whatever)
enum GoodViewCell {
case HeaderCell(/* ... */)
case ContactCell(/* ... */)
}
Specifically, sloppy view cell permits
having BOTH HeaderCell and ContactCell
Moral
Sum types are the dual of product types.
You should probably be using them as often as you use products.
Think about what values your data structures permit and if there is a logical flaw than you've defined a bad type
Exponential types
(Functions)
Exponential types
(T1 -> T2)
// intro
val addOne: (Int) -> Int = { x -> x + 1 }
// elim
val three = addOne(2)
intro
elim
Why exponential?
How many values inhabit 'Bool -> Bool?' ?
How many values inhabit 'Bool? -> Bool' ?
Bool -> Bool?
1: 0 -> 0
1 -> 0
2: 0 -> 0
1 -> 1
3: 0 -> 1
1 -> 0
4: 0 -> 1
1 -> 1
5: 0 -> 0
1 -> 2
6: 0 -> 1
1 -> 2
7: 0 -> 2
1 -> 0
8: 0 -> 2
1 -> 1
9: 0 -> 2
1 -> 2
(0 = false, 1 = true, 2 = nil)
Side-effects
(making sadness)
Side-effects
(they're scary)
Side effects are the things that an expression does that isn't expressed in the type signature
Why is this bad?
All the things I said in the beginning are broken
Correctness / Documentation / Reuse / Performance
+ concurrency is a nightmare
Side-effects
examples
Some common side effects
// printing to the console
println("hello") // benign usually
// reading/writing to a mutable variable
// the less isolated it is the harder it is to reason over
// if the variable is immutable it's easy to reason
global = 4
// reading/writing to a database or disk
db.read(data)
// making a network request
network.get("http://google.com")
// getting a random number
Math.random() // benign usually
Side-effects
(deal with it)
Unfortunately, you need side effects.
You need to talk to the network. You need to save things to databases.
This sucks
To deal with it:
Isolate
Hide
Isolation
(peripheralize it)
The idea here is because it's hard to reason about side effects.
Put all the side effect crap off to the side and then keep the rest of your program pure
http://cycle.js.org/drivers.html is a good example of this
Referential Transparency
(hide them)
Referential transparency means you can replace the expression with its evaluated value and your program is the same.
Thus, your types express what your code does.
Pure functions are always referentially transparent.
Functions with side effects CAN be referentially transparent sometimes
object Fib {
// state. impureness
private val memo =
hashMapOf<Int, Int>()
fun fib(x: Int): Int {
return memo.get(x).getOrElse {
if (x == 0 || x == 1) {
1
} else {
val res = fib(x-1) + fib(x-2)
memo.put(x, res)
res
}
}
}
}
// state is hidden.
// function call can always replace with value.
Immutability
(life is good)
Using immutable variables and values as much as possible ELIMINATES a class of side-effects.
Parallelism works.
Concurrency becomes easy.
This is important in the age of multi-core processors
Immutability
(important distinction)
// a mutable variable of a mutable list
var x = arrayListOf(1,2,3)
x = arrayListOf(2,3) // valid
x.add(4) // valid
// an immutable variable holding a mutable list
val x = arrayListOf(1,2,3)
x = arrayListOf(2,3) // INVALID
x.add(4) // valid!
// a mutable variable holding an immutable list
var x = listOf(1,2,3)
x = listOf(2,3) // valid!
x.add(4) // INVALID
// be careful!
// I find that I get a lot of side-effect related logic errors using this
// an immutable variable holding an immutable list
val x = listOf(1,2,3)
x = listOf(2,3) // INVALID
x.add(4) // INVALID
// try to use this as much as possible
Immutability
more next week
Moral
Types provide
Correctness
Documentation
Reuse
Performance
Try not to subvert them with side-effects.
Using immutability as much as possible helps you minimize side-effects.
Basic Type Theory
By bkase
Basic Type Theory
PL Part 1
- 687