Lambda functions

Discussions about the BBC BASIC language, with particular reference to BB4W and BBCSDL
Richard Russell
Posts: 457
Joined: Tue 18 Jun 2024, 09:32

Lambda functions

Post by Richard Russell »

Many modern programming languages, including C++, JavaScript and Python, have lambda functions (or lambda expressions). As defined in Python, a lambda function is a small, anonymous function; by small is implied one that will fit into a single line of code, and by anonymous is meant that the function does not have a name. Of course in the context of BBC BASIC a function also encompasses a procedure.

The main occasions when a lambda would be preferred over a conventional DEF FN or DEF PROC are as follows:
  1. The function is used only once (often as an argument to a higher-order function).
  2. Naming the function would be unnecessary clutter (it's clearer to define it inline).
  3. The logic is simple enough that a full DEF would be overkill.
Unlike the above-mentioned programming languages BBC BASIC does not have lambdas as a built-in capability, indeed most BASICs don't (although Visual BASIC.net, if you consider that to be BASIC, does). However, as is often the case, the low-level capabilities of BBC BASIC allow you to create a lambda using a user-defined routine (which could for example be in a library).

Suppose that we want to write a general-purpose procedure which will perform some specified (but not predetermined) operation on each element of a numeric array. We might do that as follows, where the parameter f%% is a function pointer:

Code: Select all

      DEF PROCapplyToNumbers(f%%, a()) LOCAL I%
      FOR I% = 0 TO DIM(a(),1) : a(I%) = FN(f%%)(a(I%)) : NEXT
      ENDPROC
This could be called in a context such as this, when we want each element of the array to be doubled:

Code: Select all

      PROCapplyToNumbers(^FNdouble(), array_of_numbers())

      DEF FNdouble(x) = x * 2
But if this is the only place in the code where FNdouble() is used, defining a global function is wasteful and doesn't adhere to the paradigm of 'encapsulation' (or 'information hiding' if you prefer) in which the visibility of something is restricted to the scope in which it is used. So instead we
could use a lambda as follows:

Code: Select all

      PROCapplyToNumbers(FN_lambda("(x) = x * 2"), array_of_numbers())
Now the doubling function is neatly contained 'inline' within the function call. Admittedly if PROCapplyToNumbers() is called multiple times this wont be the fastest approach, as the lambda is having to be evaluated every time. So in that case you could assign it to a variable:

Code: Select all

      double = FN_lambda("(x) = x * 2")
      PROCapplyToNumbers(double, array_of_numbers())
This can still preserve the local nature of the function by declaring double as LOCAL in an outer procedure.

The 'magic' that makes all this happen is in this function, which could usefully be hived off into a library. If you want a challenge, figure out how it works!

Code: Select all

      DEF FN_lambda(f$) : LOCAL I%, a%%, v%%, v$
      FOR I% = 1 TO LENf$ : v$ += CHR$(ASCMID$(f$,I%) MOD 28 + &5F) : NEXT
      v%% = EVAL("^" + v$ + "@%%") : IF ]v%% = FALSE DIM ]v%% LENf$ + 8
      IF INKEY(-256)<>&57 IF @platform% AND &40 a%% = ]332 ELSE a%% = !332
      ]]v%% = ]v%% + 8 : IF EVAL("1RECTANGLE:" + f$) $]]v%% = $(a%% + 3)
      = ]v%%
Richard Russell
Posts: 457
Joined: Tue 18 Jun 2024, 09:32

Re: Lambda functions

Post by Richard Russell »

Richard Russell wrote: Thu 17 Jul 2025, 23:13 If you want a challenge, figure out how it works!
Spoiler alert: if you're still trying to work it out, stop reading now! For anybody who wants an explanation, here is it. Hopefully it will demonstrate that even code which initially looks intimidating is straightforward when broken down:

Code: Select all

      DEF FN_lambda(f$) : LOCAL I%, a%%, v%%, v$
The usual function preamble, declaring its name, parameter(s) and LOCAL(s).

Code: Select all

      FOR I% = 1 TO LENf$ : v$ += CHR$(ASCMID$(f$,I%) MOD 28 + &5F) : NEXT
Generate a 'unique' variable name v$ based on the supplied function string. Of course it isn't actually guaranteed to be unique, because of the MOD 28, but statistically it is highly unlikely that two different functions f$ would give rise to the same variable name. It would be possible, at the cost of increased complexity and execution time, to improve the odds but the existing code is a good compromise.

If you can think of a better and/or faster way of generating a 'unique' variable name from a function string I would be very interested. The 'traditional' way would be a hash function, but that's overcomplicated for this application.

Code: Select all

      v%% = EVAL("^" + v$ + "@%%") : IF ]v%% = FALSE DIM ]v%% LENf$ + 8
Check if a 64-bit integer variable with the name we generated already exists. If it doesn't, allocate a block of memory big enough to contain the lambda and set the variable to point to this memory block.

Code: Select all

      IF INKEY(-256)<>&57 IF @platform% AND &40 a%% = ]332 ELSE a%% = !332
Get a pointer to the string accumulator, depending on whether this is a 32-bit (including BBC BASIC for Windows) or 64-bit edition of BBC BASIC. The string accumulator is where the tokeniser stores its output.

Code: Select all

      ]]v%% = ]v%% + 8 : IF EVAL("1RECTANGLE:" + f$) $]]v%% = $(a%% + 3)
Create the lambda in memory by tokenising the supplied function string f$ and storing the address of this tokenised code.

Code: Select all

      = ]v%%
Return a pointer to the lambda.
User avatar
JeremyNicoll
Posts: 75
Joined: Sun 26 Jul 2020, 22:22
Location: Edinburgh

Re: Lambda functions

Post by JeremyNicoll »

Richard Russell wrote: Sat 19 Jul 2025, 10:16

Code: Select all

      ]]v%% = ]v%% + 8 : IF EVAL("1RECTANGLE:" + f$) $]]v%% = $(a%% + 3)
Create the lambda in memory by tokenising the supplied function string f$ and storing the address of this tokenised code.
Why is the result of EVAL() boolean?

I was going to ask: why include "1RECTANGLE:", but I think I found a hint at: https://www.bbcbasic.net/wiki/doku.php?id=tokeniser
where the text suggests this form of calling EVAL() can help. Is the reason it helps just that LEN("1RECTANGLE:") is 11 bytes, which will be tokenised to
however many bytes a line number is encoded as, 2?, one byte for "RECTANGLE"?, and one for the separator? So that gives you 7? bytes extra space for the tokenised output string?

Is the "+ 3" in "a%% + 3" so that what gets stored doesn't include tokens for "1" or "1RECTANGLE"?

Why does executing "1RECTANGLE:" + f$ (assuming that EVAL() will try to evaluate it after tokenising it) not cause some sort of error because RECTANGLE doesn't have any of its expected parameters, according to: https://www.bbcbasic.co.uk/bbcwin/manua ... #rectangle Does RECTANGLE with no parms do nothing at all (ie wouldn't affect graphics output)?

If f$ is something like "(x) = x * 2" how does EVAL() evaluate it after tokenisation & avoid a runtime error? It's not as if (at this point) you're indirectly calling the code as a function. Perhaps this depends on how BASIC processes a statement that starts with "("? I could see that it might treat it as a boolean condition: ie (is) x equal to x * 2? That would be TRUE if x was undeclared so initialised as 0. But if x was a global variable with any non-zero value, wouldn't the result be FALSE (or a runtime error if it'd been too big)?


Perhaps the text at: https://www.bbcbasic.co.uk/bbcwin/manua ... .html#eval could be updated to show that EVAL() does tokenisation as the first stage of evaluation, and the tokenised string can be captured?
Richard Russell
Posts: 457
Joined: Tue 18 Jun 2024, 09:32

Re: Lambda functions

Post by Richard Russell »

JeremyNicoll wrote: Sat 19 Jul 2025, 20:17 Why is the result of EVAL() boolean?
BBC BASIC doesn't have Booleans (integer numerics have to suffice instead) so it isn't! Since the string being evaluated is "1RECTANGLE" the result is in fact 1; any non-zero integer would work equally well.
I was going to ask: why include "1RECTANGLE:", but I think I found a hint at: https://www.bbcbasic.net/wiki/doku.php?id=tokeniser
Yes, I would have referred you to that page if you hadn't found it yourself. It's not actually necessary in this case, since a Lambda isn't going to include a line number (at least, I hope not) so the reason given there doesn't apply, but I just copied-and-pasted the code.
Is the "+ 3" in "a%% + 3" so that what gets stored doesn't include tokens for "1" or "1RECTANGLE"?
Yes. Another valuable reference for the use of the tokeniser can be found at Jonathan's own site.
Why does executing "1RECTANGLE:" + f$ (assuming that EVAL() will try to evaluate it after tokenising it) not cause some sort of error
A numeric constant immediately followed by RECTANGLE is entirely legitimate in BBC BASIC, for example in this context:

Code: Select all

      IF 1234 RECTANGLE x,y,w,h
So there's no reason to expect it to throw an error. In BASICs which mandate a THEN keyword after a conditional test this wouldn't necessarily be the case, but BBC BASIC (along with many others) is happy for the THEN to be omitted in most cases, making 1 RECTANGLE entirely valid to the parser.
because RECTANGLE doesn't have any of its expected parameters
I think you're forgetting that BBC BASIC is an interpreter, not a compiler. The parser/tokeniser doesn't know anything about the syntax of RECTANGLE!
Perhaps the text at: https://www.bbcbasic.co.uk/bbcwin/manua ... .html#eval could be updated to show that EVAL() does tokenisation
Absolutely not. It's a non-contractual side-effect; such things are virtually never documented. Although the tokenisation trick does work in most versions of BBC BASIC, as documented at the BeebWiki, it sadly doesn't work in Brandy,
Richard Russell
Posts: 457
Joined: Tue 18 Jun 2024, 09:32

Re: Lambda functions

Post by Richard Russell »

Richard Russell wrote: Sat 19 Jul 2025, 21:28 Another valuable reference for the use of the tokeniser can be found at Jonathan's own site.
At the bottom of that page there's a link to the original discussion (from 2006), but of course Yahoo! Groups is long gone. Fortunately all those old messages remain accessible, and you can find the relevant thread here.