Chapter 5 - Chaining, Currying, and Partial Applications

In this lesson we will learn about three different but related design patterns. Chaining will consider the composition of functions. Currying will convert a function into multiple functions based on input parameters thus allowing you to chain the input parameters together. Partial Applications will convert a function into multiple functions based on behavior thus allowing you to execute part of the original function and reuse the result multiple times.

5.1 Chaining

If we need to apply multiple operations on our inputs, we can define each operation in a function and then chain them together. Chaining is the process of executing a series of functions where the output of one is used as the input for the next. Mathematically, we show chaining or composition as follows:

\(\mathit{f}(g(h())) = \mathit{f} \circ g \circ h\)

\(\nonumber\)

In this example, \(h\) runs first and passes the result to function \(g\). After \(g\) runs, the result is passed to function \(\mathit{f}\).

Consider the following Erlang functions that are chained together in various ways:

twice(X) -> 2 * X.
square(X) -> X * X.
third(X) -> X / 3.

test_chain() ->
    6.0 = third(twice(square(3))),
    6.0 = twice(third(square(3))),
    4.0 = square(twice(third(3))),
    2.0 = twice(square(third(3))),
    ok.

Note that we were able to chain the functions in all various permutations because each of the functions had a single number input and single number output.

If you think about the map and filter functions, these could be chained together because they both have a single list as an input and output. You will make this observation in the exercises below.

When we chain functions, we can also work with functions that return functions. Consider the functions multiply_list and greater_list:

\(spec ~ ~ multiply\_list :: real \rightarrow (\lambda :: [real] \rightarrow [real]).\)

\(spec ~ ~ \lambda_{map} :: real \rightarrow real.\)

\(de\mathit{f} ~ ~ multiply\_list :: Value \rightarrow (\lambda :: List \rightarrow (map ~ ~ (\lambda_{map} :: Item \rightarrow Item * Value) ~ ~ List)).\)

\(\nonumber\)

\(spec ~ ~ greater\_list :: real \rightarrow (\lambda :: [real] \rightarrow [real]).\)

\(spec ~ ~ \lambda_{filter} :: real \rightarrow boolean.\)

\(de\mathit{f} ~ ~ greater\_list :: Value \rightarrow (\lambda :: List \rightarrow (\mathit{filter} ~ ~ (\lambda_{filter} :: Item \rightarrow Item > Value) ~ ~ List)).\)

\(\nonumber\)

In these definitions, the function returns a function that does either a map or a filter. The lambda function for map and filter is constructed using the \(Value\) input parameter. For example, if multiply_list receives 4 for \(Value\), then a function that performs a map on the list with a lambda of \(\lambda_{map} :: Item \rightarrow Item * 4\) is created.

The implementation of the multiply_list function is given below in Erlang. The implementation for greater_list is left for an exercise. An example of chaining these together is given as well.

multiply_list(Value) -> fun(List) -> lists:map(fun(Item) -> Value * Item end, List) end.
greater_list(Value) -> implemented_in_exercise.

test_chain() ->
   % Multiply all items in list by 2 and then
   % filter for all items greater than 10.
   L = [2, 4, 6, 8, 10, 12],
   [12,16,20,24] = (greater_list(10))((multiply_list(2))(L)),

   % Filter for all items greater than 10 and 
   % then multiply all items by 2.
   [24] = (multiply_list(2))((greater_list(10))(L)),

   ok.

Problem Set 1

You can find the template for the problem sets in this lesson here: prove05.erl

  1. Chain the map, filter, and foldl Erlang functions together (single line of code) as follows:
  • Use map to triple all the values in the starting list [1,2,3,4,5,6,7,8,9,10]
  • Use filter to only save the even numbers from the map result
  • Use foldl to get the product of those even numbers from the filter result. The expected result is 933120.
  1. Implement the greater_list function provided above and the multiply_list function defined above. Test it with the code provided in the template.
  2. Create a multiples_of_list function that takes an integer parameter Value and returns a function that will perform the appropriate filter on a list of integers. The filter should only include values in the list that are multiples of Value. Include a specification and definition for the function. Test it with the code provided in the template.

5.2 Currying

With chaining, we were able to compose functions together serially. With currying, we are able to compose input parameters to a function serially. This done by creating functions to handle each input parameter separately.

Consider the following math function which adds three numbers:

\(spec ~ ~ add3 :: real ~ ~ real ~ ~ real \rightarrow real.\)

\(de\mathit{f} ~ ~ add3 :: Value_1 ~ ~ Value_2 ~ ~ Value_3 \rightarrow Value_1 + Value_2 + Value_3.\)

\(\nonumber\)

To curry this function, we need to rewrite it to accept only one parameter and return a function to take the next parameter. This process of returning a function to take the next parameter will continue until all parameters have been received. The final function that received the last parameter will actually perform the function behavior with all the inputs. Here is the curried version of the add3 function.

\(spec ~ ~ add3\_curry :: real \rightarrow (\lambda_1 :: real \rightarrow (\lambda_2 :: real \rightarrow real)).\)

\(de\mathit{f} ~ ~ add3\_curry :: Value_1 \rightarrow (\lambda_1 :: Value_2 \rightarrow (\lambda_2 :: Value_3 \rightarrow\)

\(\quad \quad Value_1 + Value_2 + Value_3)).\)

\(\nonumber\)

Here is the code implementation for the curried function:

add3_curry(Value1) -> (fun(Value2) -> (fun(Value3) -> Value1 + Value2 + Value3 end) end).

test_curry() ->
    14 = ((add3_curry(2))(5))(7),
    ok.

We can call the curried function and chain the inputs together. A useful use of a curried function is to save one of the inner functions. For example, if we frequently want to add 2 and 5 with other numbers, we can create a function Add3_StartWith2And5.

test_curry() ->
    Add3_StartWith2And5 = (add3_curry(2))(5),
    14 = Add3_StartWith2And5(7),
    17 = Add3_StartWith2And5(10).
    ok.

A generic function can be written to curry a function for specific number of input parameters. For example, to support our add3 function, we can write a function called curry3. The input parameter for the curry3 is the function we want to curry.

\(spec ~ ~ \lambda :: a_1 ~ ~ a_2 ~ ~ a_3 \rightarrow a_4.\)

\(spec ~ ~ curry3 :: \lambda \rightarrow (\lambda_1 :: a_1 \rightarrow (\lambda_2 :: a_2 \rightarrow (\lambda_3 :: a_3 \rightarrow a_4)))\)

\(de\mathit{f} ~ ~ curry3 :: \lambda \rightarrow (\lambda_1 :: Param_1 \rightarrow (\lambda_2 :: Param_2 \rightarrow (\lambda_3 :: Param_3 \rightarrow\)

\(\quad \quad (\lambda ~ ~ Param_1 ~ ~ Param_2 ~ ~ Param_3))))\)

\(\nonumber\)

When the curry3 function is called, it returns a function \(\lambda_1\) that takes the first parameter of the \(\lambda\). The function that returns will subsequently return a function \(\lambda_2\) that takes the second parameter. This process continues to receive the 3rd parameter. The final function \(\lambda_3\) when called will finally perform the \(\lambda\) function on the 3 parameters previously received.

This generic curry3 function can be written in Erlang and used as follows:

add3(Param1,Param2,Param3) -> Param1 + Param2 + Param3.
curry3(Lambda) -> (fun(Param1) -> (fun(Param2) -> (fun(Param3) -> Lambda(Param1, Param2, Param3) end) end) end).

test_curry() ->
    14 = (((curry3(fun add3/3))(2))(5))(7),

    Add3_StartWith2And5 = ((curry3(fun add3/3))(2))(5),
    14 = Add3_StartWith2And5(7),
    17 = Add3_StartWith2And5(10),
    ok.

Problem Set 2

  1. Implement the curry3 function described above. Using the curry3 function, curry the alert function provided in the starting code. Test your code with the code provided.
  2. Save and use an intermediate function from the curried alert function that contains the location (first parameter). Test your code by calling your intermediate function twice with different Category (second parameter) and Message (third parameter) values. You can make up the values for these last two parameters in your test code.
  3. Save and use an intermediate function that contains the location and category (first and second parameter). Test your code by calling your intermediate function twice with different Message values.
  4. The provided range_check function takes a range (specified by the first two parameters low and high) and a value to test. This function is not compatible with the filter function because it has arity 3. Use the curry3 to generate a function that checks if a value is in the specific range [10,20]. Apply this new function to the list provided in the provided starting code to find all numbers in the range [10,20] using the filter function.

5.3 Partial Applications

When we curried a function, we composed each parameter one at a time. We did this by creating functions that handled each parameter one at a time. Another approach is to split the function up into behavior and compose the behaviors one at a time. Just like currying, we can save intermediate functions. If the functions include behaviors, then these intermediate functions can have calculated results within them for reuse. We call this a partial application. While there are some similarities, please note that partial applications and currying are different.

Consider the following function which performs a map, filter, and fold (in order) on a range from 1 to \(Value\). Note that the definition is more complicated and the \(=\) is used to save intermediate results as each step is performed. This type of function has several parameters. Note that the range function is assumed to exist which will create a list of numbers from 1 to the specified value.

\(spec ~ ~ \lambda_{map} :: int \rightarrow real.\)

\(spec ~ ~ \lambda_{filter} :: real \rightarrow boolean.\)

\(spec ~ ~ \lambda_{fold} :: real ~ ~ real \rightarrow real.\)

\(spec ~ ~ map\_\mathit{f}ilter\_\mathit{f}old :: integer ~ ~ \lambda_{map} ~ ~ \lambda_{filter} ~ ~ real ~ ~ \lambda_{fold} \rightarrow real.\)

\(de\mathit{f} ~ ~ map\_\mathit{f}ilter\_\mathit{f}old :: Value ~ ~ \lambda_{map} ~ ~ \lambda_{filter} ~ ~ FoldInit ~ ~ \lambda_{fold} \rightarrow\)

\(\quad \quad List = (range ~ ~ Value),\)

\(\quad \quad MapList = (map ~ ~ \lambda_{map} ~ ~ List),\)

\(\quad \quad FilterList = (filter ~ ~ \lambda_{filter} ~ ~ MapList),\)

\(\quad \quad FoldResult = (foldl ~ ~ \lambda_{fold} ~ ~ FoldInit ~ ~ FilterList),\)

\(\quad \quad FoldResult.\)

\(\nonumber\)

Every time we call this function, we will have to do the initial map and filter again. If we using a common set of data, then this could be wasteful. Like currying, creating a partial application has the benefit of creating functions with fewer parameters. In a partial application, we will split this function into multiple functions in which each smaller function will perform part of the larger function and return a function to do the rest. The function that is returned will contain the partial results from the smaller function.

Here is a new version of map_filter_fold that uses partial applications:

\(spec ~ ~ \lambda_{map} :: int \rightarrow real.\)

\(spec ~ ~ \lambda_{filter} :: real \rightarrow boolean.\)

\(spec ~ ~ \lambda_{fold} :: real ~ ~ real \rightarrow real.\)

\(spec ~ ~ map\_\mathit{f}ilter\_\mathit{f}old2 :: integer \rightarrow (\lambda_1 :: \lambda_{map} \rightarrow (\lambda_2 :: \lambda_{filter} \rightarrow\)

\(\quad \quad (\lambda_3 :: real ~ ~ \lambda_{fold} \rightarrow real))\)

\(de\mathit{f} ~ ~ map\_\mathit{f}ilter\_\mathit{f}old2 :: Value \rightarrow\)

\(\quad \quad List = (range ~ ~ Value),\)

\(\quad \quad \lambda_1 :: \lambda_{map} \rightarrow\)

\(\quad \quad \quad \quad MapList = (map ~ ~ \lambda_{map} ~ ~ List),\)

\(\quad \quad \quad \quad \lambda_2 :: \lambda_{filter} \rightarrow\)

\(\quad \quad \quad \quad \quad \quad FilterList = (filter ~ ~ \lambda_{filter} ~ ~ MapList),\)

\(\quad \quad \quad \quad \quad \quad \lambda_3 :: FoldInit ~ ~ \lambda_{fold} \rightarrow\)

\(\quad \quad \quad \quad \quad \quad \quad \quad FoldResult = (foldl ~ ~ \lambda_{fold} ~ ~ FoldInit ~ ~ FilterList),\)

\(\quad \quad \quad \quad \quad \quad \quad \quad FoldResult.\)

\(\nonumber\)

The code is given below.

map_filter_fold2(Value) ->
    List = lists:seq(1, Value),
    fun (MapL) -> 
        MapList = lists:map(MapL, List),
        fun (FilterL) ->
            FilterList = lists:filter(FilterL, MapList),
            fun (FoldInit, FoldL) ->
                FoldResult = lists:foldl(FoldL, FoldInit, FilterList),
                FoldResult
            end
        end
    end.

With this code, we can create reusable partial application functions that do part of the processing:

map_filter_fold_test() ->
    % Useful lambdas for the map_filter_fold2 to use.
    Square = fun(X) -> X * X end,
    Triple = fun(X) -> 3 * X end,
    Odd = fun(X) -> X rem 2 == 1 end,
    Even = fun(X) -> X rem 2 == 0 end,
    Sum = fun(X,Y) -> X+Y end,
    Product = fun(X,Y) -> X*Y end,

    % Example not using any partial applications    
    35 = (((map_filter_fold2(6))(Square))(Odd))(0, Sum),
    
    % Example creating partial applications
    First10Squares = (map_filter_fold2(10))(Square),
    First20TriplesOnlyEven = ((map_filter_fold2(20))(Triple))(Even),

    % Using the partial applications
    165 = (First10Squares(Odd))(0, Sum),
    14745600 = (First10Squares(Even))(1,Product),
    270 = First20TriplesOnlyEven(0,Sum),
    3656994324480 = First20TriplesOnlyEven(1,Product),

    ok.

Problem Set 3

  1. Implement the map_filter_fold2 code above. Test the function with the test code provided in the starting code. Write an additional partial application function that obtains the first 20 triples that are even (uses all parts of map_filter_fold2 except the application of the fold and save into the variable First20TriplesOnlyEven provided in the test code). Test your partial application function to calculate both the sum of all of the numbers (it should result in 330) and the product of all of the numbers (219419659468800).

  2. Review the process_dataset function (there is some test code you can run as well) which has input parameters as follows:

    • Filename - The dataset csv file. You will use the file called weather.csv You can download that file here: weather.csv
    • ColumnId - The column to process (first column is designated as column 1)
    • ColumnType - Either the atom text or int or float so the proper formatting is done
    • CalcFunction - One arity function used to aggregate all the values in the column extracted. Two functions have been created for you already for use including list_average and list_text_count.

    Using this process_dataset function, create a process_dataset2 function that will allow partial applications to be created. The new process_dataset2 should split up the functions in the following ways:

    • Read the entire dataset from a user supplied file using the read_csv_file function
    • Extract a column from the dataset using a user supplied column number and column type using the extract_column_array function.
    • Perform a user supplied function on the column

    Code is provided in the starting code to test your new process_dataset2 function.

\(\nonumber\) \(\nonumber\) Creative Commons License - CC - BY