Exploring ReasonML 学习笔记 -- 7. Recursion (The End)

256 阅读2分钟

Exploring ReasonML

  1. Two cases
  • One or more base cases process atomic data. This is where the recursion stops.
  • One or more recursive cases process compound data. Processing continues via recursive function calls.
type intTree =
  | Empty /* atomic */
  | Node(int, intTree, intTree); /* compound */

let rec summarizeTree = (t: intTree) =>
  switch t {
  | Empty => 0 /* base case */
  | Node(i, leftTree, rightTree) => /* recursive case */
    i + summarizeTree(leftTree) + summarizeTree(rightTree)
  };
  1. Looping via higher-order helper functions
/* use recursion */
let rec summarizeList = (l: list(int)) =>
  switch l {
  | [] => 0
  | [head, ...tail] =>
    head + summarizeList(tail)
  };

/* use high-order helper function */
let summarizeList = (theList: list(int)) =>
  ListLabels.fold_left(
    ~f = (sum, elem) => sum + elem,
    ~init = 0,
    theList
  );

Other high-order helper functions to list

  • map: (~f: ('a) => 'b, list('a)) => list('b) Produces a new list by applying ~f to each element of the list.
  • filter: (~f: ('a) => bool, list('a)) => list('a) Produces a new list that contains only those elements of the input list for which ~f returns true.
  • for_all: (~f: ('a) => bool, list('a)) => bool Returns true if ~f returns true for all elements of the list.
  • exists: (~f: ('a) => bool, list('a)) => bool Returns true if ~f returns true for at least one element of the list.
  • iter: (~f: ('a) => unit, list('a)) => unit Invokes ~f for each element of the list. iter() only makes sense if ~f has side effects (output, mutations, etc.).
  1. Recursive technique: accumulator
/* use recursion */
let rec maxList = (l: list(int)) =>
  switch (l) {
  | [] => min_int
  | [head, ...tail] when head > maxList(tail) => head /* A */
  | [_, ...tail] => maxList(tail) /* B */
  };

/* use accumulator */
let rec maxList = (~max=min_int, l: list(int)) => {
  switch l {
  | [] => max
  | [head, ...tail] when head > max =>
    maxList(~max=head, tail);
  | [_, ...tail] =>
    maxList(~max, tail);
  };
};

accumulator: a parameter that is continually updated during recursion and returned at the very end

  1. Tail call elimination
/* recursion without tail call*/
let rec repeat = (~times: int, str: string) =>
  if (times <= 0) { /* base case */
    "";
  } else { /* recursive case */
    str ++ repeat(~times = times - 1, str); /* A */
  };

/* recursion with tail call */
let repeat = (~times: int, str: string) => {
  let rec aux = (~result="", n) =>
    if (n <= 0) { /* base case */
      result;
    } else { /* recursive case */
      aux(~result = result ++ str, n-1); /* A */
    };
  aux(times);
};

Terminology:

  • A function whose recursive calls are tail calls is called tail-recursive.
  • A tail-recursive algorithm is also called iterative. Iterative algorithms can also be implemented via loops.
/* recursion without tail call */
let rec reverse = (l: list('a)) =>
  switch l {
  | [] => l
  | [head, ...tail] =>
    List.append(reverse(tail), [head]) /* A */
  };

/* recursion with tail call */
let rec reverse = (theList: list('a)) => {
  let rec aux = (~result=[], l) =>
    switch l {
    | [] => result
    | [head, ...tail] =>
      aux(~result=[head, ...result], tail)
    };
  aux(theList);
};

Difference:

  • The recursive reverse() works with:
    • Current data: head
    • Future data: reverse(tail)
  • The iterative reverse() works with:
    • Past data: result
    • Current data: head