• Skip to primary navigation
  • Skip to main content

MarkHing.com

Carefully Crafted Content

  • Home
  • Smallworld GIS
  • Software
  • Investing
  • Random Musings
  • Blog
  • About
    • Privacy Policy
    • Terms of Service
    • GNU General Public License
  • Contact Me
  • Show Search
Hide Search

Magik Curry

Mark Hing · Jan 18, 2020 ·

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 parameter (p_hero) and returns a procedure that takes another parameter (p_partner) which returns a final procedure that takes one more parameter (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 parameter one at a time until all parameters 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 parameters 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 parameter 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 parameters 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 allow 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.

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 specialized versions of more general procedures.

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.

Smallworld

About Mark

Mark Hing, a Value Investing and Smallworld GIS specialist, created the Automatic Investor software and is the author of "The Pragmatic Investor" book.

(Buy the book now on Amazon.ca)
(Buy the book now on Amazon.com)

As President of Aptus Communications Inc., he builds cutting-edge FinTech applications for individual investors. He has also used his software expertise to architect, develop and implement solutions for IBM, GE, B.C. Hydro, Fortis BC, ComEd and many others.

When not taking photographs or dabbling in music and woodworking, he can be found on the ice playing hockey -- the ultimate sport of sports.

linkedin   Connect with Mark

All views expressed on this site are my own and not those of my employer.

Copyright © 2023 Aptus Communications Inc. All Rights Reserved.