documentation updates to reflect removal of the let keyword

also some general cleanups and improvements
This commit is contained in:
りき萌 2025-06-30 23:54:50 +02:00
parent 731046d1f7
commit bc2df73487
2 changed files with 125 additions and 67 deletions

View file

@ -317,7 +317,7 @@ Once you define a name, its associated data stays the same throughout the entire
So we can define `thickness` to be `4`, and then use it in our scribbles.
```haku
thickness = 4
thickness: 4
withDotter \d ->
[
@ -327,10 +327,11 @@ withDotter \d ->
]
```
`name = data` is a special operator in haku that tells the language "whenever we say `name`, we mean `data`."
`name: data` is a special operator in haku that means "whenever we say `name`, we mean `data`."
We call this operator _def_, short for _definition_.
We cannot use it in arbitrary places in our program, because it wouldn't make sense.
What does it mean to have a stroke whose thickness is `meow = 5`?
What does it mean to have a stroke whose thickness is `meow: 5`?
To keep a consistent program structure, haku also forces all your defs to appear _before_ your scribble.
You can think of the defs as a list of ingredients for the final scribble.
@ -348,8 +349,8 @@ We'll get to why soon!
Anyways, we can likewise replace our `2` constants with a def:
```haku
thickness = 4
xOffset = 2
thickness: 4
xOffset: 2
withDotter \d ->
[
@ -380,9 +381,9 @@ But now there's a problem.
If we change our `thickness` back to `8`, our points will overlap!
```haku
thickness = 8
---
xOffset = 2
thickness: 8
---
xOffset: 2
withDotter \d ->
[
@ -394,9 +395,9 @@ withDotter \d ->
So we'll make our `xOffset` calculated dynamically from the `thickness`, to not have to update it every time.
```haku
thickness = 8
xOffset = thickness / 2
-------------
thickness: 8
xOffset: thickness / 2
-------------
withDotter \d ->
[
@ -484,7 +485,7 @@ That'll need fixing!
Either way, let's define a function that'll make us those circles!
```haku
splat = \d, radius ->
splat: \d, radius ->
fill #0001 (circle (d To) radius)
withDotter \d ->
@ -534,7 +535,7 @@ haku limits the use of overloading to system functions for simplicity---adding o
Since these transparent circles are so much easier to draw now, let's make a few more of them!
```haku
splat = \d, radius ->
splat: \d, radius ->
fill #0001 (circle (d To) radius)
withDotter \d ->
@ -580,10 +581,10 @@ Until some threshold is reached, in which case we just make a single circle.
The first part is easy to do: haku allows us to define a function that calls itself without making any fuss.
```haku
splat = \d, radius ->
splat: \d, radius ->
fill #0001 (circle (d To) radius)
airbrush = \d, size ->
airbrush: \d, size ->
[
splat d size
airbrush d (size - 8)
@ -616,9 +617,9 @@ We call this act of switching execution paths _branching_.
Try this out---change the `radius`, and observe how your brush changes color once you set it beyond 16:
```haku
radius = 8
radius: 8
color =
color:
if (radius < 16)
#00F
else
@ -641,10 +642,10 @@ An `if` only calculates the argument it needs to produce the result.
This allows us to use it to prevent unbounded recursion in our `airbrush` example.
```haku
splat = \d, radius ->
splat: \d, radius ->
fill #0001 (circle (d To) radius)
airbrush = \d, size ->
airbrush: \d, size ->
if (size > 0)
[
splat d size
@ -667,10 +668,10 @@ But the airbrush still looks super primitive.
Let's try increasing the fidelity by doing smaller steps!
```haku
splat = \d, radius ->
splat: \d, radius ->
fill #0001 (circle (d To) radius)
airbrush = \d, size ->
airbrush: \d, size ->
if (size > 0)
[
splat d size
@ -687,11 +688,11 @@ withDotter \d ->
Well... sure, that's just a black blob with a slight gradient on the outer edge, so let's decrease the opacity.
```haku
splat = \d, radius ->
splat: \d, radius ->
fill #00000004 (circle (d To) radius)
---------
airbrush = \d, size ->
airbrush: \d, size ->
if (size > 0)
[
splat d size
@ -740,7 +741,7 @@ Most commonly, colors are blended using _linear interpolation_---which is essent
Mathematically, linear interpolation is defined using this formula:
```haku
lerp = \a, b, t ->
lerp: \a, b, t ->
a + (b - a) * t
```
@ -806,12 +807,30 @@ For now you'll have to construct your brushes with this in mind.
### And more limits
There are more limits on top of this, which stem from haku's design.
Since it's running _your_ code on _my_ server, it has some arbitrary limits set to prevent it from causing much harm.
There are more limits on top of this, which stem from rakugaki's design.
haku code cannot be too long, and it cannot execute too long.
It cannot consume too much memory---you cannot have too many definitions, or too many temporary values at once.
There are also memory usage limits on "heavyweight" data, such as functions or lists.
Recall that rakugaki is multiplayer!
You can draw with your friends.
But, your friends may not have a computer as good as yours.
So to keep the experience fair, haku sets some limits on brush code.
A brush cannot be too long, and it cannot execute too long.
It also cannot consume too much memory---you cannot have too many definitions, or too many temporary values at once.
If you ever brush (ha ha) up against these limits, you'll see colorful bars appear beside the brush preview in the bottom right corner.
These bars show you how much you're nearing the limits!
Try adding this line to your brush:
```haku
r: range 1 30000
```
`range 1 30000` generates a list of integers between 1 and 30000, without having you write them out one by one.
But we can also see that it consumes a bunch of _fuel_ (units of execution time), and _a lot_ of memory---almost all, in fact!
Code size is harder to run up against, because it requires writing a pretty huge amount of characters into the editor.
Feel free to try it out yourself---try writing out some really long lists by hand, and see what happens!
## Reticles
@ -839,17 +858,17 @@ This allows you to _animate_ your brushes over time!
For example, this brush draws a rainbow.
```haku
colorCurve = \n ->
colorCurve: \n ->
abs (cos n)
pi = 3.14159265
l = 0.1 -- wavelength
pi: 3.14159265
l: 0.1 -- wavelength
withDotter \d ->
let r = colorCurve (d Num * l)
let g = colorCurve (d Num * l + pi/3)
let b = colorCurve (d Num * l + 2*pi/3)
let color = rgba r g b 1
r = colorCurve (d Num * l)
g = colorCurve (d Num * l + pi/3)
b = colorCurve (d Num * l + 2*pi/3)
color = rgba r g b 1
stroke 8 color (line (d From) (d To))
```
@ -859,28 +878,28 @@ Currently, `withDotter` is the only reticle available in rakugaki, and it cannot
In the future rakugaki might get reticles that let you select lines, rectangles, ellipses, curves... but today is not that day.
### What's that, `let`?
### What's that, `=`?
I mentioned before that you cannot have defs inside functions.
What you _can_ have though, is `let`s, which define _variables_.
What you _can_ have though, is _variables_, defined with `name = data`.
Unlike defs, which are constant and cannot vary, variables' values can depend on function parameters---and a function can be called with a different set of parameters each time, thus making them variable!
A `let` always takes the following form.
A variable always takes the following form.
```haku
let name = value
name = value
then
```
It's very similar to a def, with one major difference.
Because a `let` by itself only _names a value_ and does not have a result, it must be followed by another expression on the following line---and that expression determines the result.
The magic is that this continuing expression can refer to the `name` we had previously assigned in the `let` expression.
Because a variable by itself only _names a value_ and does not have a result, it must be followed by another expression on the following line---and that expression determines the result.
The magic is that this continuing expression can refer to the `name` we had previously assigned in the `name = value` expression.
::: aside
Here's a bit of trivia: the variable defined by a `let` is exactly the same as a function parameter.
The `let` above is equivalent to applying the argument `value` to a function taking in the parameter `name`, and returning `then` as the result.
Here's a bit of trivia: variables are exactly the same as function parameters!
The `name = value` expression above is equivalent to applying the argument `value` to a function taking in the parameter `name`, and returning `then` as the result.
```haku
(\name -> then) value
@ -893,15 +912,15 @@ That's right. haku is a cute little Haskell for artists.
:::
`let`s aren't only useful for reusability---they're also helpful for breaking your brushes into smaller, more digestible pieces!
Variables aren't only useful for reusability---they're also helpful for breaking your brushes into smaller, more digestible pieces!
Compare the above version of the rainbow brush to this version, where all the `let`s are written inline:
```haku
colorCurve = \n ->
colorCurve: \n ->
abs (cos n)
pi = 3.14159265
l = 0.1 -- wavelength
pi: 3.14159265
l: 0.1 -- wavelength
withDotter \d ->
stroke 8 (rgba (colorCurve (d Num * l)) (colorCurve (d Num * l + pi/3)) (colorCurve (d Num * l + 2*pi/3)) 1) (line (d From) (d To))
@ -912,6 +931,41 @@ That's one hard to read beast of a `stroke`!
Generally, if a line is so long it wraps around rakugaki's narrow little text editor, it's probably a good idea to split it into variables.
### So... sometimes it's `:`, sometimes it's `=`, help, I'm confused!
If you're having trouble understanding when to use `:` and when to use `=`, here's a short version:
- `:` defines a name across your whole program.
- `=` defines a name that's only visible on the next line (more or less.)
Since `:` defines a name accessible from your whole program, it cannot access function parameters---which are temporary and local.
The opposite of "accessible in the whole program"!
As for `=`, I said it defines a name that's only visible on the next line "_more or less_", because technically the line that follows can actually be broken up into multiple lines, as would be the case with e.g. an `if`:
```haku
color: #000
thickness: 4
length: 5
duty: 0.5
or_: \a, b -> -- haku doesn't have a boolean OR yet...
if (a) a
else b
withDotter \d ->
visible = mod (d Num) length < length * duty
if (visible)
-- this is more than one line, and `visible` can still be used here!
stroke thickness color (line (d From) (d To))
else
() -- ...and here, too!
```
Another thing: as shown in the examples above, you can chain multiple `=` expressions together, which also breaks this "next line" rule of thumb.
But it should still help in building an intuition and spotting the patterns!
## Have fun
With that said, I hope you can have fun with rakugaki despite it being in its infancy!