10.02.2006
5:57 AM

Caching values in messages is really neat, I have to admit, but it is oftentimes rather verbose. To illustrate this point, I'll give you an example of a curry function without any helper functions.

curry := method(proc, left,
   IoVM getSlot("Block") clone setArgumentNames(list("right")) setMessage(
       Message clone setCachedResult(getSlot("proc")) setAttached(
           message(call) clone setArguments(list(
               Message clone setCachedResult(getSlot("left")),
               message(right)
           ))
       )
   ) setIsActivatable(true)
)

add1 := curry(method(x, y, x + y), 1)
add1(2) println // 3

Now, this is not exactly what I would call elegant! Not yet, anyway. One of the major things here is that we have to explicitly clone the Block, and set all of its parameters up manually. method() is a lot nicer for these sorts of things. Unfortunately, that doesn't leave us with a nice way to cache values within the message. So, we define a helper function, Block prependMessage(), and redefine our curry to use that.

getSlot("Block") prependMessage := method(msg,
   setMessage(msg setNext(getSlot("self") message))
   getSlot("self")
)

curry := method(proc, left,
   method(right,
       getSlot("proc") call(getSlot("left"), getSlot("right"))
   ) prependMessage(
       message(setSlot) clone setArguments(list(
           Message clone setCachedResult("proc"),
           Message clone setCachedResult(getSlot("proc"))
       )) setNext(
       message(setSlot) clone setArguments(list(
           Message clone setCachedResult("left"),
           Message clone setCachedResults(getSlot("right"))
       )))
   )
)

add2 := curry(method(x, y, x + y), 2)
add2(1) // 3

Uh-oh. That's really not that much better at all! Maybe we abstract out that setSlot() stuff? Here we'll define an attach() method that'll do that repetitive work for us.

getSlot("Block") prependMessage := method(msg,
   setMessage(msg setNext(getSlot("self") message))
   getSlot("self")
)

getSlot("Block") attach := method(name,
   prependMessage(message(setSlot) clone setArguments(list(
       Message clone setCachedResult(name),
       Message clone setCachedResult(call sender getSlot(name))
   )))
   getSlot("self")
)

curry := method(proc, left,
   method(right,
       getSlot("proc") call(getSlot("left"), getSlot("right"))
   ) attach("proc") attach("left")
)

add2 := curry(method(x, y, x + y), 2)
add2(1) println // 3

Wow, that really is a lot better! What we've got here is an explicit closure. We define a method, then we tell it what names to import from the enclosing scope. Now sure, that's all well and good, if you want to name things. But I went a step further and created something kind of monstrous. It's called Message interpolate(). It's a lot like Sequence interpolate(), in that it takes values from the outlying scope and puts them into the message. I abstrated out some of it, creating Message map(), which may just have other uses.

Message map := method(
   name := call argAt(0) name
   body := call argAt(1)

   call sender setSlot(name, self)
   call sender doMessage(body)
  
   branch := self
   while(branch,
       branch arguments foreach(arg,
           call delegateTo(arg)
       )
       branch := branch attached
   )
  
   if(next,
       call delegateTo(next)
   )
  
   self
)

Message interpolate := method(sender,
   if(call argCount ==(0),
       sender := call sender
   )
 
   self map(branch,
       if(branch name == "curlyBrackets",
           branch setCachedResult(sender doMessage(branch arguments at(0)))
       )
   )
)

IoVM getSlot("Block") interpolate := method(
   (result := getSlot("self") clone) message interpolate(call sender)
   getSlot("result")
)

Object curry := method(
   method(right,
       {call evalArgAt(0)} call({call evalArgAt(1)}, getSlot("right"))
   ) interpolate
)

add1 := curry(method(x, y, x + y), 1)
add1(2) println // 3

Now that's a sexy curry.