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. So we can define `thickness` to be `4`, and then use it in our scribbles.
```haku ```haku
thickness = 4 thickness: 4
withDotter \d -> 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. 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. 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. 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: Anyways, we can likewise replace our `2` constants with a def:
```haku ```haku
thickness = 4 thickness: 4
xOffset = 2 xOffset: 2
withDotter \d -> withDotter \d ->
[ [
@ -380,9 +381,9 @@ But now there's a problem.
If we change our `thickness` back to `8`, our points will overlap! If we change our `thickness` back to `8`, our points will overlap!
```haku ```haku
thickness = 8 thickness: 8
--- ---
xOffset = 2 xOffset: 2
withDotter \d -> 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. So we'll make our `xOffset` calculated dynamically from the `thickness`, to not have to update it every time.
```haku ```haku
thickness = 8 thickness: 8
xOffset = thickness / 2 xOffset: thickness / 2
------------- -------------
withDotter \d -> withDotter \d ->
[ [
@ -484,7 +485,7 @@ That'll need fixing!
Either way, let's define a function that'll make us those circles! Either way, let's define a function that'll make us those circles!
```haku ```haku
splat = \d, radius -> splat: \d, radius ->
fill #0001 (circle (d To) radius) fill #0001 (circle (d To) radius)
withDotter \d -> 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! Since these transparent circles are so much easier to draw now, let's make a few more of them!
```haku ```haku
splat = \d, radius -> splat: \d, radius ->
fill #0001 (circle (d To) radius) fill #0001 (circle (d To) radius)
withDotter \d -> 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. The first part is easy to do: haku allows us to define a function that calls itself without making any fuss.
```haku ```haku
splat = \d, radius -> splat: \d, radius ->
fill #0001 (circle (d To) radius) fill #0001 (circle (d To) radius)
airbrush = \d, size -> airbrush: \d, size ->
[ [
splat d size splat d size
airbrush d (size - 8) 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: Try this out---change the `radius`, and observe how your brush changes color once you set it beyond 16:
```haku ```haku
radius = 8 radius: 8
color = color:
if (radius < 16) if (radius < 16)
#00F #00F
else 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. This allows us to use it to prevent unbounded recursion in our `airbrush` example.
```haku ```haku
splat = \d, radius -> splat: \d, radius ->
fill #0001 (circle (d To) radius) fill #0001 (circle (d To) radius)
airbrush = \d, size -> airbrush: \d, size ->
if (size > 0) if (size > 0)
[ [
splat d size splat d size
@ -667,10 +668,10 @@ But the airbrush still looks super primitive.
Let's try increasing the fidelity by doing smaller steps! Let's try increasing the fidelity by doing smaller steps!
```haku ```haku
splat = \d, radius -> splat: \d, radius ->
fill #0001 (circle (d To) radius) fill #0001 (circle (d To) radius)
airbrush = \d, size -> airbrush: \d, size ->
if (size > 0) if (size > 0)
[ [
splat d size 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. Well... sure, that's just a black blob with a slight gradient on the outer edge, so let's decrease the opacity.
```haku ```haku
splat = \d, radius -> splat: \d, radius ->
fill #00000004 (circle (d To) radius) fill #00000004 (circle (d To) radius)
--------- ---------
airbrush = \d, size -> airbrush: \d, size ->
if (size > 0) if (size > 0)
[ [
splat d size 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: Mathematically, linear interpolation is defined using this formula:
```haku ```haku
lerp = \a, b, t -> lerp: \a, b, t ->
a + (b - a) * 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 ### And more limits
There are more limits on top of this, which stem from haku's design. There are more limits on top of this, which stem from rakugaki's design.
Since it's running _your_ code on _my_ server, it has some arbitrary limits set to prevent it from causing much harm.
haku code cannot be too long, and it cannot execute too long. Recall that rakugaki is multiplayer!
It cannot consume too much memory---you cannot have too many definitions, or too many temporary values at once. You can draw with your friends.
There are also memory usage limits on "heavyweight" data, such as functions or lists. 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 ## Reticles
@ -839,17 +858,17 @@ This allows you to _animate_ your brushes over time!
For example, this brush draws a rainbow. For example, this brush draws a rainbow.
```haku ```haku
colorCurve = \n -> colorCurve: \n ->
abs (cos n) abs (cos n)
pi = 3.14159265 pi: 3.14159265
l = 0.1 -- wavelength l: 0.1 -- wavelength
withDotter \d -> withDotter \d ->
let r = colorCurve (d Num * l) r = colorCurve (d Num * l)
let g = colorCurve (d Num * l + pi/3) g = colorCurve (d Num * l + pi/3)
let b = colorCurve (d Num * l + 2*pi/3) b = colorCurve (d Num * l + 2*pi/3)
let color = rgba r g b 1 color = rgba r g b 1
stroke 8 color (line (d From) (d To)) 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. 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. 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! 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 ```haku
let name = value name = value
then then
``` ```
It's very similar to a def, with one major difference. 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. 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 `let` expression. The magic is that this continuing expression can refer to the `name` we had previously assigned in the `name = value` expression.
::: aside ::: aside
Here's a bit of trivia: the variable defined by a `let` is exactly the same as a function parameter. Here's a bit of trivia: variables are exactly the same as function parameters!
The `let` above is equivalent to applying the argument `value` to a function taking in the parameter `name`, and returning `then` as the result. 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 ```haku
(\name -> then) value (\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: Compare the above version of the rainbow brush to this version, where all the `let`s are written inline:
```haku ```haku
colorCurve = \n -> colorCurve: \n ->
abs (cos n) abs (cos n)
pi = 3.14159265 pi: 3.14159265
l = 0.1 -- wavelength l: 0.1 -- wavelength
withDotter \d -> 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)) 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. 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 ## Have fun
With that said, I hope you can have fun with rakugaki despite it being in its infancy! With that said, I hope you can have fun with rakugaki despite it being in its infancy!

View file

@ -25,8 +25,6 @@ Operators may have one or two arguments, where one argument corresponds to a pre
Note that this documentation lists a unary and binary operator of the same spelling as _two separate functions_, not overloads of a single function. Note that this documentation lists a unary and binary operator of the same spelling as _two separate functions_, not overloads of a single function.
The argument name usually does not matter when calling the function - it is only used for documentation purposes. The argument name usually does not matter when calling the function - it is only used for documentation purposes.
The one exception is arguments called `...`, which signify that zero or more arguments can be passed to the function at that position.
(Currently there are no functions that accept any number of arguments, though.)
The argument _type_ however is important. The argument _type_ however is important.
If you try to use a function with the wrong type of value as its argument, it will fail with an error. If you try to use a function with the wrong type of value as its argument, it will fail with an error.
@ -567,7 +565,7 @@ For example, consider multiplicatively blending two colors.
```haku ```haku
-- This is how you can multiply two colors together. -- This is how you can multiply two colors together.
-- Note that the `*` operator works for colors, so you don't need to define this in your brushes. -- Note that the `*` operator works for colors, so you don't need to define this in your brushes.
mulRgba = \a, b -> mulRgba: \a, b ->
rgba (rgbaR a * rgbaR b) (rgbaG a * rgbaG b) (rgbaB a * rgbaB b) (rgbaA a * rgbaA b) rgba (rgbaR a * rgbaR b) (rgbaG a * rgbaG b) (rgbaB a * rgbaB b) (rgbaA a * rgbaA b)
``` ```
@ -575,11 +573,11 @@ If haku represented colors using an 8-bit `0` to `255` range instead, to multipl
```haku ```haku
-- NOTE: This example does NOT work correctly. -- NOTE: This example does NOT work correctly.
mulRgba = \a, b -> mulRgba: \a, b ->
let red = (rgbaR a * rgbaR b) / 255 red = (rgbaR a * rgbaR b) / 255
let green = (rgbaG a * rgbaG b) / 255 green = (rgbaG a * rgbaG b) / 255
let blue = (rgbaB a * rgbaB b) / 255 blue = (rgbaB a * rgbaB b) / 255
let alpha = (rgbaA a * rgbaA b) / 255 alpha = (rgbaA a * rgbaA b) / 255
rgba red green blue alpha rgba red green blue alpha
``` ```
@ -683,32 +681,38 @@ Some of these operations may be a bit confusing, so here are some examples.
```haku ```haku
-- To add two to all elements in a list: -- To add two to all elements in a list:
list = range 1 4 -- [1, 2, 3, 4] list: range 1 4 -- [1, 2, 3, 4]
twoAdded = map list \x -> twoAdded: map list \x ->
x + 2 x + 2
-- [3, 4, 5, 6]
``` ```
```haku ```haku
-- To filter out only even numbers in a list: -- To filter out only even numbers in a list:
list = range 1 10 list: range 1 10 -- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
isEven = \x -> mod x 2 == 0 isEven: \x -> mod x 2 == 0
onlyEven = filter list isEven onlyEven: filter list isEven
-- [2, 4, 6, 8, 10]
``` ```
```haku ```haku
-- To sum all the numbers in a list: -- To sum all the numbers in a list:
list = [1, 3, 10, 2, 30, 4, 1] list: [1, 3, 10, 2, 30, 4, 1]
sum = reduce list 0 \acc, value -> acc + value sum: reduce list 0 \acc, value -> acc + value
-- 51
``` ```
```haku ```haku
-- To flatten a singly-nested list: -- To flatten a singly-nested list:
list = [[1, 2], [3, 4], [5, 6]] list: [[1, 2], [3, 4], [5, 6]]
flatList = flatten list -- [1, 2, 3, 4, 5, 6] flatList: flatten list -- [1, 2, 3, 4, 5, 6]
-- Note that this only applies to a single level of nesting: -- Note that this only applies to a single level of nesting:
deepList = [[[1, 2, 3, 4]]] deepList: [[[1, 2, 3, 4]]]
lessDeepList = flatten deepList -- [[1, 2, 3, 4]] lessDeepList: flatten deepList -- [[1, 2, 3, 4]]
-- This can be used to join lists together without nesting:
join: \a, b -> flatten [a, b]
``` ```