If you came here anticipating some supernatural spicy Indian cuisine, I’m afraid you’re going to be disappointed. However, if your goal is to learn to use functional programming (FP) techniques with Smallworld Magik, then prepare to feast on new knowledge and add another tool to your growing arsenal of skills.
Currying, a subset of partial application, can make your code more readable and flexible. And no, it wasn’t named after a South-Asian food, but for the man who first investigated the technique, Haskell Curry (after which the Functional Programming language, Haskell, is named — let’s just say the guy was prolific).
In a nutshell, currying allows you to pass arguments to a procedure, requiring multiple arguments, one at a time and get back a series of procedures that each take the next argument until all the procedure’s parameters have been supplied and the procedure can be executed.
Not super clear yet?
No problem. Let’s look at a simple example.
A Curry Example
_global curry_hero <<
_proc @hero(p_hero)
_return _proc @partner(p_partner)
_import p_hero
_return _proc @group(p_group)
_import p_hero, p_partner
write(p_hero,
" has a partner named ",
p_partner,
" and is a member of the ",
p_group)
_endproc
_endproc
_endproc
The curry_hero procedure takes one argument (p_hero) and returns a procedure that takes another argument (p_partner) which returns a final procedure that takes one more argument (p_group). Once all the arguments have been passed in, the innermost procedure writes out a string using all the parameters.
Notice lines 3 and 6 return procedures rather than final values. Also notice we have to _import parameters from the enclosing procedures (because Magik doesn’t have automatic lexical scoping for procedures).
Here’s a sample run.
Magik> partner << curry_hero("Batman")
proc partner(p_partner)
Magik> grp << partner("Robin")
proc group(p_group)
Magik> grp("JLA")
Batman has a partner named Robin and is a member of the JLA
Magik>
We’ve passed each argument one at a time until all arguments have been passed, thus causing the innermost procedure to be invoked. We can also invoke curry_hero as follows:
Magik> curry_hero("Captain America")("the Falcon")("Avengers")
Captain America has a partner named the Falcon and is a member of the Avengers
Magik>
In this case we’ve supplied all the arguments to the 3 procedures at once, so as each procedure is returned, it’s immediately invoked with the next argument until the innermost procedure executes.
The uncurried version could be written as follows (notice how it requires 3 arguments to be passed at the same time)…
_global hero <<
_proc @hero(p_hero, p_partner, p_group)
write(p_hero,
" has a partner named ",
p_partner,
" and is a member of the ",
p_group)
_endproc
“Cool,” you might be thinking, “but why the heck would we ever need to do this?”
Good question.
Let’s suppose we want to create reusable components and configure them by fixing some of their arguments. As you’ve just seen, we can set a hero’s name using curry_hero and receive a returned procedure back. So let’s do that again.
Magik> partner << curry_hero("Batman")
proc partner(p_partner)
Magik>
partner now points to a procedure that’s waiting for the p_partner argument but has fixed the p_hero parameter’s value to be Batman.
So far so good. Let’s carry on.
Magik> grp << partner("Robin")
proc group(p_group)
Magik>
We’ve now created a pointer (grp) to another procedure where the partner parameter (p_partner) value has been fixed to Robin.
Finally we can invoke grp with the name of the superhero group and, because the procedures’ 3 required arguments have now been supplied, we see the results in line 2 below.
Magik> grp("JLA")
Batman has a partner named Robin and is a member of the JLA
Magik>
So what does that buy us?
Quite a bit actually.
We can use curried procedures to transform procedures that require multiple parameters into ones that take single arguments. This allows us to use functional composition (via chaining or piping),
We can also use currying as a mechanism for generalization and specialization. Returned procedures are specialized versions of their more generalized, enclosing, procedures.
Let’s look at examples of both cases.
Specialized Procedures
First we’ll see how more specialized procedures can be created from more general ones. Going back to our curry_hero example, if we wanted to change the group, we could simply do this…
Magik> grp("Avengers")
Batman has a partner named Robin and is a member of the Avengers
Magik>
Whoa! Did you see that? (No, not the fact I just said DC Characters were members of a Marvel superhero group.)
When I changed the grp procedure argument, it remembered the two enclosing procedures’ arguments (i.e. Batman and Robin) although the two enclosing procedures were no longer in scope (that is, grp had been returned in the previous call and was passed back as an independent procedure).
If you’re wondering how it still has access to parameters in the enclosing procedures, it’s because of those _import statements and something known as a closure.
So what we’ve just done, in this very simple example, is reuse the first 2 procedures with their values already set (sort of like inheriting values from a parent and grandparent) and allowed users to customize the 3rd.
In other words we’ve created a specialized instance (grp) from the more general partner and hero procedures.
Let’s look at another example.
_global add <<
_proc @add(p_number1)
_return _proc @number2(p_number2)
_import p_number1
_return p_number1 + p_number2
_endproc
_endproc
In line 2 we define the add procedure that takes a number argument (this is the generalized procedure). In line 3 we return a procedure that takes the second number. Recall this is a curried procedure because it requires 2 arguments but we are taking them one at a time.
We can invoke add in a couple of ways…
Magik> n << add(10)
proc number2(p_number2)
Magik> n(5)
15
Magik> add(1)(5)
6
First, in line 1, we’re supplying one argument (10) to add and getting a procedure (number2) returned. We can say the returned procedure is a partial application of add with p_number1 fixed in its closure scope (a partial application is a procedure that has had some, but not all, of its arguments provided).
We then invoke number2 (referenced via the variable n in line 4) with the second argument (5) to execute the procedure and display the result of 15.
The second way we can invoke add is shown in line 7, where we supply both arguments one after the other. When add(1) is invoked, it returns a partially applied procedure with p_number1 fixed to 1. But instead of assigning the procedure to a variable, like we did in the first example, we immediately invoke the returned procedure using (5). So the result is 6.
To demonstrate specialization we’ll use the first way of invoking add.
We have a general add procedure. Let’s specialize it.
Magik> add42 << add(42)
proc number2(p_number2)
Magik> add42(10)
52
Magik> inc << add(1)
proc number2(p_number2)
Magik> inc(42)
43
Magik> dec << add(-1)
proc number2(p_number2)
Magik> dec(42)
41
Magik> dec(dec(dec(42)))
39
Magik> inc(100)
101
Magik> dec(100)
99
Magik>
As you can see, we’ve created specialized versions of add (lines 1, 7 and 13) we can use independently. We’ve gone from the general case (add()) to the specialized cases of add42(), inc() and dec().
Function Composition
Okay, let’s move on to a composition example.
Suppose we have the following procedures.
_global sqr <<
_proc @sqr(p_number)
_return p_number * p_number
_endproc
_global triple <<
_proc @triple(p_number)
_return p_number * 3
_endproc
_global sqrt <<
_proc @sqrt(p_number)
_return p_number.sqrt
_endproc
We’d like to apply them, in order, to a value. We can certainly do it this way…
Magik> v << sqr(10)
100
Magik> v1 << triple(v)
300
Magik> val << sqrt(v1)
17.32
Magik>
Yeah… it works, but ugggh! We had to use temporary variables and multiple lines… could there be a better way?
A Better Way…
How about…
Magik> val << sqrt(triple(sqr(10)))
17.32
Magik>
Much better! But all those nested procedures can make it a bit difficult to read, especially when there are many procedures to be invoked (and we have to read from right to left). Can we do even better and make it more flexible and readable?
I think so… let’s utilize the reduce method defined in my article on Functional Programming with Magik to create a pipeline.
An Even Better Way…
We’ll define a simple callback procedure as follows…
_global apply_fn <<
_proc @apply(x, f)
_return f(x)
_endproc
Now we can use reduce to invoke procedures in a pipeline one after the other.
Magik> val << {sqr, triple, sqrt}.beeble_reduce(apply_fn, 10).first
17.32
Magik>
That is so much better. But what have we done?
We created a simple_vector with the procedures we want to invoke (in the order of invocation) and invoked the beeble_reduce method on it, passing in the apply_fn and an initial value of 10 (the value on which we want to invoke the procedures). Since beeble_reduce returned the result wrapped in a simple_vector, we simply took the first (and only) element and assigned it to val.
That’s it. Compared to the previous versions, this one is easier to read, easier to expand and easier to modify. In fact we could add 1000 procedures to the simple_vector and things would still work as long as the output of each preceding procedure is compatible with the input of the next procedure in the pipe.
And that’s an important point. Did you notice how all the procedures took one argument? When we invoke procedures one after another in a pipe, we’re using the point-free style where there are no intermediate variables. So the arity of the procedure has to be 1 (i.e. each procedure takes exactly one argument).
But what if we wanted to add two numbers? Obviously we’d need something like this…
_global add2 <<
_proc @add2(p_number1, p_number2)
_return p_number1 + p_number2
_endproc
Unfortunately that would break the pipe because add2 requires 2 arguments, so it’s a problem. What can we do?
If you said, “curry the procedure,” then come on by and pick up your gold star.
Remember that curried add procedure we wrote earlier and how we used it to create a specialized add42 procedure? Let’s plug that in now.
Magik> val << {sqr, triple, sqrt, add42}.beeble_reduce(apply_fn, 10).first
59.32
Magik>
In line 1 we’ve added the curried add42 procedure (recall this procedure is based on add with its first argument fixed to 42). The rest of the pipeline is invoked as before (yielding a value of 17.32 as it previously did) and add42 adds 42 to the result for a final value of 59.32.
Or, if we want to use add() directly to add an arbitrary number…
Magik> val << {sqr, triple, sqrt, add(10)}.beeble_reduce(apply_fn, 10).first
27.32
Magik>
Pretty sweet eh? But you know what? We can do even better… let’s wrap all this goodness up in some syntactic sugar…
_global pipe <<
_proc @pipe(_gather p_fns)
_return _proc @reduce(p_initial_value)
_import p_fns
_local l_apply_fn << _proc @apply(x, f)
_return f(x)
_endproc
_return p_fns.beeble_reduce(l_apply_fn, p_initial_value).first
_endproc
_endproc
You can now use the pipe procedure to pipeline your procedures. Here are a few examples…
Magik> pipe(sqr, triple, sqrt)(10)
17.32
Magik> f << pipe(sqr, triple)
proc reduce(x)
Magik> f(10)
300
Magik> f(4)
48
Magik>
And that really is how easy it is to implement function composition in Magik.
A Real World Example
Suppose you want to find hotels by name and have two separate data sources: the standard VMDS and a file (hotels.json) containing the following GeoJSON (I’ve removed most of the records for brevity).
{
"type":"FeatureCollection",
"features":[
{
"type":"Feature",
"properties":{
"Record ID":"hotel132:(Acorn Guest House)",
"Name":"Acorn Guest House",
"Address 1":"154 Chesterton Road",
"Address 2":"Cambridge",
"Type":"Guest House",
"Annotation":"unset"
},
"geometry":{
"type":"GeometryCollection",
"geometries":[
{
"type":"Point",
"coordinates":[
0.129869073,
52.215243686
]
}
]
}
},
{
"type":"Feature",
"properties":{
"Record ID":"hotel132:(Arundel House)",
"Name":"Arundel House",
"Address 1":"Chesterton Road",
"Address 2":"Cambridge",
"Type":"Hotel",
"Comments":""
},
"geometry":{
"type":"GeometryCollection",
"geometries":[
{
"type":"Point",
"coordinates":[
0.119145977,
52.212793586
]
},
{
"type":"Feature",
"properties":{
"Record ID":"text:(gis_id(5233539,1197014308,967000))",
"text_id":"gis_id(5233539,1197014308,967000)",
"type":"0",
"aid":"a sw:ds_simple_vector",
"rwo_id":"gis_id(5233539,1197231247,597)",
"rwo_code":"1010",
"app_code":"2",
"meta_data_id":"0",
"bounds":"a bounding_box",
"ds!version":"2281701378",
"n_coords_or_sectors":"2",
"bucket_table_index":"4",
"coord_1":"coordinate:(0.1191,52.21)",
"coord_2":"coordinate:(0.1191,52.21)",
"height":"1.000",
"orientation":"0.000",
"just":"22",
"nchars":"13",
"string_buffer":"Arundel House"
},
"geometry":null
}
]
}
},
.
.
.
{
"type":"Feature",
"properties":{
"Record ID":"hotel132:(University Arms)",
"Name":"University Arms",
"Address 1":"Regent Street",
"Address 2":"Cambridge",
"Type":"Hotel",
"Annotation":"unset"
},
"geometry":{
"type":"GeometryCollection",
"geometries":[
{
"type":"Point",
"coordinates":[
0.125017264,
52.201952570
]
}
]
}
}
]
}
You’d like the ability to plug in each data source as necessary with minimum impact to your application code.
How would you do that?
Well, here’s one way…
_global get_hotel_from_vmds
get_hotel_from_vmds <<
_proc @get_hotel_from_vmds(p_table, p_name)
write("getting hotel from VMDS with name=", p_name)
_constant PRED << predicate.eq(:name, p_name)
_constant HOTELS << p_table.select(PRED)
print(HOTELS)
_endproc
$
_global get_hotel_from_file
get_hotel_from_file <<
_proc @get_hotel_from_file(p_filename, p_name)
write("getting hotel from FILE with name=", p_name)
_constant HOTELS << beeble_object.from_json_file(p_filename)
_for a_hotel _over HOTELS.features.fast_elements()
_loop
_if a_hotel.properties.|Name| = p_name
_then
print(a_hotel.properties)
_leave
_endif
_endloop
_endproc
$
You simply invoke the appropriate function depending on which data source is required.
Magik> get_hotel_from_file("I:\marvin\Smallworld\hotels.json", "Arundel House")
getting hotel from FILE with name=Arundel House
{"Address 1":"Chesterton Road","Address 2":"Cambridge","Comments":"","Record ID":"hotel132:(Arundel House)","Name":"Arundel House","Type":"Hotel"}
Magik>
$
Magik> get_hotel_from_vmds(gis_program_manager.databases[:gis].collections[:hotel], "Arundel House")
getting hotel from VMDS with name=Arundel House
1 name Arundel House
address1 Chesterton Road
address2 Cambridge
type Hotel
comments
location point:(gis_id(5233539,1197231247,597))
annotation text:(gis_id(5233539,1197014308,967000))
Magik>
$
In line 1, we invoke the procedure to search the hotels.json file for the record that matches, “Arundel House”. In line 8 we invoke the procedure to search the VMDS hotel table for, “Arundel House”.
It works, but we’re tightly coupling our application code to the data source. So if we need to retrieve hotels from dozens of places in the code, we have to find all those places and replace the code if we want to change the backend.
How can we make this better?
Well, since this is an article about currying, I’m guessing you’ll say, “curry the functions.”
Yes! That’s the ticket, so let’s do that now.
_global curried_get_hotel_from_vmds
curried_get_hotel_from_vmds <<
_proc @curried_get_hotel_from_vmds(p_table)
_return _proc @get_hotel(p_name)
_import p_table
write("getting hotel from VMDS with name=", p_name)
_constant PRED << predicate.eq(:name, p_name)
_constant HOTELS << p_table.select(PRED)
print(HOTELS)
_endproc
_endproc
$
_global curried_get_hotel_from_file
curried_get_hotel_from_file <<
_proc @curried_get_hotel_from_file(p_filename)
_return _proc @get_hotel(p_name)
_import p_filename
write("Curried version: getting hotel from FILE with name=", p_name)
_constant HOTELS << beeble_object.from_json_file(p_filename)
_for a_hotel _over HOTELS.features.fast_elements()
_loop
_if a_hotel.properties.|Name| = p_name
_then
print(a_hotel.properties)
_leave
_endif
_endloop
_endproc
_endproc
$
Notice the pattern in lines 5 and 25?
It’s the same pattern we saw earlier in the curry_hero example that returns a procedure that accepts the next argument.
So we’re returning a procedure, get_hotel, that takes a hotel name argument. However this procedure does different things depending on where it was returned from. If it was returned from curried_get_hotel_from_vmds (line 3), then it uses the VMDS hotel table to search for the record, but if it was returned from curried_get_hotel_from_file (line 23), it uses the file to search for the record.
Because we’ve curried the functions, we’ve effectively separated the function definitions from their execution. That means we’re able to get a generic handle to the function, get_hotel, without worrying about its implementation details.
Magik> get_hotel << curried_get_hotel_from_file("I:\marvin\Smallworld\hotels.json")
proc get_hotel(p_name)
Magik> get_hotel("University Arms")
Curried version: getting hotel from FILE with name=University Arms
{"Address 1":"Regent Street","Annotation":"unset","Address 2":"Cambridge","Record ID":"hotel132:(University Arms)","Name":"University Arms","Type":"Hotel"}
Magik>
$
Magik> get_hotel << curried_get_hotel_from_vmds(gis_program_manager.databases[:gis].collections[:hotel])
proc get_hotel(p_name)
Magik> get_hotel("University Arms")
getting hotel from VMDS with name=University Arms
1 name University Arms
address1 Regent Street
address2 Cambridge
type Hotel
comments unset
location point:(gis_id(5233541,423820838,18796))
annotation unset
Magik>
$
Notice how we’re able to simply invoke get_hotel (lines 4 and 13) with the name of the hotel we’re looking for without regard to whether the data will come from the VMDS or the file?
This allows us to pass get_hotel to other functions or methods without them having to know the concrete implementation details. They simply use the get_hotel function blissfully unaware of where the data originated.
But… we still had to invoke curried_get_hotel_from_file (line 1) and curried_get_hotel_from_vmds (line 10) separately before setting get_hotel.
We can do even better by consolidating everything into one function to rule them all.
_global get_hotel_backend
get_hotel_backend <<
_proc @get_hotel_backend(p_source)
_if p_source _is :vmds
_then
_return curried_get_hotel_from_vmds(gis_program_manager.databases[:gis].collections[:hotel])
_elif p_source _is :file
_then
_return curried_get_hotel_from_file("I:\marvin\Smallworld\hotels.json")
_else
write("***Error: unknown backend source.")
_endif
_endproc
$
The get_hotel_backend function takes an argument specifying the source and returns the appropriate curried function (lines 7 and 11) with the source data store set, to either the VMDS table or the file, and awaiting the hotel name.
Now we can simply invoke get_hotel_backend (lines 1 and 8 below), with the desired argument, to retrieve the function hooked into the correct backend data store implementation.
Magik> get_hotel << get_hotel_backend(:file)
proc get_hotel(p_name)
Magik> get_hotel("Arundel House")
Curried version: getting hotel from FILE with name=Arundel House
{"Address 1":"Chesterton Road","Address 2":"Cambridge","Comments":"","Record ID":"hotel132:(Arundel House)","Name":"Arundel House","Type":"Hotel"}
Magik> get_hotel << get_hotel_backend(:vmds)
proc get_hotel(p_name)
Magik> get_hotel("Arundel House")
getting hotel from VMDS with name=Arundel House
1 name Arundel House
address1 Chesterton Road
address2 Cambridge
type Hotel
comments
location point:(gis_id(5233539,1197231247,597))
annotation text:(gis_id(5233539,1197014308,967000))
Magik>
$
And, as shown in lines 4 and 11, get_hotel is passed a hotel name and returns the data from the pre-configured backend data store.
This means if the backend ever needs to change, get_hotel can be re-initialized from get_hotel_backend in one place. Since the application code is just using the abstracted get_hotel function, it does not need to change. So even if you have dozens or hundreds of places get_hotel is invoked, you only need to change one piece of code to plug in another backend — or better yet, read the backend argument from a config file and you don’t have to make any code changes.
This pattern is useful in many other areas too, such as mocking a backend database for unit testing or creating different pluggable frontend GUIs.
And in Conclusion…
Curried procedures have many uses, including composition and specialization. They allow you to transform n-ary procedures into unary procedures so they can be included in composition pipelines. They also allow you to create abstract functions by hiding implementation details. The application code does not need to know how a function does its work, it just describes what it wants to do and the underlying, concrete implementation performs the necessary steps.
They’re used extensively in Functional Programming but are also useful in the Object Oriented paradigm. So if you’d like a powerful tool to add to your programming tool belt, take a look at curried procedures and think about how you can use them to solve certain classes of problems.