Unfortunately, Factor's
curry
has nothing todo with this delicious Indian meal.
What's curry?
I hear you asking, "What the hell are you talking about? What's curry?" Curry is a simple function, named after mathematician Haskell Curry who developed combinatory logic, though not currying. The man usually credited with that discovery, a bit earlier on, and far before modern computers were developed, was Moses Schönfinkel, so the process may also be known as Schönfinkelisation. Currying is the technique of implementing a multiargument function by chaining together several single-argument functions. To use Haskell as an example, say you have the function
add
, which adds two integers. The function could be defined as
add :: Integer -> (Integer -> Integer)
add = \x -> (\y -> x+y)
Not all of you are Haskellers, so let me explain how that works. The first line is the type signature (that fact is indicated by the
::
) which tells us that the type of add
is a function (indicated by ->
which takes an Integer
and returns a function (Integer -> Integer
) from Integer
to Integer
. The second line is a series of lambdas which defines the actual function. (We know it's a definition because of =
.) The syntax \a -> b
specifies an anonymous function which takes a value named a and returns the expression b. So add is a function which takes a value x, and returns a function y which takes a value y and returns the sum of x and y. As it happens, because Haskell is a so-called "auto-currying" language, and because ->
is right-associative due to the frequency of this construct, the exact same thing can be written as
add :: Integer -> Integer -> Integer
add x y = x+y
How Factor does it
Currying is a great construct, but it has almost nothing to do with Factor's
curry
. Factor's curry
is, by the more fashionable nomenclature of those at Lambda the Ultimate, partial application: given a function, apply it with a partial argument list, yielding a new function with just those arguments filled in. (I think this operation would more appropriately be called with
in Factor, but for now, it's curry
.) In Scheme, this is accomplished with the nice macro cut
(SRFI 26).At first, it may sound like partial application only suits applicative, not concatenative languages, as we do not usually think about applying a function to its arguments, but rather composing words which operate on a stack. But in this context, all we mean by partial application, or "currying", is getting something onto the stack before the quotation is called.
With this realization, this sort of currying is easy to do on quotations. In Joy, and in Factor as it was a few years ago, quotations are lists. To prepend a constant to that list is the same as to push it on the stack before executing the quotation. The operation is simply
cons
. Now that Factor's quotations are specially tagged arrays, the code for prepending an element is swap add*
, but the meaning remains the same. If you're a careful reader, you saw that I made a mistake there: if you want to partially apply a quotation with a word as your data, this word has to be "literalized" first, or wrapped so that it is not executed but rather pushed. So, until recently, the code for curry
amounted to swap literalize add*
. (literalize
does nothing if its argument is not a word.)But now,
curry
has been added as a primitive for better interpreter performance, and a compiler intrinsic so it can be safely used in compiling Factor code.Derived operations
You might be wondering if this single operation can take the place of all of the uses of Scheme's
cut
macro. It can. Using only curry
, call
and basic stack shuffling words, it is easy to define several more complex operations. For something simple, here's how to partially apply a quotation to two arguments, rather than one:
: 2curry ( a b quot -- quot' )
curry curry ; inline
The
inline
declaration is there to allow things using 2curry
to be compiled. Composing two quotations is slightly more complicated. We want this to be fast, so we'll build a quotation, using curry
, which calls the two quotations in order. This is more efficient than composition using append
.
: compose ( quot1 quot2 -- q1q2 )
[ >r call r> call ] 2curry ; inline
Another useful operation is
curry*
, which partially applies a quotation based on the second item on the stack. As an example, curry* map
is a complete replacement for map-with
. Because of this, all of the -with
combinators have been removed in favor of direct use of curry
.
: curry* ( param object quot -- object curry )
>r swap r> [ >r swap r> call ] 2curry ; inline
Practical applications
Aside from eliminating the need for all
-with
combinators (I'm very glad I'll never have to write another one myself!), curry
makes writing all sorts of combinators much simpler. For an involved example, see the new definitions of the sequence combinators, which are not so scary anymore. For something simpler, take the problem of counting up the number of elements in a sequence which satisfy a certain property. Say you have the sequence { 1 2 4 1 2 1 }
and you want to count tne number of ones. A simple way to do this is to simply take the subset satisfying the property, and then count how long it is:
: count ( seq quot -- num )
subset length ; inline
Using this, code to count up the number of ones could be
[ 1 = ] count
. This pattern is used a few times in the Factor core, though there is no word named count
. Using it, we can write a count=
word which counts the number of times an element occurs in a sequence:
: count= ( seq elt -- num )
[ = ] curry count ;
But there's a downside to this: for no good reason, you build a new sequence, which is immediately consumed and discarded. What if we just counted up the length instead? The
count
combinator could be rewritten as:
: count ( seq quot -- num )
0 -rot [ rot >r call r> swap [ 1+ ] when ] curry each ;
There's a little bit of stack acrobatics around calling that quotation so that the quot has access to lower things on the stack.
If you think about it, there are actually two pieces here, which we can factor out. The first piece, which I'll call
sigma
, incrementally maps all values in a sequence to a new value, and sums the results. The second piece calls a quotation and converts the boolean result to either one or zero, which can be summed up. When these are separated, the implementation is even cleaner:
: sigma ( seq quot -- num )
[ rot >r call r> + ] curry 0 -rot each ; inline
: count ( seq quot -- num )
[ [ 1 ] [ 0 ] if ] compose sigma ; inline
Both of these are faster than their counterparts (
map sum
and subset length
) by only a constant factor, but, just for fun, let's measure this. Right now, Factor doesn't have very advanced profiling tools, but I haven't felt the need for them. For many things, a simple timing function can work well. After entering in the definitions for sigma
, the following lines can be entered at the listener to time the word against map sum
:
: slow 1000000 [ ] map sum ;
: fast 1000000 [ ] sigma ;
{ slow fast } [ compile ] each
[ slow ] time ! prints time for map sum
[ fast ] time ! prints time for sigma
On my computer,
slow
takes 195ms, and fast
takes 126ms, for around a 35% speedup!
1 comment:
WTF are you on? "More fashionable"? How about "correct"?
Post a Comment