about summary refs log tree commit diff stats
path: root/bash/talk-to-computer/corpus/programming/lil_guide.md
blob: 72df8df97230074dd8f736e1ede3eab8143a3acc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# Multi-paradigm Programming with Lil - A Guide to Lil's Diverse Styles

## Introduction

Lil is a richly multi-paradigm scripting language designed for the Decker creative environment. It seamlessly blends concepts from **imperative**, **functional**, **declarative**, and **vector-oriented** programming languages. This flexibility allows developers to choose the most effective and ergonomic approach for a given task, whether it's managing application state, manipulating complex data structures, or performing efficient bulk computations. Understanding these paradigms is key to writing elegant, efficient, and idiomatic Lil code.

## Core Concepts

Lil's power comes from the way it integrates four distinct programming styles.

### Imperative Programming

This is the traditional, statement-by-statement style of programming. It involves creating variables, assigning values to them, and using loops and conditionals to control the flow of execution.

  - **Assignment:** The colon (`:`) is used for assignment.
  - **Control Flow:** Lil provides `if`/`elseif`/`else` for conditionals and `while` and `each` for loops.
  - **State Management:** State is typically managed by assigning and re-assigning values to variables, often stored in the properties of Decker widgets between event handlers.

<!-- end list -->

```lil
# Imperative approach to summing a list
total: 0
numbers: [10, 20, 30]
each n in numbers do
  total: total + n
end
# total is now 60
```

### Functional Programming

The functional style emphasizes pure functions, immutability, and the composition of functions without side-effects.

  - **Immutability:** All core data structures (lists, dictionaries, tables) have copy-on-write semantics. Modifying one does not alter the original value but instead returns a new, amended value.
  - **First-Class Functions:** Functions are values that can be defined with `on`, assigned to variables, and passed as arguments to other functions.
  - **Expressions over Statements:** Every statement in Lil is an expression that returns a value. An `if` block returns the value of its executed branch, and an `each` loop returns a new collection containing the result of each iteration.

<!-- end list -->

```lil
# Functional approach using a higher-order function
on twice f x do
  f[f[x]]
end

on double x do
  x * 2
end

result: twice[double 10] # result is 40
```

### Declarative (Query-based) Programming

For data manipulation, Lil provides a powerful declarative query engine that resembles SQL. Instead of describing *how* to loop through and filter data, you declare *what* data you want.

  - **Queries:** Use `select`, `update`, and `extract` to query tables (and other collection types).
  - **Clauses:** Filter, group, and sort data with `where`, `by`, and `orderby` clauses.
  - **Readability:** Queries often result in more concise and readable code for data transformation tasks compared to imperative loops.

<!-- end list -->

```lil
# Declarative query to find developers
people: insert name age job with
 "Alice"  25 "Development"
 "Sam"    28 "Sales"
 "Thomas" 40 "Development"
end

devs: select name from people where job="Development"
# devs is now a table with the names "Alice" and "Thomas"
```

### Vector-Oriented Programming

Influenced by languages like APL and K, this paradigm focuses on applying operations to entire arrays or lists (vectors) at once, a concept known as **conforming**.

  - **Conforming Operators:** Standard arithmetic operators (`+`, `-`, `*`, `/`) work element-wise on lists.
  - **Efficiency:** Vector operations are significantly more performant than writing equivalent imperative loops.
  - **The `@` Operator:** The "apply" operator (`@`) can be used to apply a function to each element of a list or to select multiple elements from a list by index.

<!-- end list -->

```lil
# Vector-oriented approach to add 10 to each number
numbers: [10, 20, 30]
result: numbers + 10 # result is [20, 30, 40]
```

-----

## Key Principles

  - **Right-to-Left Evaluation:** Expressions are evaluated from right to left unless overridden by parentheses `()`. This is a fundamental rule that affects how all expressions are composed.
  - **Copy-on-Write Immutability:** Lists, Dictionaries, and Tables are immutable. Operations like `update` or indexed assignments on an expression `(foo)[1]:44` return a new value, leaving the original unchanged. Direct assignment `foo[1]:44` is required to modify the variable `foo` itself.
  - **Data-Centric Design:** The language provides powerful, built-in tools for data manipulation, especially through its query engine and vector operations.
  - **Lexical Scoping:** Variables are resolved based on their location in the code's structure. Functions "close over" variables from their containing scope, enabling patterns like counters and encapsulated state.

-----

## Implementation/Usage

The true power of Lil emerges when you mix these paradigms to solve problems cleanly and efficiently.

### Basic Example

Here, we combine an imperative loop with a vector-oriented operation to process a list of lists.

```lil
# Calculate the magnitude of several 2D vectors
vectors: [[3,4], [5,12], [8,15]]
magnitudes: []

# Imperative loop over the list of vectors
each v in vectors do
  # mag is a vector-oriented unary operator
  magnitudes: magnitudes & [mag v]
end

# magnitudes is now [5, 13, 17]
```

### Advanced Example

This example defines a functional-style utility function (`avg`) and uses it within a declarative query to summarize data, an approach common in data analysis.

```lil
# Functional helper function
on avg x do
  (sum x) / count x
end

# A table of sales data
sales: insert product category price with
 "Apple"  "Fruit"  0.5
 "Banana" "Fruit"  0.4
 "Bread"  "Grain"  2.5
 "Rice"   "Grain"  3.0
end

# Declarative query that uses the functional helper
avgPriceByCategory: select category:first category avg_price:avg[price] by category from sales

# avgPriceByCategory is now:
# +----------+-----------+
# | category | avg_price |
# +----------+-----------+
# | "Fruit"  | 0.45      |
# | "Grain"  | 2.75      |
# +----------+-----------+
```

-----

## Common Patterns

### Pattern 1: Query over Loop

Instead of manually iterating with `each` to filter or transform a collection, use a declarative `select` or `extract` query. This is more concise, often faster, and less error-prone.

```lil
# Instead of this imperative loop...
high_scores: []
scores: [88, 95, 72, 100, 91]
each s in scores do
  if s > 90 then
    high_scores: high_scores & [s]
  end
end

# ...use a declarative query.
high_scores: extract value where value > 90 from scores
# high_scores is now [95, 100, 91]
```

### Pattern 2: Function Application with `@`

For simple element-wise transformations on a list, using the `@` operator with a function is cleaner than writing an `each` loop.

```lil
# Instead of this...
names: ["alice", "bob", "charlie"]
capitalized: []
on capitalize s do first s & (1 drop s) end # Simple capitalize, for demo
each n in names do
  capitalized: capitalized & [capitalize n]
end

# ...use the more functional and concise @ operator.
on capitalize s do first s & (1 drop s) end
capitalized: capitalize @ names
# capitalized is now ["Alice", "Bob", "Charlie"]
```

-----

## Best Practices

  - **Embrace Queries:** For any non-trivial data filtering, grouping, or transformation, reach for the query engine first.
  - **Use Vector Operations:** When performing arithmetic or logical operations on lists, use conforming operators (`+`, `<`, `=`) instead of loops for better performance and clarity.
  - **Distinguish Equality:** Use the conforming equals `=` within query expressions. Use the non-conforming match `~` in `if` or `while` conditions to avoid accidentally getting a list result.
  - **Encapsulate with Functions:** Use functions to create reusable components and manage scope, especially for complex logic within Decker event handlers.

-----

## Common Pitfalls

  - **Right-to-Left Confusion:** Forgetting that `3*2+5` evaluates to `21`, not `11`. Use parentheses `(3*2)+5` to enforce the desired order of operations.
  - **Expecting Mutation:** Believing that `update ... from my_table` changes `my_table`. It returns a *new* table. You must reassign it: `my_table: update ... from my_table`.
  - **Comma as Argument Separator:** Writing `myfunc[arg1, arg2]`. This creates a list of two items and passes it as a single argument. The correct syntax is `myfunc[arg1 arg2]`.
  - **Using `=` in `if`:** Writing `if some_list = some_value` can produce a list of `0`s and `1`s. An empty list `()` is falsey, but a list like `[0,0]` is truthy. Use `~` for a single boolean result in control flow.

-----

## Performance Considerations

Vector-oriented algorithms are significantly faster and more memory-efficient than their imperative, element-by-element counterparts. The Lil interpreter is optimized for these bulk operations. For example, replacing values in a list using a calculated mask is preferable to an `each` loop with a conditional inside.

```lil
# Slow, iterative approach
x: [1, 10, 2, 20, 3, 30]
result: each v in x
  if v < 5 99 else v end
end

# Fast, vector-oriented approach
mask: x < 5                 # results in [1,0,1,0,1,0]
result: (99 * mask) + (x * !mask)
```

-----

## Integration Points

The primary integration point for Lil is **Decker**. Lil scripts are attached to Decker widgets, cards, and the deck itself to respond to user events (`on click`, `on keydown`, etc.). All paradigms are useful within Decker:

  - **Imperative:** To sequence actions, like showing a dialog and then navigating to another card.
  - **Declarative:** To query data stored in a `grid` widget or to find specific cards in the deck, e.g., `extract value where value..widgets.visited.value from deck.cards`.
  - **Functional/Vector:** To process data before displaying it, without needing slow loops.

-----

## Troubleshooting

### Problem 1: An `if` statement behaves unpredictably with list comparisons.

  - **Symptoms:** An `if` block either never runs or always runs when comparing a value against a list.
  - **Solution:** You are likely using the conforming equals operator (`=`), which returns a list of boolean results. In a conditional, you almost always want the non-conforming match operator (`~`), which returns a single `1` or `0`.

### Problem 2: A recursive function crashes with a stack overflow on large inputs.

  - **Symptoms:** The script terminates unexpectedly when a recursive function is called with a large number or deep data structure.
  - **Solution:** Lil supports **tail-call elimination**. Ensure your recursive call is the very last operation performed in the function. If it's part of a larger expression (e.g., `1 + my_func[...]`), it is not in a tail position. Rewrite the function to accumulate its result in an argument.

-----

## Examples in Context

**Use Case: A Simple To-Do List in Decker**

Imagine a Decker card with a `grid` widget named "tasks" (with columns "desc" and "done") and a `field` widget named "summary".

```lil
# In the script of the "tasks" grid, to update when it's changed:
on change do
  # Use a DECLARATIVE query to get the done/total counts.
  # The query source is "me.value", the table in the grid.
  stats: extract done:sum done total:count done from me.value

  # Use IMPERATIVE assignment to update the summary field.
  summary.text: format["%d of %d tasks complete.", stats.done, stats.total]
end
```

This tiny script uses a declarative query to read the state and an imperative command to update the UI, demonstrating a practical mix of paradigms.