Closures... When to use them?

#1

I’ve been exploring closures and I found them to be very accommodating but I also found they can obfuscate the code if you rely on them too much.

Here’s an example I’ve been working on that embraces closures. It provides a ‘cool’ solution to a tricky problem but it also makes it very hard to understand the intent of the code.

The function dropElement takes a list and function to match an element in the the list and returns a type of ('a * 'a list) option. If the function(dropElement) finds a match in the list it will return that element and the list minus the found element.

Example:
given a list [1;2;3;4;5;6;7;8;9;10;]
and the match condition element = 4
the returned value will be Some(4, [1;2;3;5;6;7;8;9;10])

let dropElement matchElem lst =
  let rec dropElemAux lst func =
    match lst with
    | [] -> None
    | hd::tl ->
      if (matchElem hd)
      then
        Some(hd, (func tl))
      else
        dropElemAux tl (fun t -> func (hd::t)) in
  dropElemAux lst (fun d -> d)

let ans = dropElement ((=)4) [1;2;3;4;5;6;7;8;9;10;]

let () =
  match ans with
  | None -> print_endline "No element matched"
  | Some (elem, lst) ->
    print_endline(string_of_int elem);
    print_newline();
    List.iter (fun x -> print_endline(string_of_int x)) lst

Now the above code works but I find it very confusing and I wrote it.

#2

Well, in that particular case, you happen (heh) to have reinvented difference lists, so you can abstract that!

module Difflist = struct
  type 'a t = 'a list -> 'a list
  let empty x = x
  let cons hd (tl : _ t) : _ t = fun x -> tl (hd::x)
  let append (t1 : _ t) (t2 : _ t) : _ t = fun x -> t1 (t2 x)
  let retAppend (t1 : 'a t) t2 = t1 t2
end

let dropElement matchElem lst =
  let rec dropElemAux rest acc =
    match rest with
    | [] -> None
    | hd::tl ->
      if matchElem hd then
        Some(hd, Difflist.retAppend acc tl)
      else
        dropElemAux tl (Difflist.cons hd acc)
  in
  dropElemAux lst Difflist.empty

Personally, I would have gone for the the simpler:

let dropElement matchElem lst =
 let rec dropElemAux rest acc =
   match rest with
   | [] -> None
   | hd::tl ->
     if matchElem hd then
       Some(hd, List.rev_append acc tl)
     else
       dropElemAux tl (hd :: acc)
 in
 dropElemAux lst []

In general, there is no one answer to your question. If some closures are parts of some data-structure, like here, you can always abstract them and hide the internal details.

3 Likes
#3

Just for completeness: you might want to look at List.filter, which solves a similar problem but would remove all matching elements, not just the first.

let drop x xs = List.filter ((<>) x) xs
1 Like
#4

To answer your title question “when to use closures” your examples gives some guidance to that. The closure “matchElem” could hide anything – heck, a primality-checker, right? (“remove and return the first prime number”) This is an excellent use of a closure. OTOH, “func” is basically just a wrapper for an accumulator of elements that sets up what amounts to a “rev_append” (as Drup points out).

So one maxim about closures could be: take a closure argument when you want to allow a caller to provide arbitrary logic. don’t take one, when you can clearly see the logic that the closure encapsulates, and it’s obvious how to turn it into a data-structure.

Drup’s rev_append example has the extra nicety that it’s both obviously tail-recursive, using “accumulating parameters”. This “accumulating parameters” stuff is really valuable to understand – it’s connected to continuations via Reynolds’ “defunctionalization” (his “Definitional Interpreters …” paper is a classic) but even without understanding all that, just knowing how to use accumulating parameters to turn stack-recursion into tail-recursion is very important.

3 Likes