why functional programming matters --- in an object-oriented world! matthias felleisen rice...
TRANSCRIPT
Why Functional Programming Matters --- In an Object-Oriented World!
Matthias FelleisenRice University
What to Compare:
• Models of Computation
• Models of Programming– Design – Abstraction (Single Point of Control)– Extensibility
• The Winner
• Lessons Learned
The Focus of OO and Functional Computation: Data
TAG
OO Computation: manipulate data by sending a message to an object and waiting for an answer
FP Computation: apply a primitive operation to a piece of data
How can these two views possibly be related?
Two Simple Languages: FUN and OOPS
• FUN is (like ML/Scheme)
• basic data: numbers ...• datatype• function definitions• expressions, including
– variables– primitives: +, -, ...– conditionals– function application– blocks– assignment
• OOPS is (like Java/Eiffel)
• basic data: numbers ...• class definitions, interfaces• method definitions• expressions, including
– variables– primitives: +, -, …– conditionals– method application– blocks– assignment
Two Sample Programs (in lieu of Grammars):
datatype List = empty | cons(first:int,rest:List)
add1*(l:List) = case l of empty : void; cons : l.first := l.first + 1; add1*(l.rest) end
interface List { add1* : -> void }
class Empty implements List { void add1*() {} }
class Cons(first:int, rest:List) implements List { void add1* () { first := first+1; rest.add1*(); }}
The Computational Models
Given: a program Wanted: a sequence of “states” that shows its behavior
A program is a
sequence of definitions (datatype/functions or interface/classes) an expression ( “main” )
A state is a program.
Expression (Block) Expression (Block)
Definitions are Static, Expressions Capture the State:
Definitions :
let x = new cons(1,empty)in x.first := 2end
let x = new cons(2,empty)in voidend
the abovedefinitionsfor list,cons, empty
Assumption: Definitions are well-formed according to the rules of FUN and OOPS (scope, types, …)
Creating Data:
let x = … y = … … in … new CC(x,BV) … end
let x = … y = … z = new CC(x,BV) … in … z … end
FUN Definitions:
datatype T = … CC(a:Ta, b:Tb) | ...
OOPS Definitions:
class CC(a:Ta, b:Tb) implements T { … }
Extracting Pieces:
let x = … y = … z = new CC(x,BV) … in … x… end
FUN Definitions:
datatype T = … CC(a:Ta, b:Tb) | ...
OOPS Definitions:
class CC(a:Ta, b:Tb) implements T { … }
let x = … y = … z = new CC(x,BV) … in … z.a … end
Mutating Data:
let x = … y = … z = new CC(y,BV) … in … void … end
FUN Definitions:
datatype T = … CC(a:Ta, b:Tb) | ...
OOPS Definitions:
class CC(a:Ta, b:Tb) implements T { … }
let x = … y = … z = new CC(x,BV) … in … z.a := y; … end
Calling Methods in OOPS:
let x = … y = … z = new CC(x,BV) … in … exp [s=x,u=BV2,this=z]… end
OOPS Definitions:
class CC(a:Ta, b:Tb) implements T { … T m(S s, U u) { exp } … }
let x = … y = … z = new CC(x,BV) … in … z. m(x,BV2)… end
Example:
class CC(a:CC, b:int) implements T { … T m(CC s, U u) { s.a := u; } … }
Calling Functions in FUN:
let x = … y = … z = new CC(x,BV) … in … exp [t=z,s=x,u=BV2]… end
FUN Definitions:
m(T t, S s, U u) = exp
let x = … y = … z = new CC(x,BV) … in … m(z,x,BV2)… end
Example:
m(CC t, CC s, int u) = s.a := u
How about First-Class Functions?
let x = … y = … z = (lambda (x) exp) … in … z … end
let x = … y = … … in … (lambda (x) exp)… end
create
let x = … y = … z = (lambda (x) exp) … in … (z U) … end
let x = … y = … z = (lambda (x) exp) … in … exp[z = U] … end
apply
How about Inheritance?
class A(X x, Y y) { method1 method2 method3 }
class B(Z z) extends A { method4}
class A(X x, Y y) { method1 method2 method3 }
class B(X x, Y y, Z z) { method1 method2 method3 method4}
type elaboration
How about Inheritance with Overriding?
class A(X x, Y y) { method1 method2 method3 }
class B(Z z) extends A { method1 method4}
class A(X x, Y y) { method1 method2 method3 }
class B(X x, Y y, Z z) { method1 method2 method3 method4}
type elaboration
Models of Computation: Conclusion
• After type elaboration, the two pictures are nearly indistinguishable
• In both models, creation, access, and mutation of data proceeds in a coherent (“safe”) manner
• Tagged compound data are the essence of computation -- the rest is terminology
The Focus of OO and Functional Computation: Data
method1
method2
method3
function1 function2 function2
In OOPS, methods are attached to data by a physical link.
In FUN, functions are attached to data by a safety policy.
The effect: a completely safe treatment of data in both models
Models of Programming
• What is a program
• How do people design programs in FUN, OOPS– Data-driven designs – Patterns– How things relate
• How do people edit (“abstract”) and comprehend?
What is a Program?
• a batch-processing accounting software• an elevator controller (context: physical device)• a GUI with buttons and text fields and ... (context: monitor)• a space probe (context: devices, broadcast, …)• …
Program
Designing Programs in a Data-Driven Fashion
• the shape of the program is determined by the description of the class of input data
• flat: inexact numbers, chars, truth values, …
• compound: 2D 3D points, personnel records, …
• mixed: an animal is either a spider, an elephant, …
• arbitrarily large: a stack is either empty or a value pushed onto a stack
Flat Data Collections: Numbers
1.03.141
67857.
.750
In FUN, define a function.
In OOPS, define a static method.
These things require domain knowledge and CS/SE isn’t going to help much.
Compound Data
An elephant has a name, an age, and a certain demand for space.
In FUN:
datatype Elephant = e of (name:String, age:Number, space: Number)
In OOPS:
class Elephant ( name: String, age: Number, space: Number) {}
Compound Data and Programming
In FUN:
datatype Elephant = e of (name:String, age:Number, space: Number)
fits_into(ele: Elephant, cage_space: Number) = ele.space < cage_space
In OOPS:
class Elephant ( name: String, age: Number, space: Number) {
fits_into(cage_space: Number) { space < cage_space}
In both cases, remember the available pieces!
Mixed Data (Union)
An animal is either (1) an elephant (2) a spider or (3) a monkey
In FUN:
datatype Animal = e of (name: String, age: Number, space: Number) | s of (name: String, legs: Number) | m of (name: String)
In OOPS:
interface Animal {}
class Elephant ( name: String, age: Number, space: Number) implements Animal {}
class Spider (name: String; legs: Number) implements Animal {}
class Monkey(name:String) implements Animal {}
Mixed Data and ProgrammingIn FUN:
datatype Animal = e of (name: String, age: Number, space: Number) | s of (name: String, legs: Number) | m of (name: String) fits_into : Elephant Number -> Boolfits_into(a: Animal, cage_sp: Number) = case a of e : a.space < cage_sp s : true m : false
In OOPS:
interface Animal { fits_into : Number -> Bool }
class Elephant ( name: String, age: Number, space: Number) implements Animal { fits_into(cage_sp: Number) { space < cage_sp }}
class Spider (name: String; legs: Number) implements Animal { fits_into(cage_sp: Number) { true }}
class Monkey(name:String) implements Animal { … fits_into … }
Arbitrarily Large Data
A sequential file is either (1) end of file(2) a character followed by a sequential file.
A family tree is either (1) empty(2) a node consisting of a name, a family tree for the mother, and a family tree for the father.
A directory has a name, a size, and a directory listing.
A directory listing is either (1) empty (2) a directory followed by directory listing (3) a file followed by a directory listing
Arbitrarily Large Data and Data Definitions
In FUN:
datatype FT = empty | node (name: String, father: FT, mother: FT)
In OOPS:
interface FT {}
class Empty implements FT {}
class Node( name : String; father : FT; mother: FT) implements FT {}
Arbitrarily Large Data and Programming
In FUN:
datatype FT = empty | node (name: String, father: FT, mother: FT)
depth : FT -> numberdepth(a_ft: FT) = case a_ft of empty: 0 node: depth(a_ft.father) + depth(a_ft.mother)
In OOPS:
interface FT { depth: -> Number}
class Empty implements FT { depth( ) { 0 }}
class Node( name : String; father : FT; mother: FT) implements FT { depth() { father.depth() + mother.depth() }}
Arbitrarily Large Data: the Interpreter Pattern
In FUN:
layout data type definition
one clause per variant
deconstruct each variant
use natural recursions
dispatch via case
In OOPS:
layout data type definition
one clause per variant
deconstruct (implicit)
use natural recursions
More Program Design: More Similarities
• mutually recursive data definitions• parallel processing of arbitrarily large pieces of
data (multi-methods, parallel recursion)• mutable data
– keeping track of “history”– exchanging “history”
• concurrency/parallelism• launching programs
– via batch– via graphical interaction (modal or reactive) – via devices
Launching Programs: Via Interaction
In FUN:
type Callback = Event GUI -> void
button1 : Callbackbutton1(e: Event, go : GUI) = ….
menu1 : Callbackmenu1(e : Event, go : GUI) = …
*** Menu(… menu1 …) ***
*** Button(… button1 …) ***
In OOPS:
interface Callback { execute : Event GUI -> void }
class Button1 ( ) implements Callback { execute(e: Event, go : GUI) { …. }}
class Menu1 ( ) implements Callback { execute(e : Event, go : GUI) { … }}
*** Menu(… Menu1( ) …) ***
*** Button(… Button1( ) …) ***
Launching Programs: the Command Pattern
• separate “view” from “model”
• store callback functions – in FUN: use closures– in OOPS: use instances of commands
(new ~ lambda, execute ~ apply)
• process data from GUI elements using methods based on structural design, “history” design, ... modal or reactive processing …
Abstraction (That’s not an UGLY word!)
• Abstracting is “editing” • Abstracting means factoring out common pieces• Abstracting helps maintaining code:
– need to comprehend code/invariant once – fix errors once – improve code once (algorithmic, style) – add functionality once
• Abstracting affects the “bottom line”• Software engineers: “single point of control”
Abstraction in FUN: Abstracting
SUM : list-of-numbers -> number
SUM(a_list) = case a_list of empty : 0 cons: a_list.first + SUM(a_list.rest)
PI : list-of-numbers -> number
PI(a_list) = case a_list of empty : 1 cons: a_list.first * PI(a_list.rest)
F(a_list) = case a_list of empty : cons: a_list.first + F(a_list.rest)
Abstraction in FUN: Specializing
MAKE : num (num num -> num) -> (list-of-numbers ->num)
MAKE(base, combine) = let F(a_list) = case a_list of empty : base cons: combine( a_list.first , F(a_list.rest)) in F
SUM : list-of-numbers -> number
SUM = MAKE(0,+)
PI : list-of-numbers -> number
PI = MAKE(1,*)
Abstraction in OOPS: Abstracting
class Cart (x: double, y: double) { double distance_to_O() { … something with square root and squares of x and y … }
bool closer_to(pt : Point) { this.distance_to_O() <= pt.distance_to_O() }}
class Manhattan(x: double, y: double) { double distance_to_O() { … something with x and y … }
bool closer_to(pt : Point) { this.distance_to_O() <= pt.distance_to_O() }}
abstract class Cart (x: double, y: double) { bool closer_to(pt : Point) { this.distance_to_O() <= pt.distance_to_O() }}
Abstraction in OOPS: Specializing
abstract class PointA(x: double, y: double) { abstract double distance_to_O()
bool closer_to(pt : Point) { this.distance_to_O() <= pt.distance_to_O() }}
class Cart (x: double, y: double) extends PointA { double distance_to_O() { … something with square root and squares of x and y … }}
class Manhattan(x: double, y: double) extends PointA { double distance_to_O() { … something with x and y … }}
Abstraction: Inheritance and the Template Pattern
• identify similar pieces of code, differences
• create abstraction– in FUN: use higher-order function,
application– in OOPS: use inheritance, the Template
pattern
• if most class extension use same hook, make it the default and use overriding
Comprehending Code: Many Variants
An A-expression (A) is either - a variable- a numeric constant- an addition: A + A- a subtraction: A - A - a multiplication: A * A- a division: A / A - an exponentiation: A ** A- ...
A
x 5 + - * / ** …...
Comprehending Code: the Visitor Pattern vs Case
class A_Visitor(…) { void for_variables(x: variable) … void for_numbers(c: number) … void for_plus(l: A, r: A) … void for_minus(l: A, r: A) … void for_times(l: A, r: A) … void for_division(l: A, r: A) … void for_exp(l: A, r: A) … }
fun_for_A (x : A …) { case x of variable … number … plus … minus … times … division … exp …}
Comprehension: Understanding “Functionality”
• collect those pieces of code that perform a function
• create body of code– in FUN: functions and case do it naturally– in OOPS: use call-forwarding and the
Visitor pattern
Black Box Extensibility: Adding Variants
An A-expression (A) is either - a variable- a numeric constant- an addition: A + A- a subtraction: A - A - a multiplication: A * A- a division: A / A - an exponentiation: A ** A
A
x 5 + - * / **
And here are more A-expressions:-- a sin-expression: sin(A)
sin
Black Box Extensibility: Adding or Modifying Functionality
An A-expression (A) is either - a variable- a numeric constant- an addition: A + A- a subtraction: A - A - a multiplication: A * A- a division: A / A - an exponentiation: A ** A
A
We may want more methods than the original product provides or we may wish slightly different functionality.
x 5 + - * / ** sin
x 5 + - * / ** sin
Black Box Extensibility: More Cases
• what if we want visitors? Krishnamurthi, Felleisen & Friedman ECOOP 98
• what if we have modules? Flatt & Findler ICFP 98
• is it useful? Kathi Bohrer (IBM) San Francisco Project SYSTEMS Journal 97
Extensibility in the Functional World
• adding functions to a “black box” -- easy
• adding new variants to a “black box” -- difficult
• fake OO programming with protocols: Krishnamurthi and Felleisen FSE98 Hudak and Liang POPL 95 Felleisen and Cartwright TACS 94 Steele POPL94
The Winner: A Comparison
• a data-centered, safe model of computation
• a data-centered model of program design
• a rich “theory of programs”
• a data-centered, safe model of computation
• a data-centered model of program design
• a rich “practice of patterns”
but the amazing surprise: the “theory” and the “practice” lead to nearly indistinguishable programs:
- data layout induces program layout- iteration patterns or iterator functions - few (true) assignments to reflect “real world” changes (history or state)- objects as “multi-bodied, multi-entry” closures
FUN: OOPS:
The Winner: Default Functionality
• functions with many parameters
• Fortran: entry points• Common LISP: by-keyword• Scheme: rest arguments• Chez: case-lambda • … but there is also Currying
• classes with many default methods and instance variables
• derived classes that override just those few that need to be modified
FUN: OOPS:
Your web server has 27 different parameters that can be tuned … How do you tune them?
The Winner: Extensible Products
• complex programming protocols
• problem with standard types
• … soft-typing works just fine
• default strategies • extensibility patterns
(hooks)• derived classes that
override just those strategies that need to be modified
FUN: OOPS:
Your product should accommodate 435 business strategies in 47 countries …. How do you accommodate them all?
The Winner: There isn’t One
• OOPS and FUN are equivalent for many situations
• FUN has a much richer theory
• OOPS provides the practical examples
More Comparisons:
• FUN– has “lambda”, which
means it is simpler to abstract
– functions are easier to comprehend
– has “currying”, which makes up for the short-comings on extensibility
• Scheme has macros
• OOPS– can accommodate
many defaults easily – is good for producing
extensible systems– lacks “functions” and
demands visitors
So What? How does this Help?
• programming: design, reasoning about programs
• language research: theory and implementation
• education: what to teach and how to teach it
Programming and Program Engineering
• pattern mining: functional programmers have contemplated the meta-level for much longer than oo programmers
• logic mining: programming in a functional style facilitates reasoning about programs; it is easy and natural to program “functionally” in an oo language
Language Research
• type theory: parametric polymorphism, modules
• program analysis: abstract interpretation
• implementation: closures are simple objects, functional programming environments are semantically more sophisticated than OO environments (repl, module managers)
Education: FUN first, OOPS later!
• functional programming is syntactically simpler than object-oriented programming
• functional programming is more natural than object-oriented programming: append(list1,list2) versus list1.append(list2)
• functional programming is traditionally more interactive than object-oriented programming (repl)
Summary
• functional programming and object-oriented programming are closely related
• their differences should lead to important synergies
• Let us take a closer look!