KittyCAD / modeling-app

The KittyCAD modeling app.
https://kittycad.io/modeling-app/download
MIT License
411 stars 35 forks source link

Range expressions #3988

Open adamchalmers opened 1 month ago

adamchalmers commented 1 month ago

First, read https://github.com/KittyCAD/modeling-app/issues/3677

The if-else chain in that issue has two problems:

So we should consider a dedicated syntax for this, as it's likely to be a common pattern. One hypothetical example syntax:

let screwDiameter = selectFromRange(holeDiameter, "screw size", [
    minimum 0.16,
    up to 0.17 => 0.1875,
    up to 0.18 => 0.1900,
    up to 0.19 => 0.1920,
    maximum 0.19
])

the KCL executor would check that all the selections are in the right order, and that all possible values are covered. It forces users to explicitly consider the minimum/maximum ranges they support. It'd output a nicely-formatted error if the user goes above/below the range, e.g. if you put in 0.11 it'd error with

Cannot select a screw size because the holeDiameter of 0.11 is too small (minimum is 0.16)

We'd need some way to distinguish <= and <, e.g. "up to 0.18" vs. "up to but not 0.18"

adamchalmers commented 1 month ago

Moving @jtran 's comment here:

Would solution 2 support split range? I.e. a low range works and a high range works, but in the middle, it's an error.

let screwDiameter = selectFromRange(holeDiameter, "screw size", [
  minimum 0.16,
  up to 0.17 => 0.1875,
  up to 0.18 => 0.1920,
  up to 0.19 => 0.1900,
  maximum 0.19
  minimum 0.21,
  up to 0.22 => 0.22,
  maximum 0.22
])

Would solution 2 support variables, or do they need to be constant literals? For example, could I write this?

fn thing = (holeDiameter) => {
  let a = 0.16
  let increment = holeDiameter / 3
  let screwDiameter = selectFromRange(holeDiameter, "screw size", [
    minimum a,
    up to a + 1 * increment => 0.1875,
    up to a + 2 * increment => 0.1900,
    up to a + 3 * increment => 0.1920,
    maximum a + 4 * increment
  ])
}

Solution 2 feels very specialized to me. Based on the above, I'm wishing that there were some more primitive concepts that we could build solution 2 out of.

If it were a functional language, the primitives would be pattern matching combined with special underderstanding of how numbers work. You'd pattern match on holeDiameter, and the implementation could detect that you've covered all the cases, possibly with a catch-all. Maybe we can add syntax sugar if this is common.

let screwDiameter = select(holeDiameter, "screw size", [
  0.16 .. 0.17 => 0.1875,
       .. 0.18 => 0.1920,
       .. 0.19 => 0.1900,
])

In the above, to the left of => is any pattern, so the name selectFromRange doesn't need to be a range anymore, even though we've used ranges here.

The above would be a "compile" error (or whatever phase we can check, ideally before runtime) because it's non-exhaustive.

let screwDiameter = select(holeDiameter, "screw size", [
  0.16 .. 0.17 => 0.1875,
       .. 0.18 => 0.1920,
       .. 0.19 => 0.1900,
  _            => error
])

This uses a catch-all to make the match exhaustive. In that case, it evaluates to an error. It's actually a value that select recognizes and converts into a real runtime error with context of "screw size" and the runtime value of holeDiameter.

These are all just half-baked ideas off the top of my head.

jtran commented 2 weeks ago

A different take: The Ultimate Conditional Syntax.

Tl;dr: They merge pattern matching with if-then-else, similar to Rust's if-let chains, but more concise.