Code archives/Miscellaneous/Lambda syntax (closures)
This code has been declared by its author to be Public Domain code.
Download source code
| |||||
MAYBE IF I JUST PUSH A LITTTTLE HARDER THIS SQUARE PEG WILL FIT IN THAT ROUND HOLE ...ahem. This library (which is not short, simple, or clean, but anyway) adds support to Blitz3D/+ for lambda forms: anonymous functions, closures, and nested or "inner" function blocks. No, no magic involved. The system is inspired by Boost::Lambda, although lacking templates or overloading, it's nowhere near as seamless. The method is really very simple: actions are packaged up using "thunk constructors" that have a name that visually indicates what you want to do, but actually return a delayed evaluation that can be invoked later on. The challenge is mainly in making it look tidy enough to use. Thus, we can write code like this: Local L = Func("x", Add("x", 5)) ...to put a "function" in L which adds 5 to its argument (when it is called later, using CallFunc). Well ain't that handy! You can then use this to pass custom functions inline to list sorts, store event handlers directly in GUI component objects, and so on! Basically this is a very marginal improvement on simply using inlined scripting language code. Local variables to a closure are named using string literals (case-sensitive, can't begin with "@"); the last argument to Func is the closure's body expression (all closures must take at least one argument, and may take up to eight; you can combine two statements into one body expression with a "Do" block). You can pass values directly into expressions to bind them into the function objects - as we did with the number 5 above, and can therefore use "external" variables as well: Function MakeAdder(n) Return Func("x", Add(n, "x")) End Function Every time you call MakeAdder, it'll return a shiny new function that will increment its argument by whatever it closed over. How handy. Note that since there's a distinction between code that runs immediately (normal Blitz functions) and code that secretly packages delayed actions (lambda code/thunks), if you want to do something similar to the above using only lambdas (create a "curried function"), you need to do it like this: Local MakeAdder = Func("n", Lambda("x", Add("n", "x"))) Local adder = CallFunc(MakeAdder, 5) "Lambda" is to "Func" as "Add" is to "+": it's the thunking-form for use within closures, as opposed to Func which returns a value immediately to Blitz code. There's also "Apply" to use instead of "CallFunc" within lambdas (Apply is thunking, CallFunc is value-returning). If you want to close over strings or floats, you should package them with LString and LFloat (unpack results with GetLString and GetLFloat respectively). This is because the lambda constructors make minor use of strings for representing internal values, and also (as you can see) variable names; and CallFunc only deals in integers (the bundled math functions are int-only anyway for the moment). There's also an "inner function" form: Function GetPrinter(count) Local p = InnerFunc("x", "y") tWrite LString("Hello ") tPrint "x" Loop("n", 1, count, tPrint(IntToString("n"))) tWrite LString("Goodbye ") tPrint "y" EndFunc Return p End Function CallFunc(GetPrinter(3), LString("Alice"), LString("Bob")) ; Prints: ; Hello Alice ; 1 ; 2 ; 3 ; Goodbye Bob This function returns its two-parameter inner function after closing-over the "count" environment. (If using IDEal or something else that "corrects" indentation, you might want to wrap the nested instructions in a Repeat/Until True block so they indent.) You'll also notice that those member statements that would have the same name as their normal Blitz code counterparts ("Print") are prefixed with a small t ("tPrint"), to avoid name clashes. There are a few others like this in the library. Even though nested functions are multi-line, they're still limited to using one-liner control blocks (unless of course you nest further inner functions within them...). Replicating the syntax of control structures too would be going a bit far. Built-in control structures in the Basis library include tIf (which functions as a ternary operator), tWhile, Loop (a bit like For), tDelay and Force (implementing futures and promises), Let (names a local variable for use in the body expression), Do (multiple statements), and Fixpoint (allows recursion). Control structures are properly lazy (i.e. tIf will only evaluate the correct arm, and ignore the other), and all of them return a value, usually the last value evaluated (this is the Functional Programming Way). It is possible to change the value of variables, with SetVar, but do try to avoid it if at all possible (it is never necessary to set a variable, but sometimes the obvious option... and tWhile won't work if you never mutate anything). Among other things this will kinda highlight the fact that closures don't actually share environment objects. Contrary to what you might expect, there are no Lisp-style list or cons manipulation functions. I figure it makes more sense to write them to suit an existing List library (such as my own... this will happen shortly), and also because without a garbage collector it becomes very hard indeed to keep track of "ownership" of such resources, and would probably just get confusing really quickly. On the subject of memory management, using lambdas without a garbage collector is usually a real pain: to alleviate this slightly, by default this library is set up to allocate objects using my AutoReleasing semiautomatic-garbage-collector. This will take some of the pain out of tracking the lifetimes of lambda objects (all of it, in fact, if you never return them past the border of an AutoRelease block). If you really want to manually manage memory, you can comment out the AutoRelease code without breaking the library, as the library's inner workings don't use it, with one exception: LString. You'll have a very hard time keeping track of new Lambda objects though. The Basis library also includes a WithAutoReleasePool function, which creates and clears an AutoRelease pool around the body expression, to isolate its allocations and tighten up memory use (obviously, comment this if you are working without AutoRelease). (Using AutoRelease conversely introduces a bug which I can't be bothered to address: LStrings are AutoReleased separately from the functions, so closed-over LStrings may be freed before their supposed owners are, which will crash. Design around this.) Basis Library, "Lambda-Basis.bb": The evaluation system is not tail-call optimised. This means unlimited recursion is not safe, and absolutely will result in a stack overflow crash (in fact, since one function call actually requires four or five function calls, it will do it even sooner than regular Blitz code). Use the looping primitives, or call out to a normal Blitz function, to take care of such things: this is just a bit of FP-icing for Blitz, not a full programming style replacement! The same goes for other functionality: don't be afraid to call out to normal B3D code (you'll have to, to do anything interesting). The Basis library should make it fairly clear how to add more thunking functions for the lambda engine to use, both eager (normal) and lazy (control). The actual lambda library is very small and only really consists of lambda constructors and the evaluation functions. Most of the useful stuff (some integer math, input and output, control structures) is in the Basis library, "Lambda-Basis.bb". Here are some more crappy examples: Yeah, that's right, that's a fixed-point combinator in classic Blitz Basic code. As it stands, this library depends on AutoRelease for "garbage collection", available from the code archives (although this can be removed at your peril). It also requires the free FastPointer DLL by Mikhail Vostrikov, for working with real function pointers in Blitz Classic (it is possible to engineer a similar library to work without this, but it would be painful. Don't ask me to show you how). Enjoy! | |||||
; Lambda library for Blitz3D/+ ;============================== ; Requires FastPointer DLL (free): http://www.fastlibs.com/index.php ;Memory management (optional, but extremely useful) Include "AutoRelease.bb" ;Get this file at http://www.blitzbasic.com/codearcs/codearcs.php?code=2978 ;Basis library (useful predefined constructor functions) Include "Lambda-Basis.bb" ;Get this file at http://www.blitzbasic.com/codearcs/codearcs.php?code=2992 Type Closure Field rc.RefCounted ;Remove this if not using AutoRelease Field argn$[9], env.Closure Field expr.Thunk, argv End Type Type Thunk Field argn$[9], isVarName ;isVarName = bit array; upper bits are "isThunk", bit 30 is "isBound" Field val[9], argv ;val = args passed in (inc. thunks, vars), argv = evaluated args passed to fptr Field fptr, argc, env.Closure ;Function pointer, arg count (neg for isLazy), calling lambda End Type Const LAMBDA_APPLY_CONSTPTR = 1 Global LAMBDA_private_CIF_.Closure ;Create an inline anonymous function and return it to external Blitz code Function Func(a0$, a1$, a2$ = "", a3$ = "", a4$ = "", a5$ = "", a6$ = "", a7$ = "", a8$ = "", doAR = True) Local argc = 2 + (a2 <> "") + (a3 <> "") + (a4 <> "") + (a5 <> "") + (a6 <> "") + (a7 <> "") + (a8 <> "") Local f.Closure = MakeClosureInner(doAR) f\argn[0] = a0 : f\argn[1] = a1 : f\argn[2] = a2 : f\argn[3] = a3 : f\argn[4] = a4 f\argn[5] = a5 : f\argn[6] = a6 : f\argn[7] = a7 : f\argn[8] = a8 If Asc(f\argn[argc - 1]) <> 64 Then RuntimeError "Expecting thunk as body expression of lambda function" f\expr = Object.Thunk Int Mid(f\argn[argc - 1], 2) If LAMBDA_private_CIF_ <> Null Then PurgeThunkTreeFromCIF f\expr, LAMBDA_private_CIF_\expr Return Handle f End Function ;Create an inline anonymous function and return it to closure code Function Lambda$(a0$, a1$, a2$ = "", a3$ = "", a4$ = "", a5$ = "", a6$ = "", a7$ = "", a8$ = "") Return MakeThunk(0, Func(a0, a1, a2, a3, a4, a5, a6, a7, a8, False)) End Function ;Invoke a closure with arguments Function CallFunc(L, a0 = 0, a1 = 0, a2 = 0, a3 = 0, a4 = 0, a5 = 0, a6 = 0, a7 = 0, a8 = 0) Local F.Closure = Object.Closure L If Not F\argv Then F\argv = CreateBank(9 * 4) Local tmp[8], i, val : For i = 0 To 8 tmp[i] = PeekInt(F\argv, i * 4) Next PokeInt F\argv, 0, a0 : PokeInt F\argv, 4, a1 : PokeInt F\argv, 8, a2 PokeInt F\argv, 12, a3 : PokeInt F\argv, 16, a4 : PokeInt F\argv, 20, a5 PokeInt F\argv, 24, a6 : PokeInt F\argv, 28, a7 : PokeInt F\argv, 32, a8 val = EvalThunk(F\expr, F) For i = 0 To 8 PokeInt F\argv, i * 4, tmp[i] Next Return val End Function ;Invoke a closure with arguments and return it to closure code Function Apply$(L$, a0$ = "", a1$ = "", a2$ = "", a3$ = "", a4$ = "", a5$ = "", a6$ = "", a7$ = "") Return MakeThunk(1, L, a0, a1, a2, a3, a4, a5, a6, a7, 9) End Function ;Define a new nested inner function Function InnerFunc(a0$, a1$ = "", a2$ = "", a3$ = "", a4$ = "", a5$ = "", a6$ = "", a7$ = "", a8$ = "") Local argc = 2 + (a2 <> "") + (a3 <> "") + (a4 <> "") + (a5 <> "") + (a6 <> "") + (a7 <> "") + (a8 <> "") Local f.Closure = MakeClosureInner(True) f\argn[0] = a0 : f\argn[1] = a1 : f\argn[2] = a2 : f\argn[3] = a3 : f\argn[4] = a4 f\argn[5] = a5 : f\argn[6] = a6 : f\argn[7] = a7 : f\argn[8] = a8 f\expr = Object.Thunk Int Mid(MakeThunk(0, ""), 2) ;Call before setting CIF! f\expr\argv = CreateBank(0) : f\expr\argc = 0 f\env = LAMBDA_private_CIF_ : LAMBDA_private_CIF_ = f Return Handle f End Function ;End a nested inner function definition Function EndFunc() LAMBDA_private_CIF_ = LAMBDA_private_CIF_\env End Function ;Manually free closure objects Function FreeFunc(L.Closure) If L\expr <> Null Then FreeThunk L\expr If L\argv Then FreeBank L\argv Delete L End Function ;(Internal) Internal constructor for closure objects: uses AutoRelease by default Function MakeClosureInner.Closure(doAR) Local L.Closure = New Closure If doAR Local freePtr = FunctionPointer() : Goto skip : FreeFunc L ;Comment these lines if not using AutoRelease .skip L\rc = NewRefCounted(freePtr, TypePointer(L)) AutoRelease L\rc EndIf Return L End Function ;(Internal) Copy closure objects (for use with Lambda) Function CopyFuncInner.Closure(L.Closure, doAR) Local C.Closure = MakeClosureInner(doAR), i For i = 0 To 8 C\argn[i] = L\argn[i] Next If L\expr <> Null Then C\expr = CopyThunkInner(L\expr) If L\argv Then C\argv = CreateBank(BankSize(L\argv)) : CopyBank L\argv, 0, C\argv, 0, BankSize(L\argv) C\env = L\env Return C End Function ;(Internal) Attach an environment to a function to form a complete closure Function BindFuncEnvironment(L.Closure, env.Closure) If env <> Null L\argv = CreateBank(BankSize(env\argv) + 9 * 4) CopyBank env\argv, 0, L\argv, 9 * 4, BankSize(env\argv) Else L\argv = CreateBank(9 * 4) EndIf L\env = env End Function ;(Internal) Get the Func out of a lambda-thunk for manual processing Function GetLambdaFunc.Closure(lam$) Local L.Thunk = Object.Thunk Int Mid(lam, 2) Return Object.Closure L\val[0] End Function ;(Internal) Package a delayed action Function MakeThunk$(fptr, a0$, a1$ = "", a2$ = "", a3$ = "", a4$ = "", a5$ = "", a6$ = "", a7$ = "", a8$ = "", maxArgC = 0) Local t.Thunk = New Thunk, i If maxArgC t\argc = maxArgC Else ;Count them t\argc = 1 + (a1 <> "") + (a2 <> "") + (a3 <> "") + (a4 <> "") + (a5 <> "") + (a6 <> "") + (a7 <> "") + (a8 <> "") EndIf t\fptr = fptr t\argn[0] = a0 If fptr t\argn[1] = a1 : t\argn[2] = a2 : t\argn[3] = a3 : t\argn[4] = a4 t\argn[5] = a5 : t\argn[6] = a6 : t\argn[7] = a7 : t\argn[8] = a8 t\argv = CreateBank((Abs t\argc) * 4 + 4) EndIf For i = 0 To (Abs t\argc) - 1 If Asc(t\argn[i]) = 64 Then t\val[i] = Int Mid(t\argn[i], 2) : Else t\val[i] = Int t\argn[i] Next If LAMBDA_private_CIF_ <> Null ;Add statement to InnerFunc, but remove child expressions from it For i = 0 To (Abs t\argc) - 1 If Asc(t\argn[i]) = 64 Then RemoveThunkFromCIF Object.Thunk t\val[i], LAMBDA_private_CIF_\expr Next LAMBDA_private_CIF_\expr\argc = LAMBDA_private_CIF_\expr\argc + 1 ResizeBank LAMBDA_private_CIF_\expr\argv, LAMBDA_private_CIF_\expr\argc * 4 PokeInt LAMBDA_private_CIF_\expr\argv, (LAMBDA_private_CIF_\expr\argc - 1) * 4, Handle t EndIf Return "@" + Handle t End Function ;(Internal) Copy a Thunk (for use with Lambda) Function CopyThunkInner.Thunk(t.Thunk) Local i, C.Thunk = New Thunk C\fptr = t\fptr : C\argc = t\argc : C\isVarName = t\isVarName If t\argv Then C\argv = CreateBank(BankSize(t\argv)) : CopyBank t\argv, 0, C\argv, 0, BankSize(t\argv) If t\fptr For i = 0 To (Abs t\argc - 1) If Asc(t\argn[i]) = 64 C\val[i] = Handle CopyThunkInner(Object.Thunk t\val[i]) : C\argn[i] = "@" + C\val[i] Else C\val[i] = t\val[i] : C\argn[i] = t\argn[i] EndIf Next Else If t\argv ;An InnerFunc For i = 0 To t\argc - 1 PokeInt C\argv, i * 4, Handle CopyThunkInner(Object.Thunk PeekInt(t\argv, i * 4)) Next Else ;A lambda thunk C\val[0] = Handle CopyFuncInner(Object.Closure t\val[0], False) EndIf EndIf Return C End Function ;(Internal) Free a delayed action Function FreeThunk(t.Thunk) Local i If t\fptr For i = 0 To (Abs t\argc) - 1 If Asc(t\argn[i]) = 64 Then FreeThunk Object.Thunk t\val[i] Next FreeBank t\argv Else If t\argv ;An InnerFunc For i = 0 To t\argc - 1 FreeThunk Object.Thunk PeekInt(t\argv, i * 4) Next FreeBank t\argv Else ;A lambda thunk FreeFunc Object.Closure t\val[0] EndIf EndIf Delete t End Function ;(Internal) Evaluate a delayed action Function EvalThunk(t.Thunk, env.Closure) Local i, val If t\fptr ;It's an action thunk If Not t\isVarName For i = 0 To (Abs t\argc) - 1 If Asc(t\argn[i]) = 64 t\isVarName = t\isVarName Or (1 Shl (i + 9)) Else Local idx = GetThunkArgBinding(t\argn[i], env) If idx t\val[i] = idx - 1 : t\isVarName = t\isVarName Or (1 Shl i) Else PokeInt t\argv, i * 4, t\val[i] ;Simple value, poke it here (so we only do it once) EndIf EndIf Next t\isVarName = t\isVarName Or (1 Shl 30) ;Set a very high bit to nonzero EndIf If t\argc < 0 t\env = env : PokeInt t\argv, (Abs t\argc) * 4, Handle t ;Lazy thunks need an eval environment Else Local tmp[9] For i = 0 To t\argc - 1 If t\isVarName And (1 Shl (i + 9)) tmp[i] = EvalThunk(Object.Thunk t\val[i], env) EndIf Next For i = 0 To t\argc - 1 If t\isVarName And (1 Shl i) PokeInt t\argv, i * 4, PeekInt(env\argv, t\val[i]) ElseIf t\isVarName And (1 Shl (i + 9)) PokeInt t\argv, i * 4, tmp[i] EndIf Next EndIf If t\fptr = LAMBDA_APPLY_CONSTPTR ;Apply (change this to check against Apply's actual fptr sometime) Local F = PeekInt(t\argv, 0), a0 = PeekInt(t\argv, 4), a1 = PeekInt(t\argv, 8), a2 = PeekInt(t\argv, 12), a3 = PeekInt(t\argv, 16) Return CallFunc(F, a0, a1, a2, a3, PeekInt(t\argv, 20), PeekInt(t\argv, 24), PeekInt(t\argv, 28), PeekInt(t\argv, 32)) Else CallFunctionVarInt t\fptr, t\argv Return PeekInt(t\argv, (Abs t\argc) * 4) EndIf Else If t\argv ;It's an InnerFunc For i = 0 To t\argc - 1 ;If we're here, t\argc should never be negative val = EvalThunk(Object.Thunk PeekInt(t\argv, i * 4), env) Next Return val Else ;It's a thunk of lambda: return copy of value to CallFunc Local L.Closure = CopyFuncInner(Object.Closure t\val[0], True) If Not L\argv Then BindFuncEnvironment L, env Return Handle L EndIf EndIf End Function ;(Internal) Find the offset of a name in the var table of the environment Function GetThunkArgBinding(arg$, env.Closure) If arg = "" Then Return 0 Local i, count : While env <> Null For i = 0 To 8 If env\argn[i] = arg Then Return count + 1 count = count + 4 Next env = env\env Wend Return 0 End Function ;(Internal) Evaluate a packaged action lazily Function GetLazyThunkArgValue(t.Thunk, a) If t\isVarName And (1 Shl a) Return PeekInt(t\env\argv, t\val[a]) ElseIf t\isVarName And (1 Shl (a + 9)) Return EvalThunk(Object.Thunk t\val[a], t\env) Else Return PeekInt(t\argv, a * 4) EndIf End Function ;(Internal) Remove thunks from the current inner function if they shouldn't be there Function PurgeThunkTreeFromCIF(t.Thunk, cif.Thunk) Local i RemoveThunkFromCIF t, cif If t\fptr For i = 0 To (Abs t\argc) - 1 If Asc(t\argn[i]) = 64 Then PurgeThunkTreeFromCIF Object.Thunk t\val[i], cif Next Else Local f.Closure = Object.Closure t\val[0] PurgeThunkTreeFromCIF f\expr, cif EndIf End Function ;(Internal) Remove one thunk (see above) Function RemoveThunkFromCIF(t.Thunk, cif.Thunk) Local ht = Handle t, i For i = 0 To BankSize(cif\argv) - 4 Step 4 If PeekInt(cif\argv, i) = ht CopyBank cif\argv, i + 4, cif\argv, i, BankSize(cif\argv) - (i + 4) ResizeBank cif\argv, BankSize(cif\argv) - 4 cif\argc = cif\argc - 1 Return EndIf Next End Function ;~IDEal Editor Parameters: ;~F#E#14#20#2E#33#44#49#57#5C#63#6F#7B#86#8C#A9#C2#D7#115#122#12D ;~F#13B ;~C#Blitz3D |
Comments
None.
Code Archives Forum