Skip to content

Commit 5781d50

Browse files
authored
Unit tests and closures (#1)
* Closures and unit tests. I think there should only be one syntax for declaring a function, whether it's named or anonymous. I like how Javascript lets you do this with `const myFunc = (x) => {x+1}`. We should take inspiration from that. New function syntax is ```kcl (params -> return type) => expr ``` If you want to bind this function to a name, just do so! ```kcl myFunctionName = (params -> return type) => expr ``` * We don't need result because there's no fallible operations like network requests * Review from Frank * Fix Frank's comment From #1 (comment) * Feedback from Jess re GLTF
1 parent 3786243 commit 5781d50

File tree

1 file changed

+92
-26
lines changed

1 file changed

+92
-26
lines changed

fantasy-docs.md

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,9 @@ A measurement type (e.g. `Force` or `Distance`) may have a lot of different cons
141141
c. `Distance.Metre(1)`
142142
d. `Distance.Yard(0.9144)`
143143
2. A combination of different units, using a standard mathematical definition, e.g.
144-
a. `Force.mdt(Mass::Kilogram(1), Distance::Metre(1), Time::Second(1))` (force = mass * distance * time)
145-
b. `Area.rectangle(Distance::Metre(1), Distance::Metre(2))` (area = distance_x * distance_y)
146-
c. `Area.rectangle(Distance::Metre(1), Distance::Yard(1.8288))` (same equation as above, but mixing imperial and metric)
147-
148-
2. As a combination of named units, e.g. `x = Force<Kilogram, Metre, Second>(675.559)`
144+
a. `Force.mdt(Mass.Kilogram(1), Distance.Metre(1), Time.Second(1))` (force = mass * distance * time)
145+
b. `Area.rectangle(Distance.Metre(1), Distance.Metre(2))` (area = distance_x * distance_y)
146+
c. `Area.rectangle(Distance.Metre(1), Distance.Yard(1.8288))` (same equation as above, but mixing imperial and metric)
149147

150148
Both these expressions have the same _type_ (`Force`) but use different _constructors_ to give the specific value. For more about syntax, see the Syntax section below.
151149

@@ -231,7 +229,7 @@ Enums can have fields associated with them:
231229
enum ExtrudeTop = Open | Closed(Material)
232230
```
233231

234-
has two possible variants. The first variant is `Open` and this variant only has one possible value: the `Open` itself. The second variant,`Closed`, has a field `Material`, so if you create a value of type `ExtrudeTop::Material` it must also have a `Material`.
232+
has two possible variants. The first variant is `Open` and this variant only has one possible value: the `Open` itself. The second variant,`Closed`, has a field `Material`, so if you create a value of type `ExtrudeTop.Material` it must also have a `Material`.
235233

236234
The standard library uses enums, and users can create their own. There are no nulls or undefined in KCL, instead, we use the Option enum:
237235

@@ -241,8 +239,6 @@ enum Option a = None | Some(a)
241239

242240
This enum has two variants. Either it is `None` or it's `Some`, and if it's `Some` then it also has a value of type `a` (see the "Type variables" section above).
243241

244-
There is also `enum Result t e = Ok(t) | Err(e)`, which is a useful way to express operations that might succeed or fail with an error value. Option and Result work just like they do in other languages (see Java, Swift, Rust, Haskell, Elm).
245-
246242
## Syntax
247243

248244
### Functions
@@ -251,21 +247,21 @@ A KCL program is made up of _functions_. A function has a name, parameters, and
251247

252248
```kcl
253249
/// A can for our line of *awesome* new [baked beans](https://example.com/beans).
254-
can_of_beans(radius: Distance, height: Distance) -> Solid3d =
250+
can_of_beans = (radius: Distance, height: Distance -> Solid3d) =>
255251
circle(radius)
256252
|> extrude_closed(material.aluminium, height)
257253
```
258254

259255
Let's break this down line-by-line.
260256
1. Docstring: a comment which describes the function below it. Your KCL editor probably supports showing this comment when you mouse over the function, or over a visualization of the 3D shape it outputs. Your editor/viewer also probably supports Markdown.
261-
2. This is the function signature. It lets readers (and the Puffin compiler) know this is a function called "can_of_beans". The function takes two parameters, called "radius" and "height". Both parameters have type Distance. It returns a Solid3d. The "=" sign marks the end of the function signature and the start of the expression/function body.
262-
3. First line of the function body. This calls the function "circle" with the parameter "radius".
257+
2. This is where the function starts. First is the function name, "can_of_beans". The name is followed by an `=`, then the function signature. The signature describes the function's parameters and return types. This function takes two parameters, called "radius" and "height". Both parameters have type Distance. It returns a Solid3d. The `=>` marks the end of the function signature, and the start of the function body.
258+
3. The function body is an _expression_. The first line of the expression is calling the function "circle" with the parameter "radius".
263259
4. The |> operator composes two functions. If you see `f |> g` it means "calculate `f` then apply its output as input to `g`". So, this line takes the circle from the previous line, and uses it as the last parameter to `extrude_closed`.
264260

265261
You could write this same function in a different way without the `|>` operator:
266262

267263
```kcl
268-
can_of_beans(radius, height) =
264+
can_of_beans = (radius: Distance, height: Distance -> Solid3d) =>
269265
extrude_closed(material.aluminium, height, circle(radius))
270266
```
271267

@@ -274,7 +270,7 @@ But we generally find the `|>` operator makes your KCL functions easier to read.
274270
In this example function, we specified the types of both input parameters and the output type. But the KCL compiler is smart! It's smart enough to infer the types of our parameters and return types even if you don't specify them. So you could have written
275271

276272
```kcl
277-
can_of_beans(radius, height) =
273+
can_of_beans = (radius, height) =>
278274
circle(radius)
279275
|> extrude_closed(material.aluminium, height)
280276
```
@@ -284,22 +280,23 @@ Here, the KCL compiler:
284280
* Infers `height` must be a `Distance` because the second parameter of `extrude_closed` is a `Distance`.
285281
* Infers the function returns a `Solid3d` because it's returning the return value of `extrude_closed`, which is `Solid3d`.
286282

287-
To invoke the function, you'd type
283+
To invoke the function, you'd do this:
288284

289285
```kcl
290-
can_of_beans((10, Centimeter), (1, Foot))
286+
can_of_beans(Distance.Centimeter(10), Distance.Foot(1))
291287
```
292288

289+
(Note that, as discussed above, this example uses KCL measurement types (e.g. distance) instead of general-purpose number types. This lets you seamlessly interoperate between different units of measurement, like feet and centimeters)
290+
293291
This is an expression which evaluates `can_of_beans` with its two input parameters, i.e. radius and height. You can use this anywhere an expression is valid, which currently is just
294292
1. As an argument to a function
295293
2. As the body of a function
296294

297-
Note that, as discussed above, this example uses KCL measurement types (e.g. distance) instead of general-purpose number types. This lets you seamlessly interoperate between different units of measurement, like feet and centimeters.
298295

299296
Some units have aliases, so you could also write
300297

301298
```kcl
302-
can_of_beans((10, Cm), (1, Ft))
299+
can_of_beans(Cm(10), Ft(1))
303300
```
304301

305302
See the [docs](units) for all units and aliases.
@@ -308,35 +305,69 @@ Every KCL function body is a single expression. If a function body gets really b
308305

309306
```kcl
310307
let
311-
can_radius = (10, Cm)
312-
can_height = (1, Ft)
308+
can_radius = Cm(10)
309+
can_height = can_radius * 5
313310
in can_of_beans(can_radius, can_height)
314311
```
315312

316313
The constants you create in `let` are scoped to the let-in expression. The value of the expression is the `in` part. Let-in blocks are a standard piece of notation from functional programming languages (e.g. in [Elm][elm-let-in], [OCaml][ocaml-let-in] and [Haskell][haskell-let-in]) that help make large expressions more readable. We find they make large KCL functions readable too.
317314

318-
### Constants
315+
#### Constants
319316

320317
KCL doesn't have any mutation or changes, so there aren't any variables. Files contain a number of functions -- that's it.
321318

322-
Other languages have named constants and variables. KCL doesn't have variables (because the language describes unchanging geometry and physical characteristics of real-world objects). But it _does_ have named constants. Here's two different ways to declare it:
319+
Other languages have named constants and variables. KCL doesn't have variables (because the language describes unchanging geometry and physical characteristics of real-world objects). But it _does_ have named constants. Here's how you declare them.
323320

324321
```kcl
325-
my_can = can_of_beans((10, Cm), (1, Ft))
322+
my_can = can_of_beans(Cm(10), Ft(1))
326323
```
327324

328325
This declares a named constant called `my_can`, which is the result of calling the `can_of_beans` function we defined above. KCL compiler inferred the type, but you can add a type annotation if you want to:
329326

330327
```kcl
331-
my_can: Solid3d = can_of_beans((10, Cm), (1, Ft))
328+
my_can: Solid3d = can_of_beans(Cm(10), Ft(1))
332329
```
333330

334331
This named constant is actually just syntactic sugar for a function that takes 0 parameters. After all, functions called with the same inputs always return the same value -- they're fully deterministic. So a function with 0 parameters is just a function that always returns a constant value. Or, to simplify: it _is_ a constant value.
335332

336333
Without the syntactic sugar, `my_can` could be declared like this:
337334

338335
```kcl
339-
my_can() -> Solid3d = can_of_beans((10, Cm), (1, Ft))
336+
my_can = (-> Solid3d) => can_of_beans(Cm(10), Ft(1))
337+
```
338+
339+
Note the function signature. Function signatures are always (parameters -> return type), but here we have no parameters, so the function signature just omits them.
340+
341+
#### Functions as values
342+
343+
Sometimes, functions are parameters to other functions.
344+
345+
```kcl
346+
doubleDistance = (d: Distance) =>
347+
d * 2
348+
349+
doubleAllDistances = (distances: List Distance -> List Distance) =>
350+
List.map(doubleDistance, distances)
351+
```
352+
Here, the `doubleAllDistances` function takes a list of distances and returns a list where all distances are doubled. It does this using the standard library function `List.map`. This takes two parameters:
353+
354+
1. A function to call on every element of a list
355+
2. The list whose elements should be passed into the above function
356+
357+
This is neat. You can do a lot with standard library functions like this. However, there's another way to write this code.
358+
359+
```kcl
360+
doubleAllDistances = (distances: List Distance -> List Distance) =>
361+
List.map((x) => x * 2, distances)
362+
```
363+
364+
In this version, we've replaced the named function `doubleDistance` with an _anonymous function_ (also known as a _closure_). These closures use the same syntax for function declaration -- parameters, then `=>`, then the body. This lets you keep your code a little bit more concise.
365+
366+
Again, you don't need to specify function types, but if you want to, you can.
367+
368+
```kcl
369+
doubleAllDistances = (distances: List Distance -> List Distance) =>
370+
List.map((x: Distance -> Distance) => x * 2, distances)
340371
```
341372

342373
### KCL files
@@ -351,9 +382,9 @@ This invites the question: how do I use this function and why did I write it?
351382

352383
There is an open ecosystem of tooling that understands KCL files, and can visualize or analyze the functions contained therein. Here are some examples of what you can do with a KCL function.
353384

354-
1. Open it in a KCL viewer. The primary KCL visualizer is built into KittyCAD's KCL live-editing [web app](untitled-app). However, other visualizers exist too. These visualizers help you understand your model and show it to teammates, clients, fans, etc.
385+
1. Open it in a KCL viewer. The primary KCL visualizer is built into KittyCAD's [modeling app](untitled-app). However, other visualizers exist too. These visualizers help you understand your model and show it to teammates, clients, fans, etc.
355386
2. Send it to a service like KittyCAD's analysis API. Generally, you send a KCL file, a query type (e.g. "mass" or "cost to print") and your desired unit of measurement (e.g. "kilograms") to that API. Then the API will analyze your KCL, figure out the answer, and convert it to your requested units.
356-
3. Send it to a 3D printing or prototyping service. They'll accept a KCL file and print out the object returned by your function.
387+
3. Export it to KittyCAD's GLTF file format, then send that to 3D printing services or manufacturing services. They'll print/manufacture the object your function describes.
357388
4. Convert it to other, less advanced formats, for your colleagues stuck at legacy companies that use Autodesk.
358389

359390
In all these cases, you can choose one or more KCL functions to visualize/analyze/print/export. If you don't specify, the KCL ecosystem generally defaults to looking for a function called `main`. This convention is useful! For example, if a client wants you to build a bookshelf, you can send them a KCL file, where the `main` function outputs the bookshelf. When they open the file in a KCL viewer, they'll see the bookshelf. But they can also open up the sidebar, and look at all the other KCL functions your bookshelf is composed of. Then they can visualize those function separately -- e.g. they might want to drill down to view only the shelf, or the backboard.
@@ -362,6 +393,41 @@ If your function accepts 0 parameters, then it can be visualized easily. But how
362393

363394
This is a really powerful way to let consumers customize the goods you've designed before buying or manufacturing them. For example, you might put a design for a cool 3D printed office chair on thingiverse.com which has a function `main(name: Text)`. This function describes a chair with the given name embossed into the back. When a consumer wants to 3D print it, the 3D printing service will let them input their desired name, view the chair with that name embossed, and then order it.
364395

396+
## Tests
397+
Functions are marked as tests by putting the `#[test]` attribute above them. They're run via the KittyCAD CLI or by the test runner built into the KittyCAD modeling app.
398+
399+
```kcl
400+
#[test]
401+
division_by_1_doesnt_change_number = () =>
402+
assert_eq(4/1, 4)
403+
404+
// Because these functions take no arguments, you can use the syntax sugar
405+
// from the "Constants" section above.
406+
#[test]
407+
division_by_10 =
408+
let
409+
expected = 10;
410+
actual = 100/10;
411+
in assert_eq(expected, actual)
412+
```
413+
The `assert_eq` function will fail the test if the arguments aren't equal. There are similar functions like `assert()` which just checks if its argument is true, or `assert_ne()` which asserts the two are not equal. Tests are run in parallel, because there's no way for two tests to interfere with each other.
414+
415+
Test functions cannot take parameters, nor can they return values. So, what if you want to test many different (expected, actual) pairs for your function? Well, you can call `assert_eq` on a list of values. Like this:
416+
417+
```kcl
418+
#[test]
419+
multiplication_by_zero() =
420+
let
421+
n = 100
422+
inputs = List.range(0, n) // A list of numbers from `0` to `n`.
423+
expected = List.replicate(0, n) // A list of length `n`, every element is `0`.
424+
actual = List.map((x) => x * 0, inputs)
425+
in
426+
List.map2(assert_eq, actual, expected)
427+
```
428+
Here, the function `List.map2` is a lot like `List.map` except it has _two_ input lists. Its function argument takes an element from each list, instead of just from one list. So, it takes a function of type `(a, b) => c`, a `List a` and a `List b` and passes them into the function, element by element, creating a `List c`.
429+
430+
So, used here, it takes the input lists `actual` and `expected`, then passes an element from each into `assert_eq`.
365431

366432
[units]: https://kittycad.io/docs/units
367433
[untitled-app]: https://kittycad.io/untitled-app

0 commit comments

Comments
 (0)