A Bit of Functional Programming By Example

Given a gzip-compressed file, the shell command gzip -l returns various information about it, including the name of the uncompressed file. I needed to read that name from the output of gzip -l. The output looks like this.

compressed        uncompressed  ratio uncompressed_name
115815              136227  15.0% Foo.jpg.gz

The lines are separated by return characters, and the formatting of columns is done using multiple spaces. Splitting the output into lines is easy. of course.

dim outputLines() as String = Split(theShell.Result, Encodings.UTF8.Chr(13))

The string I want is on the second line. As Ken Thompson famously stated, "when in doubt, use brute force";. I apply his advice here.

dim secondLine() as String = Split(outputLines(1), " ")

This array contains a bunch of empty strings corresponding to the extra spaces. So here is a function to remove them.

Function RemoveBlanks(s() as String) As String()
    dim outputList() as String

    for i as Integer = 0 to UBound(s)
        if s(i) <> "" then
            outputList.Append s(i)
        end if
    next
    return outputList
End Function

Before I do that, I would like to trim the elements in the array to also eliminate any other all-whitespace lines.

Function Trim(s() as String) As String()
  dim outputList() as String

  for i as Integer = 0 to UBound(s)
    outputList.Append Trim(s(i))
  next
  return outputList
End Function

Using these functions, my code now looks like

dim secondLine() as String = RemoveBlanks(Trim(Split(outputLines(1), " ")))

Now I can reasonably expect to find the uncompressed file name as the last element of the array secondLine.

Parsing the output of gzip is made quite simple by the use of the two auxiliary functions Trim and RemoveBlanks. Each of these functions is of course more generally useful. And we can make them yet more general.

Let's take a look at Trim first. What this function does is to apply the built-in function Trim to every element of a string array. Replace Trim by an arbtrary function of the same signature, and we have something quite general.

Delegate Function StringMapDelegate(s as String) As String

Function Map(s() as String, f as StringMapDelegate) as String()
   dim outputList() as String

  for i as Integer = 0 to UBound(s)
    outputList.Append f.Invoke(s(i))
  next
  return outputList
End Function

We can similarly decompose RemoveBlanks.

Delegate Function StringFilterDelegate(s as String) As Boolean

:::xojo
Function Filter(s() as String, f as StringFilterDelegate) As String()
  dim outputList() as String

  for i as Integer = 0 to UBound(s)
    if f.Invoke(s(i)) then
      outputList.Append s(i)
    end if
  next
  return outputList
End Function

Using these functions Map and Filter, I could write my code as follows.

Function IsNotEmpty(s as String) as Boolean
  return s <> ""
End Function
dim secondLine() as String = Filter(Map(Split(outputLines(1), " "), AddressOf Trim), AddressOf IsNotEmpty)

Not long after you decide that Filter is a very useful addition to your toolkit, you will ask questions like "how do I pass two conditions to Filter"; or "how do I pass the negation of a condition?". It turns out this requires a bit more work to do in a clean way. I will post some answers in the future.