First-Class Functions

  • "First-class functions" are a thing. A thing is "first-class" if all the sensible operations of the language do indeed apply to it

  • In Rust, functions are first class

    • They have their own types

    • They can be passed as parameters, returned as results, stored in arrays, used as struct and enum fields, etc

    • They can be created in any block scope

    • References are available (indeed, a function essentially is a static reference)

  • examples/prog.rs

Closures

  • The problem with pure functions is that anything they compute has to be based on arguments

  • A closure is a function that is allowed to access and mutate its "environment": names that are statically in scope at the point of its declaration

  • This means that different calls to the function with the same parameters may produce different results

  • Closures in Rust are mostly first-class, and can access parameters and locals like anything else

  • Syntax has wacky pipes and stuff; types are optional and inferred

Closures, Environments, Types

  • "Pure" closures are just functions

      fn call(f: fn(u64)) {
          f(12);
      }
    
      fn main() {
          call(|x| println!("{}", x));
      }
    
  • Closures that capture the environment are each their own type, but all obey some corresponding Fn trait

      fn call<T: Fn(u64)>(f: T) {
          f(12);
      }
    
      fn main() {
          let y = 5;
          call(|x| println!("{}", x + y));
      }
    
  • Be careful about generics vs trait objects

The Problem With Closures (in a non-GC language)

  • Need to keep closed-over memory alive as long as the closure

  • To be useful, this needs to be longer than the scope in which the closure is created

  • GC solution: who cares? Tracking lifetimes at runtime anyhow

  • Rust solution: Make a closure pointer a fat pointer

    • Pointer to the code
    • Pointer to a struct that closes over the environment
  • Normal borrow checker rules then apply

Closures As Environment Structs

  • Think of a closure as a code pointer and some anonymous struct holding the captured vars or refs to them and its implementation

  • Three kinds of implementation of the anonymous struct:

    • &self: Closure is of trait Fn

    • &mut self: Closure is of trait FnMut

    • self: Closure is of trait FnOnce

  • Also, environment values themselves can be:

    • By reference (normal case)

    • Owned (move closure)

Generics vs Trait Objects

  • Each closure has a different size, so can't just handle them willy-nilly

  • Each closure is of a different type, so generics don't work so well with them

  • This often leaves the joy of trait objects

  • examples/prog2.rs

Final Notes

  • Values implementing Copy confuse everybody

  • This stuff is hard; get some experience with it right now

Last modified: Tuesday, 4 May 2021, 1:32 PM