Functional Programming

CSC 310 - Programming Languages

Programming Language Paradigms

  • Imperative: change state, assignments
  • Structured: if/block/routine control flow
  • Object-oriented: message passing (dynamic dispatch), inheritance
  • Functional: functions are first-class citizens that can be passed around or called recursively.

Functional Programming

  • By now, you know OOP and Structured Imperative
  • Functional programming
    • Computation is evaluating (math) functions
    • Avoid “global state” and “mutable data”
    • Get stuff done: apply (higher-order) functions
    • Avoid sequential commands
  • Important features
    • Higher-order, first-class functions
    • Closures and recursion
    • Lists and list processing

State

  • The state of a program is all of the current variable and heap values
  • Imperative programs destructively modify existing state
  • Functional programs yield new similar states over time.

Basic OCaml

  • Let us start with a C example:

    double avg(int x, int y) {
        double z = (double)(x + y);
        z = z/2;
        printf("Answer is %g\n", z);
        return z;
    }

Basic OCaml

  • OCaml version:

    let avg (x:int) (y:int) : float = begin
      let z = float_of_int(x + y) in
      let z = z /. 2.0 in
      printf "Answer is %g\n" z;
      z
    end

The Tuple (or Pair)

  • Example 1:

    let x = (22, 58) in (* tuple creation *)
    ...
    let y, z = x in (* tuple field extraction *)
    printf "first element is %d\n" y;
    ...
  • Example 2:

    let add_points p1 p2 =
      let x1, y1 = p1 in
      let x2, y2 = p2 in
      (x1 + x2, y1 + y2)

List Syntax in OCaml

  • Empty list: []
  • Singleton: [ element ]
  • Longer list: [ e1; e2; e3]
  • Cons: x :: [y;z] = [x;y;z]
  • Append: [w;x] @ [y;z] = [w;x;y;z]
  • Every element must have the same type

Functional Example

  • Simple functional set (built out of lists)

    let rec add_elem (s, e) =
      if s = [] then [e]
      else if List.hd s = e then s
      else List.hd s :: add_elem(List.tl s, e)
  • Pattern matching version

    let rec add_elem (s, e) = match s with
      | [] -> [e]
      | hd :: tl when e = hd -> s
      | hd :: tl -> hd :: add_elem(tl, e)

Imperative Code

  • C version; more cases to handle

    List* add_elem(List *s, item e) {
        if (s == NULL) {
            return list(e, NULL);
        }
        else if (s->hd == e) {
            return s;
        }
        else if (s->tl == NULL) {
            s->tl = list(e, NULL);
            return s;
        }
        else {
            return add_elem(s->tl, e);
        }
    }

Functional-Style Advantages

  • Tractable program semantics
    • Procedures are functions
    • Formulate and prove assertions about code
    • More readable
  • Referential transparency
    • Replace any expression by its value without changing the result
  • No side-effects
    • fewer errors

Functional-Style Disadvantages

  • Efficiency
    • Copying takes time
  • Compiler implementation
    • Frequent memory allocation
  • Unfamiliar (to you!)
    • New programming style
  • Not appropriate for every program
    • Operating systems, etc.

ML Innovative Features

  • Type system
    • Strongly typed
    • Type inference
    • Abstraction
  • Modules
  • Patterns
  • Polymorphism
  • Higher-order functions
  • Concise semantics

Type System

  • Type inference

    let rec add_elem (s, e) = match s with
      | [] -> [e]
      | hd :: tl when e = hd -> s
      | hd :: tl -> hd :: add_elem(tl, e)
    
    val add_elem : 'a list * 'a -> 'a list
    • 'a list means List<T>
  • ML infers types
  • Optional type declarations (exp : type)
    • Clarify ambiguous cases, documentation
    • Can be read exp has type type

Pattern Matching

  • Simplifies Code (elimates if, accessors)

    type btree = (* binary tree of strings *)
      | Node of btree * string * btree
      | Leaf of string
    
    let rec height tree = match tree with with
      | Leaf _ -> 1
      | Node(x, _, y) -> 1 + max (height x) (height y)
    
    let rec mem tree elt = match tree with
      | Leaf str -> str = elt
      | Node(x,str,y) -> str = elt || mem x elt || mem y elt

Pattern Matching Mistakes

  • What if I forget a case?

    let rec is_odd x = match x with
      | 0 -> false
      | 2 -> false
      | x when x > 2 -> is_odd(x-2)
  • Message

    Warning 8: this pattern-matching is not exhaustive.
    Here is an example of a case that is not matched:
    1

Polymorphism

  • Functions and type type inference are polymorphic

    • Operate on more than one type

    • Example:
    let rec length x = match x with
      | [] -> 0
      | hd :: tl -> 1 + length tl
    
    val length: 'a list -> int
    • The 'a means “any one type”

Higher-Order Functions

  • Functions are first-class values
    • Can be used whenever a value is expected
    • Notably, can be passed around
    • Closure capures the environment
    let rec map f lst = match lst with
      | [] -> []
      | hd :: tl -> f hd :: map f tl
    
    val map : ('a -> 'b) -> 'a -> 'b list
  • Extremely powerful programming technique
    • General iterators
    • Implement abstraction

Fold

  • We have seen length and map
  • We can also imagine …
    • sum [1; 5; 8] = 14
    • product [1; 5; 8] = 40
    • and [true; true; false] = false
    • or [true; true; false] = true
    • filter (fun x -> x > 4) [1; 5; 8] = [5; 8]
    • reverse [1; 5; 8] = [8; 5; 1]
    • mem 5 [1; 5; 8] = true

Fold

  • The fold operator comes from Recursion Theory (Kleene, 1952)

    let rec fold f acc lst = match lst with
      | [] -> acc
      | hd :: tl -> fold f (f acc hd) tl
    
    val fold: ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
  • Imagine we are summing a list: f is addition

Fold Examples

  • Let us build things with fold
    • length lst = fold (fun acc elt -> acc + 1) 0 lst
    • sum lst = fold (fun acc elt -> acc + elt) 0 lst
    • product lst = fold (fun acc elt -> acc * elt) 1 lst
    • and lst = fold (fun acc elt -> acc & elt) true lst
  • Can we implement or or reverse with fold?

More Fold Examples

  • Examples
    • reverse lst = fold (fun acc e -> acc @ [e]) [] lst
    • filter keep_it lst = fold (fun acc elt -> if keep_it elt then elt :: acc else acc) [] lst
    • mem wanted lst = fold (fun acc elt -> acc || wanted = elt) false lst

Map From Fold

let map f lst = fold (fun acc -> elt -> (f elt) :: acc) [] lst
  • Types:
    • f : 'a -> 'b
    • lst : 'a list
    • acc : 'b list
    • elt : 'a

Partial Application and Currying

  • Currying: “if you fix some arguments, then you get a function of the remaining arguments”

  • Example:

    let myadd x y = x + y
    val myadd : int -> int -> int
    
    let addtwo = myadd 2
    val addtwo : int -> int
    
    addtwo 77 = 79

Referential Transparency

  • To find the meaning of a functional program, we replace each reference to a variable with its definition.
    • This is called referential transparency
  • Example:

    let y = 55
    let f x = x + y
    
    f 3 --> means --> 3 + y --> means --> 3 + 55

Insertion Sort in OCaml

let rec insert_sort cmp lst =
  match lst with
  | [] -> []
  | hd :: tl -> insert cmp hd (insert_sort cmp tl)
and insert cmp elt lst =
  match lst with
  | [] -> [elt]
  | hd :: tl when cmp hd elt -> hd :: (insert cmp elt tl)
  | _ -> elt :: lst

Sorting

  • sort type: (sort : ('a * 'a -> bool) -> 'a list -> 'a list

  • Examples:

    langs = ["fortran"; "algol"; "c"]
    sort (fun a b -> a < b) langs
    sort (fun a b -> a > b) langs
    sort fun a b -> strlen a < strlen b) langs

Functional Programming Summary

  • In functional programming, functions are first-class citizens that operate on, and produce, immutable data.
  • Functions and type inference are polymorphic and operate on more than one type.
  • OCaml (and Haskell, etc.) support pattern matching over user-defined data types.
  • fold is a powerful and general higher-order function.