Thursday, June 12, 2008

Let There Be Local Variables

Actually, there aren't any. There are only function parameters. But there's a workaround: use functions.



Suppose you're writing a token analyser for some input. 'token-generator is a generator function. You need to determine the type of a token - return 'string if you think it's a string, 'char, 'int, etc. Naïvely, you might



(def token-type (token-generator)
(get-token-type (token-generator)))

(def get-token-type (tok)
(if (is (tok 0 #\")) 'string
(is (tok 0 #\#) 'char)
(is-digit (tok 0)) 'int))


So you get your input from 'token-generator, but you're really interested in 'tok. So the function 'get-token-type provides the local variable 'tok for you to work with. The disadvantage is that your namespace gets polluted with a zillion little functions that are only useful to their immediate neighbours. Wouldn't it be nice to, like, click Refactor > Inline Method like you can in a real language?



Well, you can.



(def token-type (token-generator)
((fn (tok)
(if (is (tok 0 #\")) 'string
(is (tok 0 #\#) 'char)
(is-digit (tok 0) 'int))) (token-generator)))


The ( (fn (tok) (if blah)) (token-generator) ) is a call to an anonymous function that declares the 'tok parameter for you to work with.



Now your namespace is no longer polluted, but you have this damned ugly thing in the middle of your function. No ordinary human is expected to read this.



Enter 'let



'let is a macro that transforms its humanly-readable input into something resembling the mess above.



(def token-type (token-generator)
(let tok (token-generator)
(if (is (tok 0 #\")) 'string
(is (tok 0 #\#) 'char)
(is-digit (tok 0) 'int))))


'let lets you pretend you are actually declaring a local variable. Coming from another language, you might think that this is, really and truly, a local variable. It's not. Behind the scenes, it's an anonymous function creating a lexically scoped 'tok parameter. But you don't care. You have your local variable.



Here is the pattern:



(let a b stuff)


becomes:



((fn (a) stuff) b)



'with is a more general purpose macro that does the same thing, but for multiple variables.



(with (a 1 b 2 c 3) stuff)


becomes:



((fn (a b c) stuff) 1 2 3)


'let exists because in the special case of binding just one variable, we don't need parens to separate the variable bindings from the body of the code. Just because you write in lisp doesn't mean you actually like parens ...

No comments: