BPL Function Trains and Operators
!pip install -U basedplRequirement already satisfied: basedpl in /app/data/.local/lib/python3.12/site-packages (0.1.82)
Requirement already satisfied: fastcore in /usr/local/lib/python3.12/site-packages (from basedpl) (2.1.16)
Requirement already satisfied: jupyter-core in /usr/local/lib/python3.12/site-packages (from basedpl) (5.9.1)
Requirement already satisfied: platformdirs>=2.5 in /usr/local/lib/python3.12/site-packages (from jupyter-core->basedpl) (4.11.0)
Requirement already satisfied: traitlets>=5.3 in /usr/local/lib/python3.12/site-packages (from jupyter-core->basedpl) (5.16.0)
%load_ext basedpl.notebooks
#%reload_ext basedpl.notebooksThe basedpl.notebooks extension is already loaded. To reload it, use:
%reload_ext basedpl.notebooks
import basedpl
basedpl.__version__'0.1.82'Writing functions without ⍵
One way to write your own function is with braces, like {(+/⍵)÷≢⍵}, where ⍵ stands for the data passed into the function. In this dialog we build new functions by combining existing functions, without ever mentioning the data. This style is called tacit (meaning "unspoken": the argument is never named). It is shorter, it reads like a sentence once you're used to it, and some patterns run faster.
Glyphs and ideas this dialog teaches:
| Glyph | Plain English |
|---|---|
↣ | fix the left side of a function |
↢ | fix the right side of a function |
2× | shorthand for 2↣×: "times 2" |
⊢ ⊣ | right / left: give back one side, ignore the other |
f g h | a fork: (f x) g (h x) |
f g | an atop: f applied to the result of g |
∘ | atop, written as an operator |
⁝ | tricolon (type Alt-:, new in BPL 0.1.76): keeps two names apart, so tally⁝split is two functions, not one name. Does no maths. |
⍨ | use one argument on both sides, or swap the two sides |
@ | under: change only some items, or do / work / undo |
⁻¹ | run a function backwards (its inverse) |
f↢g f↣g | a hook: x f (g x) and (f x) g x, using x twice |
⍤ | rank: apply a function to each row (⍤1), or other pieces |
Other glyphs used along the way:
| Glyph | Plain English |
|---|---|
← | store a value or function in a name |
⍝ | comment: BPL ignores the rest of the line |
{ } and ⍵ | a function written with braces; ⍵ is its right argument |
"…" and '…' | text, and a single character |
+ - × ÷ | add, subtract, multiply, divide |
+/ ⌈/ ⌊/ | total, biggest, smallest of a list |
+\ | running total |
≢ | count the items |
⌊ | round down to a whole number |
< > ≠ | compare: less than, greater than, not equal (answers $t true or $f false) |
∊ | is each item in the list? |
# | keep the items where the answer is $t |
⊆ | cut a list into pieces |
⌽ | reverse |
≡ | match: $t if two things are exactly the same |
[… ⋄ …] | a table written row by row; ⋄ starts the next row |
[ ] | a list of positions, e.g. @[5 6] |
↦ | ↦5 gives 1 to 5 |
ₓ | (in output) marks an exact number |
The data
We use a week of midday temperatures (°C, Monday to Sunday) and a short sentence.
%%bpl
temps←12 15 11 18 20 14 9 ⍝ Mon to Sun
s←"the cat sat on the mat"
s
temps"the cat sat on the mat"
12 15 11 18 20 14 9
1. Fix the left side: ↣ and the shorthand 2×
The right-tail arrow ↣ fixes a function's left argument. 2↣× means "put 2 on the left of ×, then wait for the right argument". In other words, it makes the function "times 2".
When the fixed left argument is a value, BPL lets us leave out ↣. Therefore 2× is the shorter way to write the same function. You can store either form under a name, just like a value.
%%bpl
double←2↣× ⍝ explicit: fix 2 as the left argument of ×
double 1 2 3
double←2× ⍝ shorthand for the same function
double 1 2 32 4 6
2 4 6
A longer example: Celsius to Fahrenheit is "times 1.8, then add 32". BPL reads right to left, so 32+1.8× means "times 1.8, then 32 plus that".
%%bpl
toF←32+1.8× ⍝ Celsius → Fahrenheit
toF 100 ⍝ boiling point
toF 0 37 ⍝ freezing point, body temperature212
32 98.6
2. Fix the right side: ↢
The two arrows form a pair. 2↣÷ fixes 2 as the left argument, making "2 divided by". ÷↢2 fixes 2 as the right argument, making "divide by 2", which is "halve".
Why not just write ÷2? Because a single ÷ with only a right side already means something else (reciprocal - one over the number), so ÷2 is just the value 0.5, not a function. ↢ makes it clear that the 2 belongs on the right.
%%bpl
half←÷↢2 ⍝ divide by 2
half 10 20
isvowel←∊↢"aeiou" ⍝ "is it in aeiou?" (∊: is each item in the list?)
isvowel "banana"5 10
$f $t $f $t $f $t
3. Right and left: ⊢ ⊣
Two very simple functions that become useful in a moment:
⊢(right) gives back what's on its right and ignores the left.⊣(left) gives back what's on its left and ignores the right.
With nothing on the left, both just give back their argument.
%%bpl
5⊢1 2 3 ⍝ right: 1 2 3
5⊣1 2 3 ⍝ left: 5
⊣1 2 3 ⍝ nothing on the left: 1 2 3
⊢1 2 3 ⍝ nothing on the left: 1 2 31 2 3
5
1 2 3
1 2 3
4. Three functions in a row: a fork
When three functions sit side by side, f g h, BPL reads (f g h) x as (f x) g (h x):
- apply the left function to x,
- apply the right function to x,
- combine the two results with the middle function.
A classic example: mean←+/÷≢ is (+/x) ÷ (≢x), the total divided by the count.
Another example: the range of the temperatures is the hottest minus the coldest. ⌈/ is the biggest and ⌊/ the smallest, so the fork ⌈/-⌊/ means (⌈/x) - (⌊/x).
%%bpl
mean←+/÷≢ ⍝ total ÷ count
mean temps
spread←⌈/-⌊/ ⍝ hottest − coldest
spread temps14.14285714
11
The temperatures average about 14.1 and spread over 11 degrees (from 9 to 20).
5. Two functions in a row: an atop
When two functions sit side by side, f g, BPL applies g first and then f to its result: (f g) x is f (g x).
⌊meanmeans "round down the mean". (⌊on its own rounds down to a whole number.)+/isvowelmeans "add up the isvowel answers", which counts the vowels.
%%bpl
roundmean←⌊mean ⍝ mean, then round down
roundmean temps
vowels←+/isvowel ⍝ isvowel, then add up the trues
vowels "banana"14ₓ
3ₓ
6. Fixed functions inside a train
The functions from steps 1 and 2 can go inside a fork. Here are two everyday examples.
Keep the warm days. warm←14< asks "is 14 less than it?", which is "is it above 14?". Then the fork warm#⊢ means (warm x) # (⊢ x): use the yes/no answers to filter x itself (# keeps the items where the answer is yes).
%%bpl
warm←14< ⍝ is it above 14?
warm temps
hot←warm#⊢ ⍝ (warm x) # x
hot temps$f $t $f $t $t $f $f
15 18 20
Although hot receives only one argument, the fork sends that same argument down both branches:
hot←warm#⊢
This is equivalent to the explicit function:
hot←{(warm ⍵)#⍵}
Because a tacit function does not name ⍵, ⊢ supplies the role of the second ⍵: it receives temps and returns it unchanged while warm turns the other copy into a Boolean mask. Thinking of ⊢ as “holding onto the original input” is useful, although it does not actually store or delay the value.
Split into words. notspace←' '≠ asks "is it not a space?". Then the fork notspace⊆⊢ means (notspace x) ⊆ x: split x where it's not a space (⊆ cuts a list into pieces, keeping each run marked yes).
%%bpl
notspace←' '≠ ⍝ is it not a space?
notspace "hel lo"
split←notspace⊆⊢ ⍝ (notspace x) ⊆ x
split s
split "this is not a drill"$t $t $t $f $t $t
┌→───────────────────────────────────┐ │ "the" "cat" "sat" "on" "the" "mat" │ └∊───────────────────────────────────┘
┌→──────────────────────────────┐ │ "this" "is" "not" "a" "drill" │ └∊──────────────────────────────┘
7. Atop as an operator: ∘
f∘g means the same as the atop f g from step 5: do g, then f to its result. Writing it with ∘ makes the joining explicit, and it also works with a value on each side: x f∘g y is f (x g y).
For example, ⌊∘÷ means "divide, then round down", which is whole-number division.
%%bpl
roundmean←⌊∘mean ⍝ the same as ⌊mean in step 5
roundmean temps
intdiv←⌊∘÷ ⍝ divide, then round down
7 intdiv 2 ⍝ 7÷2 is 3.5, rounded down is 314ₓ
3ₓ
8. Two named functions side by side: ⁝
- Many functions in this dialog have names:
mean,warm,split. - Say we give
≢(count) the nametally, and want the atop "split, thentally" (as in step 5). - Writing
tallysplitfails: two names touching are read as one name, which doesn't exist. ⁝(the tricolon, typed Alt-:, new in BPL 0.1.76) goes between them. It keeps the two names apart and does no maths, sotally⁝splitis the atoptally split.tally∘splitfrom step 7 does the same job.
%%bpl
tally←≢ ⍝ a name for "count the items"
count←tally⁝split ⍝ atop: split, then tally
count s ⍝ 6 words
count←tally split ⍝ atop: split, then tally
count s ⍝ 6 words6ₓ
6ₓ
%%bpl
s
(tally∘split s)≡count s ⍝ $t: same as using ∘"the cat sat on the mat"
$t
9. Hooks: a function on each side of ↢ or ↣
In steps 1 and 2, ↣ and ↢ had a value on one side. Put a function on both sides and you get a hook. A hook uses the argument x twice: once as it is, and once changed by one of the functions.
f↢g xmeansx f (g x): dogtox, thenfwithxitself on the left.f↣g xmeans(f x) g x: doftox, thengwithxitself on the right.
So ×↢- 5 is 5 × (-5), which is ¯25, and -↣+ 5 is (-5) + 5, which is 0. ×↢⊢ is x × x: another way to square (⊢ gives back x, from step 3).
%%bpl
×↢- 5 ⍝ 5 × (-5): ¯25
-↣+ 5 ⍝ (-5) + 5: 0
×↢⊢ 5 ⍝ 5 × 5: 25
⍝ As a function
sqH←×↢⊢ ⍝ x × x
sqH 1 2 3 4 ⍝ 1 4 9 16¯25
0
25
1 4 9 16
10. Rebuild hot and split as hooks
- Operators join from the left, so
14↣<↣#means(14↣<)↣#. 14↣<is "is 14 less than it?" (step 1). Then↣#uses those yes/no answers on the left of#, withxitself on the right. So it keeps the warm days, just likehotfrom step 6.' '↣≠↣⊆works the same way: mark the non-spaces, then cutxwith them. That'ssplitfrom step 6.- The new names end in
H(for hook). They can't end in a digit, because a digit ends a name in BPL:hot2would meanhotapplied to 2.
%%bpl
temps
14↣< temps
14↣<↣# temps
hotH←14↣<↣# ⍝ (14<x) # x
hotH temps ⍝ 15 18 20
(hotH temps)≡hot temps ⍝ $t: same as hot
s
' '↣≠ s
splitH←' '↣≠↣⊆ ⍝ (' '≠x) ⊆ x
splitH s
(splitH s)≡split s ⍝ $t: same as split12 15 11 18 20 14 9
$f $t $f $t $t $f $f
15 18 20
15 18 20
$t
"the cat sat on the mat"
$t $t $t $f $t $t $t $f $t $t $t $f $t $t $f $t $t $t $f $t $t $t
┌→───────────────────────────────────┐ │ "the" "cat" "sat" "on" "the" "mat" │ └∊───────────────────────────────────┘
$t
Fixing versus hooking. In 14↣<, ↣ fixes the value 14 as the left argument of <, producing the function “is greater than 14?”. In 14↣<↣#, operators join from the left, so it parses as (14↣<)↣#. The second ↣ has a function on each side and therefore forms a hook: it combines the fixed function 14↣< with #; it does not fix one function as an argument of the other.
When hooks are useful in practice
A hook is useful when a function needs the input twice: one copy unchanged and one copy transformed.
f↢g ⍝ x f (g x)
f↣g ⍝ (f x) g x
A useful question is:
Do I need to calculate something from the data, then combine that result with the original data?
If so, a hook may be a natural fit.
Scale values relative to their maximum, with a fork
%%bpl
relative←⊢÷⌈/
relative 2 4 80.25 0.5 1
This fork is (⊢x) ÷ (⌈/x): the left branch returns x, while the right branch finds its maximum. It gives 0.25 0.5 1. This kind of normalization is useful when comparing measurements on different scales.
Subtract the mean from every value
%%bpl
temps
center←-↢mean
onedp←(⌊ 0.5↣+)@(10×)
onedp center temps12 15 11 18 20 14 9
¯2.1 0.9 ¯3.1 3.9 5.9 ¯0.1 ¯5.1
This is x - mean x. It shows how far each value is above or below the average—a common operation in statistics and data analysis.
Test whether something is a palindrome
%%bpl
palindrome←≡↢⌽
palindrome "level" ⍝ $t
palindrome "train" ⍝ $f
⍝ Ordering doesn't matter as long as the hook 'direction' is correct
ppalindrome←⌽↣≡
ppalindrome "level" ⍝ $t
ppalindrome "train" ⍝ $f$t
$f
$t
$f
This is x ≡ (⌽x): compare the original value with its reversal.
So hooks are particularly valuable for comparing, testing, or combining data with a result calculated from that same data. When the calculation naturally has two branches and a combining function—as in relative—a fork may be clearer.
11. One argument on both sides, or swap: ⍨
⍨ (self / commute) does two jobs:
- With one argument,
f⍨ xmeansx f x. So×⍨multiplies a number by itself: it squares. - With two arguments,
a f⍨ bswaps them:b f a. So2-⍨10is10-2.
%%bpl
sq←×⍨ ⍝ x × x
sq 1 2 3 4
2-⍨10 ⍝ swapped: 10 − 21 4 9 16
8
12. Change only some items: @ with positions
f@[positions] x applies f to just the items at those positions, and leaves every other item as it was. The positions go in square brackets and count from 0.
If you put a value where f goes, those items are simply replaced by that value. Here we replace the weekend (positions 5 and 6) with 0.
In 10+@[5 6] temps, 10+ is not another value placed to the left of @: it is shorthand for the function 10↣+, meaning “add 10”. The @ therefore applies that function only at positions 5 and 6.
%%bpl
temps
0@[5 6] temps ⍝ Sat and Sun replaced by 0
10+@[5 6] temps ⍝ add 10 to Sat and Sun only12 15 11 18 20 14 9
12 15 11 18 20 0 0
12 15 11 18 20 24 19
Note that temps itself doesn't change: @ gives back a new list.
13. Change only items that pass a test: @ with a function
Instead of positions, you can put a yes/no function in the brackets. @ then changes only the items where it says yes.
cold←15> asks "is 15 greater than it?", which is "is it below 15?".
%%bpl
temps
cold←15> ⍝ is it below 15?
cold temps
0@[cold] temps ⍝ cold days replaced by 012 15 11 18 20 14 9
$t $f $t $f $f $t $t
0 15 0 18 20 0 0
14. Do, work, undo: @ with a function on the right
@ has one more use, called under. f@g x means:
- do
gto x, - do
fto the result, - undo
gagain.
Example: round down to one decimal place. ⌊ only rounds to whole numbers, so: multiply by 10, round down, divide by 10. Here are the three steps by hand first.
%%bpl
x←1.25 2.78
x
10×x ⍝ 1. times 10
⌊10×x ⍝ 2. round down
(⌊10×x)÷10 ⍝ 3. undo the times 101.25 2.78
12.5 27.8
[12 27]ₓ
1.2 2.7
⌊@(10×) does all three steps. BPL works out by itself that the way to undo "times 10" is "divide by 10".
%%bpl
x
tenths←⌊@(10×) ⍝ round down, under "times 10"
tenths x1.25 2.78
1.2 2.7
15. Run a function backwards: ⁻¹
Many functions can be undone. Writing ⁻¹ (a small raised "minus one") straight after a function gives its inverse: the function that undoes it.
+\makes a running total, so+\⁻¹turns a running total back into the original list.toF(step 1) turns Celsius into Fahrenheit, sotoF⁻¹turns Fahrenheit back into Celsius. BPL works out the reverse steps (subtract 32, divide by 1.8) by itself.
%%bpl
r←+\1 2 3 ⍝ running total: 1 3 6
r
+\⁻¹ r ⍝ back to 1 2 3
toF 0 100 ⍝ Celsius → Fahrenheit
toF⁻¹ 32 212 ⍝ Fahrenheit → Celsius1 3 6
1 2 3
32 212
0 100
16. Work on each row: ⍤ (rank)
[… ⋄ …]writes a table row by row:⋄starts the next row. Herewholds two weeks of temperatures, one week per row.mean wgoes wrong.+/totals each row, but≢counts the rows (2), so it divides each week's total by 2.⍤(rank, typed Alt-1 t) says which pieces a function works on.mean⍤1 wappliesmeanto each piece of rank 1. A rank-1 array is a list, so each piece is one row.
%%bpl
w←[12 15 11 18 20 14 9⋄13 16 10 17 21 15 8] ⍝ two weeks, one per row
w
mean w ⍝ wrong: each week's total ÷ 2
mean⍤1 w ⍝ right: the mean of each week┌→──────────────────┐ ↓12 15 11 18 20 14 9│ │13 16 10 17 21 15 8│ └~──────────────────┘
49.5 50
14.14285714 14.28571429
Week 1 averages about 14.1 °C and week 2 about 14.3 °C.
17. Pair each row with a vector: ⍤ with two arguments
m has shape 2 3: axis 0 has 2 rows, and axis 1 has 3 columns. v has shape 3: it has only axis 0, of length 3.
Plain + is pervasive; it does not default to the last axis. BPL agreement aligns leading axes, so m+v tries to match axis 0 of m (length 2) with axis 0 of v (length 3). They do not agree, so the result is a LENGTH error.
%%bpl
m←[1 2 3⋄4 5 6] ⍝ shape 2 3: axis lengths 2 and 3
v←100 200 300 ⍝ shape 3: one axis of length 3
m
v┌→────┐ ↓1 2 3│ │4 5 6│ └~────┘
100 200 300
m+v ⍝ LENGTH error: leading-axis lengths 2 and 3 do not agree
%%bpl
m+⍤1 v ⍝ add v to each rank-1 row of m┌→──────────┐ ↓101 202 303│ │104 205 306│ └~──────────┘
+⍤1 changes the cells given to +. The rank-1 cells of m are its two rows, each of shape 3; the rank-1 cell of v is the whole vector, also shape 3. The single v cell is therefore paired with each row of m.
The rank also determines how many times the operand function is called. Here +⍤1 calls + twice—once for each rank-1 cell (row) of m:
1 2 3 + 100 200 300
4 5 6 + 100 200 300
At rank 0, the cells of m are its six individual numbers. A scalar right argument has an empty frame, so it can be reused for every cell: m+⍤0 10 calls scalar + six times. By contrast, m+⍤0 v would still give a LENGTH error: its frames would be 2 3 and 3, whose leading axes do not agree.
%%bpl
m
m+⍤0:10 ⍝ six scalar calls: add 10 to each element
m+(⍤0)10 ⍝ we can also uses parens for explicit parsing┌→────┐ ↓1 2 3│ │4 5 6│ └~────┘
┌→───────┐ ↓11 12 13│ │14 15 16│ └~───────┘
┌→───────┐ ↓11 12 13│ │14 15 16│ └~───────┘
Note: The above BPL expression did not work as m+⍤0 10 so I used the new tricolon : glyph rather than explicit parentheses.
18. Braces vs trains, side by side
Here are two small functions written both ways: first with braces, then as a train. ≡ (match) checks that the braces version and the train version give exactly the same answer.
Palindrome: "the word matches its reverse". The fork ⊢≡⌽ means (⊢ x) ≡ (⌽ x): x matches reversed x.
%%bpl
palB←{⍵≡⌽⍵} ⍝ braces version
pal←⊢≡⌽ ⍝ train version: x ≡ ⌽x
pal "racecar"
(palB "level")≡pal "level"$t
$t
Your turn
- Make
triple←3×and use it on1 2 3. - Square the numbers 1 to 5 with
sq. (↦5gives 1 to 5.) - Convert 50 °F to Celsius with
toF⁻¹. - Use
@to replace Monday's temperature (position 0) with 99. - Give
⌽(reverse) the namerev, then use⁝to make the atoprevwords←rev⁝split. Try it ons: it should list the words in reverse order. - Make
cubeas a hook: x × (x × x), and try it on 3. Hint:×↢sq: dosq, then multiply byxitself. - Count the days in each week of
wwith≢⍤1, and compare with plain≢w. Hint: plain≢counts the rows.