Generalize constraints to observables #48

Merged
glen merged 25 commits from Vectornaut/dyna3:observables_on_main into main 2025-03-10 23:43:25 +00:00
Member

Summary

The branch to be merged unifies the interface elements for measuring and constraining real-valued observables, as proposed in issue #47. Observables are presented as text inputs in the interface. When the observable is in measurement mode, the text field displays its value. Entering a desired value into the text field puts the observable in constraint mode. Setting the desired value to the empty string switches the observable back to measurement mode. If you enter a desired value that can't be parsed as a floating point number, the observable is set to constraint mode, but flagged as invalid.

Pitfalls

In this implementation of observables:

  • #49 The only way to deactivate a constraint is to remove its desired value, which throws away information.
  • #50 You can't see the measured value of an unsatisfied constraint.

Review notes

The overall diff for this pull request includes a lot of line noise from renaming existing variables. The renaming is concentrated in commit 677ef47, so you should be able to focus on structural changes by looking at the diffs for the other commits individually.

## Summary The branch to be merged unifies the interface elements for measuring and constraining real-valued observables, as proposed in issue #47. Observables are presented as text inputs in the interface. When the observable is in measurement mode, the text field displays its value. Entering a desired value into the text field puts the observable in constraint mode. Setting the desired value to the empty string switches the observable back to measurement mode. If you enter a desired value that can't be parsed as a floating point number, the observable is set to constraint mode, but flagged as invalid. ## Pitfalls In this implementation of observables: - **#49** The only way to deactivate a constraint is to remove its desired value, which throws away information. - **#50** You can't see the measured value of an unsatisfied constraint. ## Review notes The overall diff for this pull request includes a lot of line noise from renaming existing variables. The renaming is concentrated in commit 677ef47, so you should be able to focus on structural changes by looking at the diffs for the other commits individually.
Vectornaut added 4 commits 2025-02-10 21:19:34 +00:00
Also rename corresponding CSS classes and add methods to check roles.
Distinguish constraints from observables using dark background rather
than marker. Customize focus highlighting. Drop the input type selector
that used to make styling apply to the Lorentz product field but not the
constraint activation check box.
Owner

Another pitfall: If you have entered a collection of constraints that is unsatisfiable, then the current Assembly will have different values than the constraints for at least some of the measurements, but there will be no way to see what the actual values are. So it will be hard to gauge "how far off" the actual assembly is from the desired but unachievable one.

Do we want to consider addressing either of these pitfalls or any others that may come up in this PR? Or merely file them as issues when this PR is merged?

Another pitfall: If you have entered a collection of constraints that is unsatisfiable, then the current Assembly will have different values than the constraints for at least some of the measurements, but there will be no way to see what the actual values are. So it will be hard to gauge "how far off" the actual assembly is from the desired but unachievable one. Do we want to consider addressing either of these pitfalls or any others that may come up in this PR? Or merely file them as issues when this PR is merged?
Author
Member

I think it makes more sense to file them as issues, because they seem like things that could be addressed by building on top of the framework for observables laid out in this pull request.

I think it makes more sense to file them as issues, because they seem like things that could be addressed by building on top of the framework for observables laid out in this pull request.
Owner

OK, as we discussed in meeting today:

(a) Fine to file these pitfalls as two WIP issues

(b) These controllable quantities created in this PR should be labeled in the display as some sort of a thing to kick off such labeling. We talked about 'cos θ' as one possibility. I also looked up what people call this quantity when it is bigger than 1 in magnitude. According to the Wikipedia page authors vary in whether it is always called the "inversive distance" (regardless of its magnitude) or whether when it is no larger than one in magnitude it is called 'cos θ' and when it is larger than one in magnitude it is called 'cosh δ' where δ is the thing that is called the "inversive distance" -- the rationale in the latter case being that δ is additive among spheres that have the same radical plane with (either of) these spheres as they have with each other, just like θ is additive among all spheres that go through the same intersection circle, in that regime. And so this latter convention is equivalent to calling it 'cos iδ', and then I suppose we could call 'iδ' the imaginary angle between the two spheres, even though I did not really find any reference that uses that terminology (I didn't search for a long time). That terminology actually makes a fair amount of sense, though, even though it doesn't seem to be much used, especially given that it corresponds very well to the situation of the circle of intersection becoming one point of intersection at tangency and then turning into an imaginary circle of intersection as the spheres move apart -- and this latter terminology is used, the "imaginary intersection" of two nonintersecting spheres, so why not "imaginary angle" between them by analogy?

So anyhow, please add a commit to this PR that calls it something, choosing your favorite among options like "cos θ" (where we can explain that θ is allowed to be pure imaginary = iδ), "inversive distance" (just calling it that always), or "cos θ or cosh δ" or something along those lines. It's likely just a temporary name anyway in the long run.

(I take it that with our normalization choices, the lorentz product of any two planes has magnitude at most 1? It's not that they coincide at product = 1 and then stay parallel but with separation at larger lorentz products, is it?)

(c) we should contemplate what we are calling these entities in the code. Some options:

  1. Leave them as "Observable"
  2. Per the above paragraph, "ConstrainableQuantity" or "ControllableQuantity" (a bit of a keyboardful)
  3. What about "Dial"? That's the only common-English term that I could think of that definitely has connotations both of reading off a value and of setting a value. Short and sweet and has a nice ring to it. I guess I also though of "Thermostat" that has similar connotations -- people both read off the current temperature, and set it with a thermostat.
  4. Certainly open to other suggestions.
OK, as we discussed in meeting today: (a) Fine to file these pitfalls as two WIP issues (b) These controllable quantities created in this PR should be labeled in the display as some sort of a thing to kick off such labeling. We talked about 'cos θ' as one possibility. I also looked up what people call this quantity when it is bigger than 1 in magnitude. According to the [Wikipedia page](https://en.wikipedia.org/wiki/Inversive_distance) authors vary in whether it is always called the "inversive distance" (regardless of its magnitude) or whether when it is no larger than one in magnitude it is called 'cos θ' and when it is larger than one in magnitude it is called 'cosh δ' where δ is the thing that is called the "inversive distance" -- the rationale in the latter case being that δ is additive among spheres that have the same radical plane with (either of) these spheres as they have with each other, just like θ is additive among all spheres that go through the same intersection circle, in that regime. And so this latter convention is equivalent to calling it 'cos iδ', and then I suppose we could call 'iδ' the imaginary angle between the two spheres, even though I did not really find any reference that uses that terminology (I didn't search for a long time). That terminology actually makes a fair amount of sense, though, even though it doesn't seem to be much used, especially given that it corresponds very well to the situation of the circle of intersection becoming one point of intersection at tangency and then turning into an imaginary circle of intersection as the spheres move apart -- and this latter terminology _is_ used, the "imaginary intersection" of two nonintersecting spheres, so why not "imaginary angle" between them by analogy? So anyhow, please add a commit to this PR that calls it something, choosing your favorite among options like "cos θ" (where we can explain that θ is allowed to be pure imaginary = iδ), "inversive distance" (just calling it that always), or "cos θ or cosh δ" or something along those lines. It's likely just a temporary name anyway in the long run. (I take it that with our normalization choices, the lorentz product of any two planes has magnitude at most 1? It's not that they coincide at product = 1 and then stay parallel but with separation at larger lorentz products, is it?) (c) we should contemplate what we are calling these entities in the code. Some options: 1. Leave them as "Observable" 2. Per the above paragraph, "ConstrainableQuantity" or "ControllableQuantity" (a bit of a keyboardful) 3. What about "Dial"? That's the only common-English term that I could think of that definitely has connotations both of reading off a value and of setting a value. Short and sweet and has a nice ring to it. I guess I also though of "Thermostat" that has similar connotations -- people both read off the current temperature, and set it with a thermostat. 4. Certainly open to other suggestions.
Author
Member

(c) we should contemplate what we are calling these entities in the code

I like the control theory flavor of "thermostat." Here are some more suggestions in that vein.

  • "Regulator." Many familiar regulators, including thermostats and pressure regulators, show both the current value and the set point of the regulated variable. Suggests a name like "gauge" for observables that can't be constrained.
  • "Process value." Doesn't quite fit, because we're controlling a static system rather than a process.
> (c) we should contemplate what we are calling these entities in the code I like the control theory flavor of "thermostat." Here are some more suggestions in that vein. * [**"Regulator."**](https://en.wikipedia.org/wiki/Regulator_(automatic_control)) Many familiar regulators, including thermostats and [pressure regulators](https://en.wikipedia.org/wiki/Pressure_regulator), show both the current value and the set point of the regulated variable. Suggests a name like "gauge" for observables that can't be constrained. * [**"Process value."**](https://en.wikipedia.org/wiki/Process_variable) Doesn't quite fit, because we're controlling a static system rather than a process.
Author
Member

The term inversive distance sounds perfect for our use case. Nice find! With the current Lorentz product, -Q(I, J) is the inversive distance between the spheres represented by I and J. The ObservableInput component shows Q(I, J), but maybe we can ignore the sign difference and call that the inversive distance for now.

(b) So anyhow, please add a commit to this PR that calls it something

I've done this locally by hard-coding the label "Inversive distance" into the ObservableOutlineItem component, styled as shown in the attached screenshot. Is that good enough for this pull request? If so, I'll push it.

The term *inversive distance* sounds perfect for our use case. Nice find! With the current Lorentz product, $-Q(I, J)$ is the inversive distance between the spheres represented by $I$ and $J$. The `ObservableInput` component shows $Q(I, J)$, but maybe we can ignore the sign difference and call that the inversive distance for now. > (b) So anyhow, please add a commit to this PR that calls it something I've done this locally by hard-coding the label "Inversive distance" into the `ObservableOutlineItem` component, styled as shown in the attached screenshot. Is that good enough for this pull request? If so, I'll push it.
Author
Member

I've added issues for both of the pitfalls we've identified and linked them from the pull request description.

I propose using the tag "prospective" for issues that would be introduced by pull requests under review. If you like that name, could you add the tag and assign it to each pitfall issue? As a placeholder, I've put "[prospective]" at the beginning of each issue title.

I've added issues for both of the pitfalls we've identified and linked them from the pull request description. I propose using the tag "prospective" for issues that would be introduced by pull requests under review. If you like that name, could you add the tag and assign it to each pitfall issue? As a placeholder, I've put "[prospective]" at the beginning of each issue title.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

I propose using the tag "prospective" for issues that would be introduced by pull requests under review. If you like that name, could you add the tag and assign it to each pitfall issue?

Done.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2078: > I propose using the tag "prospective" for issues that would be introduced by pull requests under review. If you like that name, could you add the tag and assign it to each pitfall issue? Done.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

I've done this locally by hard-coding the label "Inversive distance" into the ObservableOutlineItem component, styled as shown in the attached screenshot. Is that good enough for this pull request? If so, I'll push it.

Sure, given that we will shortly add a radius Thermostat so that this PR is more or less just a mockup. But as a mockup, it occurs to me that once we switch to a split view of Entities in one pane and Dials in the other, then the constraints will have to be labeled something like inversive distance(Deimos, Pollux) so if you want to switch to labeling along those lines so see how it looks now, that would also be fine with me.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2075: > I've done this locally by hard-coding the label "Inversive distance" into the `ObservableOutlineItem` component, styled as shown in the attached screenshot. Is that good enough for this pull request? If so, I'll push it. Sure, given that we will shortly add a radius Thermostat so that this PR is more or less just a mockup. But as a mockup, it occurs to me that once we switch to a split view of Entities in one pane and Dials in the other, then the constraints will have to be labeled something like `inversive distance(Deimos, Pollux)` so if you want to switch to labeling along those lines so see how it looks now, that would also be fine with me.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

(c) we should contemplate what we are calling these entities in the code

I like the control theory flavor of "thermostat." Here are some more suggestions in that vein.

* [**"Regulator."**](https://en.wikipedia.org/wiki/Regulator_(automatic_control)) Many familiar regulators, including thermostats and [pressure regulators](https://en.wikipedia.org/wiki/Pressure_regulator), show both the current value and the set point of the regulated variable. Suggests a name like "gauge" for observables that can't be constrained.

* [**"Process value."**](https://en.wikipedia.org/wiki/Process_variable) Doesn't quite fit, because we're controlling a static system rather than a process.

I agree that if we were showing the values graphically, e.g. a literal pointer to indicate an angle, or a slider for a radius, or a disk/rectangle scaled to indicate an area, then it would make perfect sense to overlay the current value and the set point so that when the constraint was on and satisfied, they would line up visually.

For numerical data, that visual layout does not work as well.

I am OK with a wide-ish text box where when a constraint is on and satisfied it just shows one number, but when it is suspended or unable to be satisfied it splits left/right and shows both the set point and the current value, with say the set point in red when it is supposed to be in force but currently violated, and maybe lighter/greyed out when it is suspended from having effect. And when something is just being measured, again only the measurement shows up. This scheme would suggest that when no constraining value has been specified, the current measurement should show up where the true measurement would be in the split view, and when one has been specified and is successfully being enforced, that value should show up where the set point does in the split view. I think that was one of your mock-ups, but not the one you pushed. If you like that as a future direction for how to deal with the pitfall issues, feel free to go back to that mockup, with/without any of the other contrast elements that are present in the current PR.

On the topic of what to name the class, I am OK with any of Dial, Thermostat, or Regulator, although I will say the last one speaks to me the least, perhaps because I have never used anything called a Regulator. You should also feel free to use a nonce word of your choosing, e.g., "Conquant" or "ConQuant", for constrainable quantity or "RWValue" for "read/write value" or whatever... I just think we should use a term that is nicely agnostic as to the direction of information flow.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2074: > > (c) we should contemplate what we are calling these entities in the code > > I like the control theory flavor of "thermostat." Here are some more suggestions in that vein. > > * [**"Regulator."**](https://en.wikipedia.org/wiki/Regulator_(automatic_control)) Many familiar regulators, including thermostats and [pressure regulators](https://en.wikipedia.org/wiki/Pressure_regulator), show both the current value and the set point of the regulated variable. Suggests a name like "gauge" for observables that can't be constrained. > > * [**"Process value."**](https://en.wikipedia.org/wiki/Process_variable) Doesn't quite fit, because we're controlling a static system rather than a process. I agree that if we were showing the values graphically, e.g. a literal pointer to indicate an angle, or a slider for a radius, or a disk/rectangle scaled to indicate an area, then it would make perfect sense to overlay the current value and the set point so that when the constraint was on and satisfied, they would line up visually. For numerical data, that visual layout does not work as well. I am OK with a wide-ish text box where when a constraint is on and satisfied it just shows one number, but when it is suspended or unable to be satisfied it splits left/right and shows both the set point and the current value, with say the set point in red when it is supposed to be in force but currently violated, and maybe lighter/greyed out when it is suspended from having effect. And when something is just being measured, again only the measurement shows up. This scheme would suggest that when no constraining value has been specified, the current measurement should show up where the true measurement would be in the split view, and when one has been specified and is successfully being enforced, that value should show up where the set point does in the split view. I think that was one of your mock-ups, but not the one you pushed. If you like that as a future direction for how to deal with the pitfall issues, feel free to go back to that mockup, with/without any of the other contrast elements that are present in the current PR. On the topic of what to name the class, I am OK with any of Dial, Thermostat, or Regulator, although I will say the last one speaks to me the least, perhaps because I have never used anything called a Regulator. You should also feel free to use a nonce word of your choosing, e.g., "Conquant" or "ConQuant", for constrainable quantity or "RWValue" for "read/write value" or whatever... I just think we should use a term that is nicely agnostic as to the direction of information flow.
Author
Member

On the topic of what to name the class, I am OK with any of Dial, Thermostat, or Regulator, although I will say the last one speaks to me the least, perhaps because I have never used anything called a Regulator.

Among those three, I'd be happy with "Regulator." I wouldn't be so happy with "Dial" (because it suggests a specific visual interface design) or "Thermostat" (because I'm used to it referring specifically to a temperature regulator).

You should also feel free to use a nonce word of your choosing, e.g., "Conquant" or "ConQuant", for constrainable quantity […]

How would you feel about "Constrainable"? We could contrast this with "Observable" for measurement-only variables, since I think you said that term has a read-only connotation to you.

> On the topic of what to name the class, I am OK with any of Dial, Thermostat, or Regulator, although I will say the last one speaks to me the least, perhaps because I have never used anything called a Regulator. Among those three, I'd be happy with "Regulator." I wouldn't be so happy with "Dial" (because it suggests a specific visual interface design) or "Thermostat" (because I'm used to it referring specifically to a temperature regulator). > You should also feel free to use a nonce word of your choosing, e.g., "Conquant" or "ConQuant", for constrainable quantity […] How would you feel about "Constrainable"? We could contrast this with "Observable" for measurement-only variables, since I think you said that term has a read-only connotation to you.
Author
Member

[…] it occurs to me that once we switch to a split view of Entities in one pane and Dials in the other, then the constraints will have to be labeled something like inversive distance(Deimos, Pollux) so if you want to switch to labeling along those lines so see how it looks now, that would also be fine with me.

I'd rather explore that labeling while designing the dual list component, since it might feel different in that context.

> […] it occurs to me that once we switch to a split view of Entities in one pane and Dials in the other, then the constraints will have to be labeled something like inversive distance(Deimos, Pollux) so if you want to switch to labeling along those lines so see how it looks now, that would also be fine with me. I'd rather explore that labeling while designing the dual list component, since it might feel different in that context.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

I'd rather explore that labeling while designing the dual list component, since it might feel different in that context.

Sure. As I said, either way fine.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2089: > I'd rather explore that labeling while designing the dual list component, since it might feel different in that context. Sure. As I said, either way fine.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

On the topic of what to name the class, I am OK with any of Dial, Thermostat, or Regulator, although I will say the last one speaks to me the least, perhaps because I have never used anything called a Regulator.

Among those three, I'd be happy with "Regulator." I wouldn't be so happy with "Dial" (because it suggests a specific visual interface design) or "Thermostat" (because I'm used to it referring specifically to a temperature regulator).

You should also feel free to use a nonce word of your choosing, e.g., "Conquant" or "ConQuant", for constrainable quantity […]

How would you feel about "Constrainable"? We could contrast this with "Observable" for measurement-only variables, since I think you said that term has a read-only connotation to you.

To me, "Constrainable" falls too much on the side of just constraining. Let's just go with "Regulator" then since we both have some level of contentment with that one. We can worry about what to name a measurement-only quantity once we have any, although off the top of my head, "Gauge" seems like the read-only analogue of a "Regulator" -- certainly you can have either a pressure gauge, which will only show you the pressure, and a pressure regulator, as you mentioned.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2088: > > On the topic of what to name the class, I am OK with any of Dial, Thermostat, or Regulator, although I will say the last one speaks to me the least, perhaps because I have never used anything called a Regulator. > > Among those three, I'd be happy with "Regulator." I wouldn't be so happy with "Dial" (because it suggests a specific visual interface design) or "Thermostat" (because I'm used to it referring specifically to a temperature regulator). > > > You should also feel free to use a nonce word of your choosing, e.g., "Conquant" or "ConQuant", for constrainable quantity […] > > How would you feel about "Constrainable"? We could contrast this with "Observable" for measurement-only variables, since I think you said that term has a read-only connotation to you. To me, "Constrainable" falls too much on the side of just constraining. Let's just go with "Regulator" then since we both have some level of contentment with that one. We can worry about what to name a measurement-only quantity once we have any, although off the top of my head, "Gauge" seems like the read-only analogue of a "Regulator" -- certainly you can have either a pressure gauge, which will only show you the pressure, and a pressure regulator, as you mentioned.
Vectornaut added 3 commits 2025-02-12 19:56:09 +00:00
Right now, there's only one type of observable, so the label can be
hard-coded.
Author
Member

The pull request should be ready for review again now! I've made the following changes:

  • Renamed observables to regulators
  • Labeled the regulators in the outline view
The pull request should be ready for review again now! I've made the following changes: - Renamed observables to regulators - Labeled the regulators in the outline view
Owner

I am still confused as to why Regulators have a role. It seems to me they always measure, so having the role not include measurement seems weird, and they constrain if and only if they have a setpoint. These considerations would seem to render the role superfluous?

I am still confused as to why Regulators have a role. It seems to me they always measure, so having the role not include measurement seems weird, and they constrain if and only if they have a setpoint. These considerations would seem to render the role superfluous?
Author
Member

I am still confused as to why Regulators have a role.

Another approach I considered was changing the type of set_point to Signal<Option<f64>> and using the following logic to determine the role.

  • If set_point is Some, we're using the regulator as a constraint.
  • If set_point is None:
    • If set_point_text is the empty string, we're using the regulator as a measurement.
    • If set_point_text is non-empty, we're trying to use the regulator as a constraint, but the constraint is invalid because the set point couldn't be parsed.

I decided that explicitly storing the role would make the code easier to understand, and that decoupling the role from the contents of set_point_text would make the code more flexible and less finicky. However, it wouldn't be too hard to switch to a non-explicit approach like the one described here.

> I am still confused as to why Regulators have a role. Another approach I considered was changing the type of `set_point` to `Signal<Option<f64>>` and using the following logic to determine the role. * If `set_point` is `Some`, we're using the regulator as a constraint. * If `set_point` is `None`: * If `set_point_text` is the empty string, we're using the regulator as a measurement. * If `set_point_text` is non-empty, we're trying to use the regulator as a constraint, but the constraint is invalid because the set point couldn't be parsed. I decided that explicitly storing the role would make the code easier to understand, and that decoupling the role from the contents of `set_point_text` would make the code more flexible and less finicky. However, it wouldn't be too hard to switch to a non-explicit approach like the one described here.
Owner

In my experience, having fewer places where the same information is kept or might be perceived to be kept actually makes the code easier to understand, as well as more concise (which also helps with clarity). There are fewer ways that things might get out of sync, or conversely, less work that needs to be done to keep the data in sync. That point of view supports removing the role if that information can clearly be deduced from the set point.

And along those lines, f64s, which I am assuming are IEEE doubles, are already natively essentially Option values: NaN could very reasonably and clearly mean there is no current setting, without having to invoke the machinery (and verbosity) of Rust's Option generic.

And more importantly, it now seems to me that set_point_text is very likely a violation of the Model-View-Controller separation of concerns. A Regulator is an entity in an Assembly, which is the platonic truth of some configuration we are studying with the software, and should only contain components of that platonic truth. So, what quantities are being regulated, and what those quantities are set to, are properly part of an Assembly. But a text representation of the set point of a Regulator belongs in whatever View might want to show that set point in that way, and then that view might have an entry box that could Control that set point, but if something invalid were put in the box, it simply would not change the set point; there would be no issue of the "text of the set point having been set to an invalid value". You can see this design philosophy in frontscope: when you enter an invalid value in a param entry box, there is simply no new value sent to the actual parameter in the visualizer/sequence code.

Or to put it another way: These inversive distance Regulators might well end up represented in a graphical view by little angle indicators (at least when the magnitude of the set point is ≤ 1). And quite plausibly, we might at some point want to enable dragging on the angle indicator to change the set point, just the way you can turn a thermostat. But that wouldn't mean we would put an "AngleDisplayWidget" here in the Regulator, correct? Similarly, it appears that having a text form of the set point in here is tied too tightly to the Table view (or whatever we are currently calling the text representation of the assembly), and it appears that it is an undesirable blurring of the separation of concerns between our Model (i.e., the Assembly) and our Views and Controllers (the table and/or graphical panel).

Please consider these aspects, minimizing redundancy (corresponding to maximizing DRYness) and clean separation of concerns, and refactor the Regulator-related code accordingly.

(I of course could be mistaken and there may be some good reason why a text representation of a floating point number set point is an inherent part of the platonic assembly of entities, so happy to discuss if you feel so.)

I know it might seem like I am making a big deal out of smallish points, but I think the beginnings of things are important and a good time to get the underlying structure of the code as "right" as can be foreseen at that time.

In my experience, having fewer places where the same information is kept or might be perceived to be kept actually makes the code easier to understand, as well as more concise (which also helps with clarity). There are fewer ways that things might get out of sync, or conversely, less work that needs to be done to keep the data in sync. That point of view supports removing the role if that information can clearly be deduced from the set point. And along those lines, `f64`s, which I am assuming are IEEE doubles, are already natively essentially Option values: NaN could very reasonably and clearly mean there is no current setting, without having to invoke the machinery (and verbosity) of Rust's Option generic. And more importantly, it now seems to me that set_point_text is very likely a violation of the Model-View-Controller separation of concerns. A Regulator is an entity in an Assembly, which is the platonic truth of some configuration we are studying with the software, and should only contain components of that platonic truth. So, what quantities are being regulated, and what those quantities are set to, are properly part of an Assembly. But a text representation of the set point of a Regulator belongs in whatever View might want to show that set point in that way, and then that view might have an entry box that could Control that set point, but if something invalid were put in the box, it simply would not change the set point; there would be no issue of the "text of the set point having been set to an invalid value". You can see this design philosophy in frontscope: when you enter an invalid value in a param entry box, there is simply no new value sent to the actual parameter in the visualizer/sequence code. Or to put it another way: These inversive distance Regulators might well end up represented in a graphical view by little angle indicators (at least when the magnitude of the set point is ≤ 1). And quite plausibly, we might at some point want to enable dragging on the angle indicator to change the set point, just the way you can turn a thermostat. But that wouldn't mean we would put an "AngleDisplayWidget" here in the Regulator, correct? Similarly, it appears that having a text form of the set point in here is tied too tightly to the Table view (or whatever we are currently calling the text representation of the assembly), and it appears that it is an undesirable blurring of the separation of concerns between our Model (i.e., the Assembly) and our Views and Controllers (the table and/or graphical panel). Please consider these aspects, minimizing redundancy (corresponding to maximizing DRYness) and clean separation of concerns, and refactor the Regulator-related code accordingly. (I of course could be mistaken and there may be some good reason why a text representation of a floating point number set point is an inherent part of the platonic assembly of entities, so happy to discuss if you feel so.) I know it might seem like I am making a big deal out of smallish points, but I think the beginnings of things are important and a good time to get the underlying structure of the code as "right" as can be foreseen at that time.
Author
Member

And more importantly, it now seems to me that set_point_text is very likely a violation of the Model-View-Controller separation of concerns.

This field goes back to pull request #15, where it appears as the lorentz_prod_text field of Constraint. If we change it, I'd recommend doing it in a separate PR, since it's compatible with the changes that this PR focuses on.

According to my meeting notes from back then, we decided that the element and constraint structures should only include data that's shared across all views. As a user, I think it's helpful for a regulator's set point text to be shared across all views—both the text input views we have now and the angle display widgets we might have later. Similarly, if there's some extra data that should be shared across all angle display widgets, that extra information could also be stored in the constraint structure.

Some advantages of sharing set point text across all views

Handling invalid input transparently

Suppose I enter some invalid set point text in one view of a regulator. I'd like the invalid text to stay there so I can correct it, rather than disappearing. While the invalid text is there, is the constraint in force? If so, what set point is being used? I don't want the answers to depend on whether there are other views of the regulator, or on hidden information.

Sharing the set point text across all views accomplishes this. While the invalid text is there, the constraint is not in force, and you can see this in any view of the regulator by noticing the invalid text.

A possible alternate solution is to add a display of the actual set point to any view containing a failed attempt to enter a new set point. That would take up extra space in the interface, though.

Reducing ambiguity during text entry

If set point text isn't shared across views, typing into one view will temporarily put the interface in a state where different views show different set points. This shouldn't be a huge problem, because the views will all become consistent again when the change event fires, but I do find it somewhat annoying and confusing.

> And more importantly, it now seems to me that set_point_text is very likely a violation of the Model-View-Controller separation of concerns. This field goes back to pull request #15, where it appears as the `lorentz_prod_text` field of `Constraint`. If we change it, I'd recommend doing it in a separate PR, since it's compatible with the changes that this PR focuses on. According to my meeting notes from back then, we decided that the element and constraint structures should only include data that's shared across all views. As a user, I think it's helpful for a regulator's set point text to be shared across all views—both the text input views we have now and the angle display widgets we might have later. Similarly, if there's some extra data that should be shared across all angle display widgets, that extra information could also be stored in the constraint structure. ### Some advantages of sharing set point text across all views #### Handling invalid input transparently Suppose I enter some invalid set point text in one view of a regulator. I'd like the invalid text to stay there so I can correct it, rather than disappearing. While the invalid text is there, is the constraint in force? If so, what set point is being used? I don't want the answers to depend on whether there are other views of the regulator, or on hidden information. Sharing the set point text across all views accomplishes this. While the invalid text is there, the constraint is not in force, and you can see this in any view of the regulator by noticing the invalid text. A possible alternate solution is to add a display of the actual set point to any view containing a failed attempt to enter a new set point. That would take up extra space in the interface, though. #### Reducing ambiguity during text entry If set point text isn't shared across views, typing into one view will temporarily put the interface in a state where different views show different set points. This shouldn't be a huge problem, because the views will all become consistent again when the `change` event fires, but I do find it somewhat annoying and confusing.
Author
Member

In my experience, having fewer places where the same information is kept or might be perceived to be kept actually makes the code easier to understand, as well as more concise (which also helps with clarity).

Okay, I'll switch to a system like the one described here.

And along those lines, f64s, which I am assuming are IEEE doubles, are already natively essentially Option values: NaN could very reasonably and clearly mean there is no current setting, without having to invoke the machinery (and verbosity) of Rust's Option generic.

I think that matching an Option could make for more streamlined code in this case. I also prefer to treat NaN values as exceptions rather than using them to store information, partly because of all the caveats in the f64::NAN documentation. If you strongly prefer it, though, the NaN approach should be just as easy to implement.

> In my experience, having fewer places where the same information is kept or might be perceived to be kept actually makes the code easier to understand, as well as more concise (which also helps with clarity). Okay, I'll switch to a system like the one described [here](pulls/48/#issuecomment-2095). > And along those lines, f64s, which I am assuming are IEEE doubles, are already natively essentially Option values: NaN could very reasonably and clearly mean there is no current setting, without having to invoke the machinery (and verbosity) of Rust's Option generic. I think that matching an `Option` could make for more streamlined code in this case. I also prefer to treat NaN values as exceptions rather than using them to store information, partly because of all the caveats in the [`f64::NAN`](https://doc.rust-lang.org/std/primitive.f64.html#associatedconstant.NAN) documentation. If you strongly prefer it, though, the NaN approach should be just as easy to implement.
Owner

We had a productive discussion on what data should be in a regulator. Aaron made the point that one could reasonably specify an inversive distance as either 0.24 or .24, and it would be weird if the system changed that for you. Moreover, an angle widget might reasonably set the inversive distance to some specification like 60°, or we might at some point allow specifications like 1/√2 or √2/2, and we would not want the system to alter our input either way from one to the other. Finally, one possible future way that we may want to implement linked constraints is by allowing Scalar elements, and if we have one named t, perhaps you should be able to enter t as the value -- and we certainly don't want that to just be converted into the current float value of t, as the point is we need all the constraints that have t in them to change in concert.

These examples suggest that the set point of a regulator should be a text specification of its value, and that it should not have a floating-point set point at all; rather, the floating-point value of the set point will be computed (and perhaps cached internally if the consistency issues with that can be dealt with) when needed.

We'll try to finalize this discussion next time.

P.S. If the set point is a string, then there remains the same question as to whether the absence of a set point is the empty string, or if the set point is an Option<string>. I am guessing in this case, the practicalities will come down on the side of using the empty string for no set point.

We had a productive discussion on what data should be in a regulator. Aaron made the point that one could reasonably specify an inversive distance as either 0.24 or .24, and it would be weird if the system changed that for you. Moreover, an angle widget might reasonably set the inversive distance to some specification like 60°, or we might at some point allow specifications like 1/√2 or √2/2, and we would not want the system to alter our input either way from one to the other. Finally, one possible future way that we may want to implement linked constraints is by allowing Scalar elements, and if we have one named `t`, perhaps you should be able to enter `t` as the value -- and we certainly don't want that to just be converted into the current float value of `t`, as the point is we need all the constraints that have `t` in them to change in concert. These examples suggest that the set point of a regulator should be a text specification of its value, and that it should not have a floating-point set point at all; rather, the floating-point value of the set point will be computed (and perhaps cached internally if the consistency issues with that can be dealt with) when needed. We'll try to finalize this discussion next time. P.S. If the set point is a string, then there remains the same question as to whether the absence of a set point is the empty string, or if the set point is an `Option<string>`. I am guessing in this case, the practicalities will come down on the side of using the empty string for no set point.
Author
Member

Whoops—I've rediscovered why the role field, or something equivalent, is needed to make regulator views act the way they currently do. This logic for determining a regulator's role would work if set_point_text, like set_point, were only updated when you commit the new set point by firing a change event. However, the set_point_text signal is updated more frequently than that, because it's used to keep the set point's specification consistent across all views from keystroke to keystroke. Thus, seeing that set_point is None and set_point_text is non-empty doesn't always mean that the user has tried and failed to enter a new set point: it could also mean that the user is in the process of typing a set point for a regulator that doesn't currently have one.

Storing role information elsewhere

We could still get rid of the role field by upgrading set_point from an Option an enum with three variants:

  • One that holds a set point.
  • One that says the user tried and failed to specify a set point.
  • One that says the regulator is playing the role of a measurement, with no set point.

Behavior changes that would remove the need for role information

If we decide that the set point's specification should no longer be consistent across all views from keystroke to keystroke, maybe this logic would work. I still think that behavior change would be orthogonal to this purpose of this pull request, though.

Errata

I realized while writing this that during our meeting, and in the comments above, I kept saying "input event" when I meant "change event." I've corrected the comments.

Whoops—I've rediscovered why the `role` field, or something equivalent, is needed to make regulator views act the way they currently do. [This logic](pulls/48#issuecomment-2095) for determining a regulator's role would work if `set_point_text`, like `set_point`, were only updated when you commit the new set point by firing a `change` event. However, the `set_point_text` signal is updated more frequently than that, because it's used to keep the set point's specification consistent across all views from keystroke to keystroke. Thus, seeing that `set_point` is `None` and `set_point_text` is non-empty doesn't always mean that the user has tried and failed to enter a new set point: it could also mean that the user is in the process of typing a set point for a regulator that doesn't currently have one. #### Storing role information elsewhere We could still get rid of the `role` field by upgrading `set_point` from an `Option` an enum with three variants: * One that holds a set point. * One that says the user tried and failed to specify a set point. * One that says the regulator is playing the role of a measurement, with no set point. #### Behavior changes that would remove the need for role information If we decide that the set point's specification should no longer be consistent across all views from keystroke to keystroke, maybe [this logic](pulls/48#issuecomment-2095) would work. I still think that behavior change would be orthogonal to this purpose of this pull request, though. #### Errata I realized while writing this that during our meeting, and in the comments above, I kept saying "`input` event" when I meant "`change` event." I've corrected the comments.
Owner

Per our discussion, I feel like we are heading to a state where the set point is a string, and I thought you preferred the style of input in which each keystroke did not change the set point, but only the "commit" action - enter or change focus. Ergo, when you are typing in one control for the set point, the other displays of the set point (textual or graphical) should not change, as they are merely displaying the current set point; they are not involved in the uncompleted action of setting it. Only when you commit the change would all the other displays update. Similarly, as you are dragging a protractor widget, only that widget would show the potential new value -- only when you release the drag would the other displays update with the result of the gesture.

This is all hanging together for me, and I thought it was more or less what you were advocating. We can finalize the discussion Monday hopefully.

Per our discussion, I feel like we are heading to a state where the set point is a string, and I thought you preferred the style of input in which each keystroke did not change the set point, but only the "commit" action - enter or change focus. Ergo, when you are typing in one control for the set point, the other displays of the set point (textual or graphical) should _not_ change, as they are merely displaying the current set point; they are not involved in the uncompleted action of setting it. Only when you commit the change would all the other displays update. Similarly, as you are dragging a protractor widget, only that widget would show the potential new value -- only when you release the drag would the other displays update with the result of the gesture. This is all hanging together for me, and I thought it was more or less what you were advocating. We can finalize the discussion Monday hopefully.
Author
Member

Ergo, when you are typing in one control for the set point, the other displays of the set point (textual or graphical) should not change, as they are merely displaying the current set point; they are not involved in the uncompleted action of setting it. Only when you commit the change would all the other displays update.

That seems reasonable. Should I try to lump that behavior change into this pull request? Or should I leave the behavior and role field as-is for this pull request and open an issue (in the Demo project) for the input behavior and architecture overhaul?

> Ergo, when you are typing in one control for the set point, the other displays of the set point (textual or graphical) should not change, as they are merely displaying the current set point; they are not involved in the uncompleted action of setting it. Only when you commit the change would all the other displays update. That seems reasonable. Should I try to lump that behavior change into this pull request? Or should I leave the behavior and `role` field as-is for this pull request and open an issue (in the *Demo* project) for the input behavior and architecture overhaul?
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

That seems reasonable. Should I try to lump that behavior change into this pull request? Or should I leave the behavior and role field as-is for this pull request and open an issue (in the Demo project) for the input behavior and architecture overhaul?

I'd love for this PR to get regulators to a solid prototypical behavior for future development, so yes, lumping it in would be great.

As for validation, I think it becomes clearer in this model. We can discuss further Monday, but I would propose that each regulator knows how to validate a possible set point, and a text-entry widget for a string set point would validate (only) per keystroke, indicating in some way when the current value in the box is a valid new set point. When it's not, committing is not possible: enter "klunks" (and if we use the "blur focus to indicate successful commit" analogy, the blur would not happen, or in general, whatever the indicator of a successful commit is, it would not happen), and we would need to decide if external focus change would discard the invalid input so far, or leave the invalid input in place with the invalidity indicator, or what, but the set point would not change and any other views of the same regulator would still show the set point that is still in force). This is similar to the situation in which one drags a protractor to an invalid spot (say to its hinge, where the angle is ambiguous) -- releasing there would have no effect on the current set point.

This validation scheme has the attraction that the actual set point in the Regulator can never be set to any invalid value. That clears the way for it to potentially in some future revision be some other, perhaps more abstract type that can only take on valid values, as long as it can faithfully reproduce the string that produced it. For example, if we do eventually have some expression language for valid set points, it could be a parse tree in that language, annotated with the characters that produced each node.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2103: > That seems reasonable. Should I try to lump that behavior change into this pull request? Or should I leave the behavior and `role` field as-is for this pull request and open an issue (in the _Demo_ project) for the input behavior and architecture overhaul? I'd love for this PR to get regulators to a solid prototypical behavior for future development, so yes, lumping it in would be great. As for validation, I think it becomes clearer in this model. We can discuss further Monday, but I would propose that each regulator knows how to validate a possible set point, and a text-entry widget for a string set point would _validate_ (only) per keystroke, indicating in some way when the current value in the box is a valid new set point. When it's not, committing is not possible: enter "klunks" (and if we use the "blur focus to indicate successful commit" analogy, the blur would not happen, or in general, whatever the indicator of a successful commit is, it would not happen), and we would need to decide if external focus change would discard the invalid input so far, or leave the invalid input in place with the invalidity indicator, or what, but the set point would not change and any other views of the same regulator would still show the set point that is still in force). This is similar to the situation in which one drags a protractor to an invalid spot (say to its hinge, where the angle is ambiguous) -- releasing there would have no effect on the current set point. This validation scheme has the attraction that the actual set point in the Regulator can never be set to any invalid value. That clears the way for it to potentially in some future revision be some other, perhaps more abstract type that can only take on valid values, as long as it can faithfully reproduce the string that produced it. For example, if we do eventually have some expression language for valid set points, it could be a parse tree in that language, annotated with the characters that produced each node.
Owner

Just for reference/comparison on this aspect, both GeoGebra and Desmos allow you to set the coordinates of a point to a decimal string, and GeoGebra then canonicalizes that string (by eg. inserting a leading zero before the decimal place if there isn't one) but Desmos does not; it preserves your exact input. GeoGebra allows exponential notation like 3E-4 for 0.0003, Desmos does not (it creates a slider named E and sets that slider to an initial value of 1, so you get a coordinate intially equal to minus one but controllable by the E slider).

Amusingly enough in GeoGebra when you enter 3E-4 it canonicalizes it for display to 0.0003, but if you then re-focus that text entry field for further typing, it is pre-filled with 3*10^(-4) -- so it is clearly keeping both a "definition string" and an "actual value", but given that, it's weird that it doesn't preserve your exact input. So the canonicalization is occurring to the "definition string", and then the values are simply shown in the current preferred format (you can set that format to some extent, e.g. the number of digits; if you enter more digits than the current format, it keeps them in the definition string but rounds in the actual value display). And note that you can toggle the display of the elements between their actual values and their definition strings. But even in the latter case, canonicalization occurs from what you actually typed to the canonical definition string, as soon as you hit enter.

So if we do at some point have a mode in which you can display both set points and actual values, it would be OK if a particular regulator showed .37 as its set point -- the analogue of GeoGebra's "definition string" for a Regulator -- if that's what the person typed, but 0.3700000001 (or 0.37) as its actual value; I think it is perfectly OK to have a canonical format to present result values, that does not need to depend on the definitions of entities.

I didn't check any other systems in this very brief survey of the field.

Just for reference/comparison on this aspect, both GeoGebra and Desmos allow you to set the coordinates of a point to a decimal string, and GeoGebra then canonicalizes that string (by eg. inserting a leading zero before the decimal place if there isn't one) but Desmos does not; it preserves your exact input. GeoGebra allows exponential notation like `3E-4` for 0.0003, Desmos does not (it creates a slider named `E` and sets that slider to an initial value of 1, so you get a coordinate intially equal to minus one but controllable by the E slider). Amusingly enough in GeoGebra when you enter `3E-4` it canonicalizes it for _display_ to `0.0003`, but if you then re-focus that text entry field for further typing, it is pre-filled with `3*10^(-4)` -- so it is clearly keeping both a "definition string" and an "actual value", but given that, it's weird that it doesn't preserve your exact input. So the canonicalization is occurring to the "definition string", and then the values are simply shown in the current preferred format (you can set that format to some extent, e.g. the number of digits; if you enter more digits than the current format, it keeps them in the definition string but rounds in the actual value display). And note that you can toggle the display of the elements between their actual values and their definition strings. But even in the latter case, canonicalization occurs from what you actually typed to the canonical definition string, as soon as you hit enter. So if we do at some point have a mode in which you can display both set points and actual values, it would be OK if a particular regulator showed .37 as its set point -- the analogue of GeoGebra's "definition string" for a Regulator -- if that's what the person typed, but 0.3700000001 (or 0.37) as its actual value; I think it is perfectly OK to have a canonical format to present result values, that does not need to depend on the definitions of entities. I didn't check any other systems in this very brief survey of the field.
Owner

Not for this PR, but for the future on the point of auto-change-per-keystroke which I understand your reservations about -- if we have the ability, it could well be that a control widget in the midst of making a change to a Regulator, with a current uncommitted input that is nevertheless currently valid, could initiate a trial recompute and when that completes, if the current state of the control widget corresponds to the same value and is still uncommitted, is displayed as a preview in some way -- perhaps a ghost of the new configuration, either on its own or overlaid on the current configuration. That could be super useful. It answers a number of your reservations except for "it makes my computer thrash", but I think at some point we should still try such previewing, and throttle its resource consumption if necessary. GeoGebra, for example, does in many but not all circumstances, do such previewing. If we did it, we would want to be more consistent.

(In slightly more detail, it seems GeoGebra will preview when the current input constructs a new entity without changing any existing ones. So if I type A = (1,1) and hit enter and then type Circle(A, 3) even before I hit enter a preview circle of radius 3 about (1,1) appears, and if I backspace and change it to Circle(A, 2) the preview changes immediately. Then if I hit enter, the new circle is truly created and the display changes a bit accordingly. But now if I type A=(2,3) there is no preview, even though if I hit enter the point A moves and the circle is re-drawn with its new center.)

Not for this PR, but for the future on the point of auto-change-per-keystroke which I understand your reservations about -- if we have the ability, it could well be that a control widget in the midst of making a change to a Regulator, with a current uncommitted input that is nevertheless currently valid, could initiate a trial recompute and when that completes, if the current state of the control widget corresponds to the same value and is still uncommitted, is displayed as a preview in some way -- perhaps a ghost of the new configuration, either on its own or overlaid on the current configuration. That could be super useful. It answers a number of your reservations except for "it makes my computer thrash", but I think at some point we should still try such previewing, and throttle its resource consumption if necessary. GeoGebra, for example, does in many but not all circumstances, do such previewing. If we did it, we would want to be more consistent. (In slightly more detail, it seems GeoGebra will preview when the current input constructs a new entity without changing any existing ones. So if I type `A = (1,1)` and hit enter and then type `Circle(A, 3)` even before I hit enter a preview circle of radius 3 about (1,1) appears, and if I backspace and change it to `Circle(A, 2)` the preview changes immediately. Then if I hit enter, the new circle is truly created and the display changes a bit accordingly. But now if I type `A=(2,3)` there is no preview, even though if I hit enter the point A moves and the circle is re-drawn with its new center.)
Author
Member

As for validation, I think it becomes clearer in this model. We can discuss further Monday, but I would propose that each regulator knows how to validate a possible set point, and a text-entry widget for a string set point would validate (only) per keystroke, indicating in some way when the current value in the box is a valid new set point. When it's not, committing is not possible: enter "klunks" (and if we use the "blur focus to indicate successful commit" analogy, the blur would not happen, or in general, whatever the indicator of a successful commit is, it would not happen), and we would need to decide if external focus change would discard the invalid input so far, or leave the invalid input in place with the invalidity indicator, or what, but the set point would not change and any other views of the same regulator would still show the set point that is still in force).

I like the way Desmos handles validation. When you enter an invalid expression, like y=x&, it stays in the expression list with an invalidity indicator next to it, and it doesn't produce any output. Editing an expression from valid to invalid removes its output.

Geogebra behaves similarly, although not as consistently. You can see this behavior by entering the expressions A=(1,0), B=(0,1), Circl(A,B) in the algebra view (including the deliberate typo in the third expression).

> As for validation, I think it becomes clearer in this model. We can discuss further Monday, but I would propose that each regulator knows how to validate a possible set point, and a text-entry widget for a string set point would validate (only) per keystroke, indicating in some way when the current value in the box is a valid new set point. When it's not, committing is not possible: enter "klunks" (and if we use the "blur focus to indicate successful commit" analogy, the blur would not happen, or in general, whatever the indicator of a successful commit is, it would not happen), and we would need to decide if external focus change would discard the invalid input so far, or leave the invalid input in place with the invalidity indicator, or what, but the set point would not change and any other views of the same regulator would still show the set point that is still in force). I like the way Desmos handles validation. When you enter an invalid expression, like `y=x&`, it stays in the expression list with an invalidity indicator next to it, and it doesn't produce any output. Editing an expression from valid to invalid removes its output. Geogebra behaves similarly, although not as consistently. You can see this behavior by entering the expressions `A=(1,0)`, `B=(0,1)`, `Circl(A,B)` in the algebra view (including the deliberate typo in the third expression).
Owner

That's perfectly reasonable, and I think consistent with the above.

That's perfectly reasonable, and I think consistent with the above.
Author
Member

I've now implemented this behavior change in a way that I think is consistent with our meeting discussions on February 13 and 17. The implementation is on the branch regulators-always-valid in my version of the repository. Once you try it out and confirm that it's working the way you want, I'll fast-forward the pull request branch to match it.

Summary of new behavior

  • A regulator's set point has type Option<f64>.
  • A regulator's set point specification is a string that provides a valid specification of the set point.
    • If the set point is Some(x), the string parses to the floating-point value x.
    • If the set point is None, the specification is empty.
  • When you enter a valid set point specification into a RegulatorInput, the regulator's set point gets updated, and all RegulatorInput views of the same regulator update with it.
  • When you enter an invalid set point specification into a RegulatorInput, the regulator's set point doesn't change. The input value is marked as invalid with an indicator icon and a distinctive color.
  • When a RegulatorInput has focus, pressing escape resets its value to the regulator's set point specification.
    • This ameliorates the hidden state problem, because the invalidity indicator tells you that there's hidden state, and resetting the input gives you a way to show the state.
    • I don't think this is a great way of dealing with hidden state in the long term, but I think it's good enough for a tech demo.
I've now implemented [this behavior change](https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2103) in a way that I think is consistent with our meeting discussions on February 13 and 17. The implementation is on the branch [`regulators-always-valid`](https://code.studioinfinity.org/Vectornaut/dyna3/src/branch/regulators-always-valid) in my version of the repository. Once you try it out and confirm that it's working the way you want, I'll fast-forward the pull request branch to match it. #### Summary of new behavior - A regulator's set point has type `Option<f64>`. - A regulator's set point specification is a string that provides a valid specification of the set point. - If the set point is `Some(x)`, the string parses to the floating-point value `x`. - If the set point is `None`, the specification is empty. - When you enter a valid set point specification into a `RegulatorInput`, the regulator's set point gets updated, and all `RegulatorInput` views of the same regulator update with it. - When you enter an invalid set point specification into a `RegulatorInput`, the regulator's set point doesn't change. The input value is marked as invalid with an indicator icon and a distinctive color. - When a `RegulatorInput` has focus, pressing *escape* resets its value to the regulator's set point specification. - This ameliorates the hidden state problem, because the invalidity indicator tells you that there's hidden state, and resetting the input gives you a way to show the state. - I don't think this is a great way of dealing with hidden state in the long term, but I think it's good enough for a tech demo.
Owner

Excellent, thanks. I'd first like to get straight the Regulator interface. Am I more or less correct in thinking of the pub items in the Regulator struct as the public member data, the other items in the Regulator struct as private member data, and the pub functions in the Regulator impl as the Regulator public methods?

Presuming so, I understand the subjects and measurement, and the validating try_specify_set_point method (with its slightly verbose name; I would have gravitated to something like set, or try_set to emphasize it could fail). But I am a bit unclear on the other data/methods. First, I think it is much clearer to think of the specification as the set point -- that's what you've "punched into the thermostat" -- and the resulting f64 as say the set_point_value, so I am going to use those terms and you can consider whether that renaming would make the Regulator structure and operation clearer. Clearly the engine needs to be able to get the set_point_value, and oh, I guess a graphical widget that wants to display the set point of the Regulator as a calipers or a slider bar needs the set_point_value. So OK, that's a public read-only value, makes sense.

But anything that wants a textual display of the Regulator is interested in the set point itself, and is likely not to care at all about the set_point_value. So shouldn't the presence/absence of a set point be encoded in the set_point itself (the thing you are now calling set_point_spec) and not in the set_point_value (the thing you are now calling the set_point)? Why make a text-only control of the regulator need to deal with the set_point_value at all? I don't see generally speaking how it cares.

So if the source of knowledge of whether there is a set point becomes the set_point itself, then set_point_value could be simplified to a ReadSignal<f64>. And then we would need to decide how the presence/absence of a set_point is encoded in the set_point entity, currently a String. My gut is whether the string is empty or non-empty, but maybe you have reasons to prefer an Option the way you preferred an Option over using NaN? Anyhow, let me know your thoughts on that.

(As an aside, I am unclear on how the setting of the private data currently called set_point_writable is linked to clients getting the value from the public ReadSignal currently called set_point -- I'd be happy to be illuminated on this, although it's not explicitly germane to the design issue here. I am similarly unclear how the engine gets the measurement into a Regulator when that's a ReadSignal with no apparent affordance to write to it...)

And so that just leaves: how do clients get what I am calling the set_point (the string) and notifications when it updates? This certainly has to be read-only access; to update the set_point, clients must go through try_specify_set_point. I am assuming that's what the two methods get_set_point_spec_clone and get_set_point_spec_clone_untracked accomplish, but I am unclear on how this mechanism works and why two methods are needed. From my naive not-yet-familiar-with-Rust (or the reactive programming package we are working with) perspective I would have just expected something like a ReadSignal<String> or maybe a ReadSignal<Option<String>> if it's deemed better to use the Option to encode whether there is a set point at all. Thanks for some explanation of how this all works.

Looking forward to understanding this all better; if you want to wait to discuss verbally on Thurs rather than write a long text response, that's fine.

Excellent, thanks. I'd first like to get straight the Regulator interface. Am I more or less correct in thinking of the pub items in the Regulator struct as the public member data, the other items in the Regulator struct as private member data, and the pub functions in the Regulator impl as the Regulator public methods? Presuming so, I understand the subjects and measurement, and the validating `try_specify_set_point` method (with its slightly verbose name; I would have gravitated to something like `set`, or `try_set` to emphasize it could fail). But I am a bit unclear on the other data/methods. First, I think it is much clearer to think of the _specification_ as the set point -- that's what you've "punched into the thermostat" -- and the resulting f64 as say the `set_point_value`, so I am going to use those terms and you can consider whether that renaming would make the Regulator structure and operation clearer. Clearly the engine needs to be able to get the set_point_value, and oh, I guess a graphical widget that wants to display the set point of the Regulator as a calipers or a slider bar needs the set_point_value. So OK, that's a public read-only value, makes sense. But anything that wants a textual display of the Regulator is interested in the set point itself, and is likely not to care at all about the set_point_value. So shouldn't the presence/absence of a set point be encoded in the set_point itself (the thing you are now calling set_point_spec) and not in the set_point_value (the thing you are now calling the set_point)? Why make a text-only control of the regulator need to deal with the set_point_value at all? I don't see generally speaking how it cares. So if the source of knowledge of whether there is a set point becomes the set_point itself, then set_point_value could be simplified to a `ReadSignal<f64>`. And then we would need to decide how the presence/absence of a set_point is encoded in the set_point entity, currently a String. My gut is whether the string is empty or non-empty, but maybe you have reasons to prefer an Option<String> the way you preferred an Option<f64> over using NaN? Anyhow, let me know your thoughts on that. (As an aside, I am unclear on how the setting of the private data currently called set_point_writable is linked to clients getting the value from the public ReadSignal currently called set_point -- I'd be happy to be illuminated on this, although it's not explicitly germane to the design issue here. I am similarly unclear how the engine gets the measurement into a Regulator when that's a ReadSignal with no apparent affordance to write to it...) And so that just leaves: how do clients get what I am calling the set_point (the string) and notifications when it updates? This certainly has to be read-only access; to update the set_point, clients _must_ go through try_specify_set_point. I am assuming that's what the two methods `get_set_point_spec_clone` and `get_set_point_spec_clone_untracked` accomplish, but I am unclear on how this mechanism works and why two methods are needed. From my naive not-yet-familiar-with-Rust (or the reactive programming package we are working with) perspective I would have just expected something like a `ReadSignal<String>` or maybe a `ReadSignal<Option<String>>` if it's deemed better to use the Option to encode whether there is a set point at all. Thanks for some explanation of how this all works. Looking forward to understanding this all better; if you want to wait to discuss verbally on Thurs rather than write a long text response, that's fine.
Author
Member

Setting regulators' private fields

Am I more or less correct in thinking of the pub items in the Regulator struct as the public member data [fields], the other items in the Regulator struct as private member data [fields], and the pub functions in the Regulator impl as the Regulator public methods?

Yes, that's correct. Note that private items are visible throughout the module where they're defined (in contrast to the convention I'm most used to, where a structure's private items are only visible from its own methods). The function insert_new_regulator, which is in the assembly module but isn't a method of Regulator, takes advantage of this when it initializes a new Regulator, including the private fields. Outside the assembly module, it should be impossible to create a Regulator this way—a detail that will be relevant to your question about how set_point_writable is linked to set_point.

I would have gravitated to something like set, or try_set

A shorter name would be fine with me. Maybe we could use try_specify for the method that takes a new set point specification, leaving set available for a future method that would take a new set point value. This future method might be used, for example, by graphical views of the regulator.

Set point specifications and values

First, I think it is much clearer to think of the specification as the set point -- that's what you've "punched into the thermostat" -- and the resulting f64 as say the set_point_value

I've been imagining that most internal operations will use the set point value, because I expect it to be more canonical and easier to extract information from. The set point specification, in contrast, might require complicated parsing to extract information (as it already does) and allow many ways of expressing the same set point value (although it doesn't yet).

I am going to use those terms and you can consider whether that renaming would make the Regulator structure and operation clearer

To avoid confusion in references to the current source code, I'll keep using the field names set_point and set_point_spec in this comment.

But anything that wants a textual display of the Regulator is interested in the set point [specification] itself, and is likely not to care at all about the set_point_value. So shouldn't the presence/absence of a set point be encoded in the set_point itself (the thing you are now calling set_point_spec) and not in the set_point_value (the thing you are now calling the set_point)? Why make a text-only control of the regulator need to deal with the set_point_value at all? I don't see generally speaking how it cares.

Yes, the set point specification currently encodes whether or not there is a set point, as explained at the end of this section. If you'd like, I could rewrite the text-only RegulatorInput view to avoid reading from set_point: I'd just have to rewrite this closure to check whether set_point_spec is empty instead of checking whether set_point is None.

I chose the current version because I think it more clearly communicates that we're checking whether or not there's a set point. I don't think it costs us anything, because passing the whole regulator as an argument to RegulatorInput makes sense to me—and if we have it, we might as well take advantage of all its public data.

So if the source of knowledge of whether there is a set point becomes the set_point itself, then set_point_value could be simplified to a ReadSignal<f64>.

Could you say more about what simplification you're proposing here? For me, two possibilities come to mind:

  • Using NaN values rather than None to record the absence of a set point, as previously proposed.
  • Allowing set_point to have a value even when set_point_spec specifies that there is no set point.

However, neither of these feels like a simplification to me.

And then we would need to decide how the presence/absence of a set_point is encoded in the set_point entity, currently a String. My gut is whether the string is empty or non-empty

Yes, that's the scheme used in the current revision. It's described, somewhat implicitly, in my last comment and in the source code.

Getting regulators' private fields

I am unclear on how the setting of the private data currently called set_point_writable is linked to clients getting the value from the public ReadSignal currently called set_point

As the Sycamore documentation explains: "A ReadSignal can be simply obtained by dereferencing a Signal. In fact, every Signal is a ReadSignal with additional write abilities!" The set_point field always holds the underlying ReadSignal obtained by dereferencing set_point_writable. This invariant is enforced by the factory method insert_new_regulator, which is supposed to be the only way to create a Regulator from outside of the assembly module.

And so that just leaves: how do clients get what I am calling the set_point (the string) and notifications when it updates? This certainly has to be read-only access; to update the set_point, clients must go through try_specify_set_point. I am assuming that's what the two methods get_set_point_spec_clone and get_set_point_spec_clone_untracked accomplish, but I am unclear on how this mechanism works and why two methods are needed.

Sycamore's ReadSignal type provides several access methods. They come in pairs: a reactive version with a basic name and a non-reactive version with _untracked at the end of the name. Right now, the only methods we use to access a regulator's set point specification are get_clone and get_clone_untracked, so I found it convenient to write wrappers for these access methods: get_set_point_spec_clone and get_set_point_spec_clone_untracked.

From my naive not-yet-familiar-with-[Sycamore] perspective I would have just expected something like a ReadSignal or maybe a ReadSignal<Option> if it's deemed better to use the Option to encode whether there is a set point at all. Thanks for some explanation of how this all works.

Yes, exposing the set point specification's underlying ReadSignal as a public field might be a cleaner solution. It would match what we currently do for the set point value.

### Setting regulators' private fields > Am I more or less correct in thinking of the pub items in the Regulator struct as the public member data [*fields*], the other items in the Regulator struct as private member data [*fields*], and the pub functions in the Regulator impl as the Regulator public methods? Yes, that's correct. Note that private items are [visible throughout the module](https://doc.rust-lang.org/reference/visibility-and-privacy.html#r-vis.access) where they're defined (in contrast to the convention I'm most used to, where a structure's private items are only visible from its own methods). The function `insert_new_regulator`, which is in the `assembly` module but isn't a method of `Regulator`, takes advantage of this when it [initializes a new `Regulator`](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/assembly.rs#L260-L266), including the private fields. Outside the `assembly` module, it should be impossible to create a `Regulator` this way—a detail that will be relevant to your question about how `set_point_writable` is linked to `set_point`. > I would have gravitated to something like `set`, or `try_set` A shorter name would be fine with me. Maybe we could use `try_specify` for the method that takes a new set point specification, leaving `set` available for a future method that would take a new set point value. This future method might be used, for example, by graphical views of the regulator. ### Set point specifications and values > First, I think it is much clearer to think of the specification as the set point -- that's what you've "punched into the thermostat" -- and the resulting f64 as say the `set_point_value` I've been imagining that most internal operations will use the set point value, because I expect it to be more canonical and easier to extract information from. The set point specification, in contrast, might require complicated parsing to extract information (as it already does) and allow many ways of expressing the same set point value (although it doesn't yet). > I am going to use those terms and you can consider whether that renaming would make the Regulator structure and operation clearer To avoid confusion in references to the current source code, I'll keep using the field names `set_point` and `set_point_spec` in this comment. > But anything that wants a textual display of the Regulator is interested in the set point [*specification*] itself, and is likely not to care at all about the set_point_value. So shouldn't the presence/absence of a set point be encoded in the set_point itself (the thing you are now calling set_point_spec) and not in the set_point_value (the thing you are now calling the set_point)? Why make a text-only control of the regulator need to deal with the set_point_value at all? I don't see generally speaking how it cares. Yes, the set point specification currently encodes whether or not there is a set point, as explained at the end of this section. If you'd like, I could rewrite the text-only `RegulatorInput` view to avoid reading from `set_point`: I'd just have to rewrite [this closure](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/outline.rs#L41-L50) to check whether `set_point_spec` is empty instead of checking whether `set_point` is `None`. I chose the current version because I think it more clearly communicates that we're checking whether or not there's a set point. I don't think it costs us anything, because passing the whole regulator as an argument to `RegulatorInput` makes sense to me—and if we have it, we might as well take advantage of all its public data. > So if the source of knowledge of whether there is a set point becomes the set_point itself, then set_point_value could be simplified to a `ReadSignal<f64>`. Could you say more about what simplification you're proposing here? For me, two possibilities come to mind: - Using `NaN` values rather than `None` to record the absence of a set point, as previously proposed. - Allowing `set_point` to have a value even when `set_point_spec` specifies that there is no set point. However, neither of these feels like a simplification to me. > And then we would need to decide how the presence/absence of a set_point is encoded in the set_point entity, currently a String. My gut is whether the string is empty or non-empty Yes, that's the scheme used in the current revision. It's described, somewhat implicitly, in [my last comment](https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2143) and in [the source code](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/assembly.rs#L114-L121). ### Getting regulators' private fields > I am unclear on how the setting of the private data currently called set_point_writable is linked to clients getting the value from the public ReadSignal currently called set_point As the Sycamore [documentation](https://docs.rs/sycamore/latest/sycamore/reactive/struct.ReadSignal.html) explains: "A `ReadSignal` can be simply obtained by dereferencing a `Signal`. In fact, every `Signal` is a `ReadSignal` with additional write abilities!" The `set_point` field always holds the underlying `ReadSignal` obtained by dereferencing `set_point_writable`. This invariant is [enforced](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/assembly.rs#L258-L266) by the factory method `insert_new_regulator`, which is supposed to be the only way to create a `Regulator` from outside of the `assembly` module. > And so that just leaves: how do clients get what I am calling the set_point (the string) and notifications when it updates? This certainly has to be read-only access; to update the set_point, clients must go through try_specify_set_point. I am assuming that's what the two methods get_set_point_spec_clone and get_set_point_spec_clone_untracked accomplish, but I am unclear on how this mechanism works and why two methods are needed. Sycamore's `ReadSignal` type provides several access methods. They come in pairs: a reactive version with a basic name and a non-reactive version with `_untracked` at the end of the name. Right now, the only methods we use to access a regulator's set point specification are [`get_clone`](https://docs.rs/sycamore/latest/sycamore/reactive/struct.ReadSignal.html#method.get_clone) and [`get_clone_untracked`](https://docs.rs/sycamore/latest/sycamore/reactive/struct.ReadSignal.html#method.get_clone_untracked), so I found it convenient to write wrappers for these access methods: `get_set_point_spec_clone` and `get_set_point_spec_clone_untracked`. > From my naive not-yet-familiar-with-[*Sycamore*] perspective I would have just expected something like a ReadSignal<String> or maybe a ReadSignal<Option<String>> if it's deemed better to use the Option to encode whether there is a set point at all. Thanks for some explanation of how this all works. Yes, exposing the set point specification's underlying `ReadSignal` as a public field might be a cleaner solution. It would match what we currently do for the set point value.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

leaving set available for a future method that would take a new set point value. This future method might be used, for example, by graphical views of the regulator.

No.

I think we continue to be struggling with a possible non-DRY mental model of a Regulator. For the design to be maintainable in the long run, there can only be one authoritative location of the truth of what a Regulator is set to. You have convinced me, by the 0.24 vs .24 example (which I assume is already in effect), that such location cannot be an f64 -- it must be a parse tree or a string or a RegSpec or something. Therefore, any f64 accessible from a Regulator is only an efficiency/convenience read-only reflection of the underlying truth. (And yes, of course we do need a way to get that f64 that doesn't mean re-interpreting the authoritative set point every time.)

Therefore, there can never be any way to directly modify the f64 value of the set point of a Regulator. There could, at most, be a convenience method that converts an f64 in some canonical way into a candidate for being the authoritative set point of a Regulator.

So, a graphical control can take advantage of reading the f64 to decide what pixels to paint, but when a geometer person makes a gesture on that control that should be interpreted as modifying the set point, the graphical control must at that point create a new parse tree or string or RegSpec or whatever entity is an instance of the type of the authoritative set point, and attempt to set the Regulator to that. (This will likely in practice have some consequences along the lines of whenever you use a graphical control, the new set point is of the form 0.35, even if it had been .6 before.)

Is this making sense? Again, we can continue the discussion in person if it will be more productive. Thanks for bearing with it!

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2145: > leaving `set` available for a future method that would take a new set point value. This future method might be used, for example, by graphical views of the regulator. No. I think we continue to be struggling with a possible non-DRY mental model of a Regulator. For the design to be maintainable in the long run, there can only be one authoritative location of the truth of what a Regulator is set to. You have convinced me, by the 0.24 vs .24 example (which I assume is already in effect), that such location cannot be an f64 -- it must be a parse tree or a string or a RegSpec or something. Therefore, any f64 accessible from a Regulator is only an efficiency/convenience read-only reflection of the underlying truth. (And yes, of course we do need a way to get that f64 that doesn't mean re-interpreting the authoritative set point every time.) Therefore, there can never be any way to directly modify the f64 value of the set point of a Regulator. There could, at most, be a convenience method that converts an f64 in some canonical way into a candidate for being the authoritative set point of a Regulator. So, a graphical control can take advantage of reading the f64 to decide what pixels to paint, but when a geometer person makes a gesture on that control that should be interpreted as modifying the set point, the graphical control must at that point create a new parse tree or string or RegSpec or whatever entity is an instance of the type of the authoritative set point, and attempt to set the Regulator to that. (This will likely in practice have some consequences along the lines of whenever you use a graphical control, the new set point is of the form 0.35, even if it had been .6 before.) Is this making sense? Again, we can continue the discussion in person if it will be more productive. Thanks for bearing with it!
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

I think it more clearly communicates that we're checking whether or not there's a set point. I don't think it costs us anything, because passing the whole regulator as an argument to RegulatorInput makes sense to me—and if we have it, we might as well take advantage of all its public data.

I disagree on both counts. It's the spec that is the set point, so looking at it is the clearest and cleanest way to check whether there is a set point. And if you can look at just one bit of a structure to do your work, that's always better than "taking advantage of all its public data". For one thing, it makes future refactors more convenient.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2145: > I think it more clearly communicates that we're checking whether or not there's a set point. I don't think it costs us anything, because passing the whole regulator as an argument to `RegulatorInput` makes sense to me—and if we have it, we might as well take advantage of all its public data. I disagree on both counts. It's the spec that **is** the set point, so looking at it is the clearest and cleanest way to check whether there is a set point. And if you can look at just one bit of a structure to do your work, that's always better than "taking advantage of all its public data". For one thing, it makes future refactors more convenient.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

Could you say more about what simplification you're proposing here?

The simplification is precisely: only having one way to check whether there is a set point. If that way is checking whether the specification of the set point is empty, then that is the way. And if that's the way, then making the value of the set point be an Option<f64> is non-useful redundancy at best and confusing at worst: could a regulator have a set point and yet not have a value? Or vice-versa? A future coder who is not you could rightly worry about such possibilities.

So if no set point \equiv "set point specification is empty," then an f64 for the value suffices, and Option<f64> is in any situation less simple than f64 when an f64 suffices. As for how to interpret said f64, the interpretation is: if there is no set point, the f64 is meaningless. if there is a set point, the f64 is the corresponding value for the Q-product of the subjects implied by the set point. Hence, there is no need to worry about what the f64 might or might not be when there is no set point; it's simply moot.

If we had gone down a path like GeoGebra where it was OK for the system to canonicalize a widget input of .24 to an f64 printed representation like 0.24, then Option<f64> might be a perfectly reasonable type for the actual set point -- it would need a bit for whether it has been set or not, which bit is supplied by the Option -- you made a plausible case that isNaN may in some ways be a less good source for that bit. And in that world, we would definitely not have a string in the Regulator, except possibly for a read-only "printed_form" string that was completely determined by the set point (and so could not have a 0.24/.24 alternation).

But that's not the decision we took, and I am comfortable in a world in which the set point is some other specification. As I keep saying, it's reasonable for the set point to be what you "type into the thermostat" -- but then we need to keep clear and clean that said data item is the set point, and other information from the Regulator consists just of read-only consequences of that setting.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2145: > Could you say more about what simplification you're proposing here? The simplification is precisely: only having one way to check whether there is a set point. If that way is checking whether the specification of the set point is empty, then that _is_ the way. And if that's the way, then making the value of the set point be an `Option<f64>` is non-useful redundancy at best and confusing at worst: could a regulator have a set point and yet not have a value? Or vice-versa? A future coder who is not you could rightly worry about such possibilities. So if no set point $\equiv$ "set point specification is empty," then an f64 for the value suffices, and `Option<f64>` is in any situation less simple than f64 when an f64 suffices. As for how to interpret said f64, the interpretation is: if there is no set point, the f64 is meaningless. if there is a set point, the f64 is the corresponding value for the Q-product of the subjects implied by the set point. Hence, there is no need to worry about what the f64 might or might not be when there is no set point; it's simply moot. If we had gone down a path like GeoGebra where it was OK for the system to canonicalize a widget input of .24 to an f64 printed representation like 0.24, then `Option<f64>` might be a perfectly reasonable type for _the actual set point_ -- it would need a bit for whether it has been set or not, which bit is supplied by the Option -- you made a plausible case that `isNaN` may in some ways be a less good source for that bit. And in that world, we would definitely not have a string in the Regulator, except possibly for a read-only "printed_form" string that was completely determined by the set point (and so could not have a 0.24/.24 alternation). But that's not the decision we took, and I am comfortable in a world in which _the set point_ is some other specification. As I keep saying, it's reasonable for _the set point_ to be what you "type into the thermostat" -- but then we need to keep clear and clean that said data item is _the set point_, and other information from the Regulator consists just of read-only consequences of that setting.
Author
Member

For the design to be maintainable in the long run, there can only be one authoritative location of the truth of what a Regulator is set to. […] Therefore, any f64 accessible from a Regulator is only an efficiency/convenience read-only reflection of the underlying truth. […] Therefore, there can never be any way to directly modify the f64 value of the set point of a Regulator. There could, at most, be a convenience method that converts an f64 in some canonical way into a candidate for being the authoritative set point of a Regulator. […] Is this making sense?

Yes, that makes sense. In the next revision of the code, I'll rename fields along the lines you suggested earlier to reflect this point of view. How about these names?

Old name New name
set_point_spec set_point
set_point set_point_parsed

Advantages of _parsed:

  • It emphasizes that set_point_parsed is a transformation of the set point specification, cached for convenience.
  • It makes room for other cached reflections of the set point specification. For example, in the future set_point_parsed might become a tree, and set_point_evaluated could be a floating-point value that approximates the evaluation of that tree.

If we go with set_point_value, then at some point we should change the suffix _val that I currently use to distinguish the contents of a Sycamore signal from the signal itself. We could switch to _eval, for example.

> For the design to be maintainable in the long run, there can only be one authoritative location of the truth of what a Regulator is set to. […] Therefore, any f64 accessible from a Regulator is only an efficiency/convenience read-only reflection of the underlying truth. […] Therefore, there can never be any way to directly modify the f64 value of the set point of a Regulator. There could, at most, be a convenience method that converts an f64 in some canonical way into a candidate for being the authoritative set point of a Regulator. […] Is this making sense? Yes, that makes sense. In the next revision of the code, I'll rename fields along the lines you suggested earlier to reflect this point of view. How about these names? Old name | New name ---|--- `set_point_spec` | `set_point` `set_point` | `set_point_parsed` Advantages of `_parsed`: - It emphasizes that `set_point_parsed` is a transformation of the set point specification, cached for convenience. - It makes room for other cached reflections of the set point specification. For example, in the future `set_point_parsed` might become a tree, and `set_point_evaluated` could be a floating-point value that approximates the evaluation of that tree. If we go with `set_point_value`, then at some point we should change the suffix `_val` that I currently use to distinguish the contents of a Sycamore signal from the signal itself. We could switch to `_eval`, for example.
Owner

I'm cool with those names -- I'll leave it up to you to decide whether it makes sense to just go to set_point_evaluated now and if we want to introduce something like a tree, use the name set_point_parsed later (the name does strongly suggest the value will be some sort of abstract syntax tree). Of course, it could also be that the set_point itself might in the future go from an unstructured string to an AST -- there's no reason its authoritative representation has to be a plain string, as long as the clients that need a string form can obtain the string from the set_point.

Aside: If we go that route, we may run into the canonicalization question again: if a geometer enters 1/ √2 as the set point and hits enter, is it OK if that auto-changes to 1 / √2, say? And note that even if we don't want this kind of canonicalization, the set_point still might not need to be just an unstructured string, as long as the exact input string is recoverable from the AST (maybe leaves are annotated with the exact characters that created them, and a concatenating traversal reconstructs the string; or maybe every node is annotated, and so the annotation of the root is the original string...).

So anyhow, at the moment I am mellow about any names you prefer as long as they make it clear to the code reader which exact piece of information is the authoritative set point.

I'm cool with those names -- I'll leave it up to you to decide whether it makes sense to just go to set_point_evaluated now and if we want to introduce something like a tree, use the name set_point_parsed later (the name does strongly suggest the value will be some sort of abstract syntax tree). Of course, it could also be that the set_point itself might in the future go from an unstructured string to an AST -- there's no reason its authoritative representation has to be a plain string, as long as the clients that need a string form can obtain the string from the set_point. Aside: If we go that route, we may run into the canonicalization question again: if a geometer enters `1/ √2` as the set point and hits enter, is it OK if that auto-changes to `1 / √2`, say? And note that even if we don't want this kind of canonicalization, the set_point still might not need to be just an unstructured string, as long as the exact input string is recoverable from the AST (maybe leaves are annotated with the exact characters that created them, and a concatenating traversal reconstructs the string; or maybe every node is annotated, and so the annotation of the root is the original string...). So anyhow, at the moment I am mellow about any names you prefer as long as they make it clear to the code reader which exact piece of information is the authoritative set point.
Author
Member

Single source of truth about whether there's a set point

The simplification is precisely: only having one way to check whether there is a set point.

I agree that it would be good to maintain a single approved way to check whether there's a set point. I propose making that a public method or read-only signal field of Regulator. (The latter would let us write effects like this without cloning the whole Regulator.)

We can't use visibility to prevent future developers from inventing other ways to check, because we can't avoid making the set point specification publicly readable, but we can emphasize in the documentation that this method or field is the only way to check which is guaranteed to work.

Reflecting whether there's a set point in set_point helps keep out bugs

As for how to interpret said f64, the interpretation is: if there is no set point, the f64 is meaningless. if there is a set point, the f64 is the corresponding value for the Q-product of the subjects implied by the set point. Hence, there is no need to worry about what the f64 might or might not be when there is no set point; it's simply moot.

An Option pushes you to check conditions

When set_point is an Option<f64>, there are only two ways to get an f64 out of it:

  • Explicitly handle the case where there's no set point using a match expression or a method like unwrap_or.
  • Risk a panic by calling unwrap or expect, which come with big red flags in the documentation. These functions tell you, as you're typing them, that you'd better make sure the data you're trying to extract is really there. At worst, if you try to use the f64 when there's no set point, the program will panic rather than misbehaving more subtly.

A bare f64 feels free to use

When set_point is just an f64, using it without additional checks feels perfectly natural. It raises neither an error during compilation nor a red flag during code review. This makes it easier to use the f64 in a situation where it's meaningless because there's no set point.

Abstractly, the problem here is that conditional invariants are trickier to use correctly. That point of view is described next.

Reflecting whether there's a set point in set_point simplifies invariants

The current unconditional invariant

If that way is checking whether the specification of the set point is empty, […] then making the value of the set point be an Option is non-useful redundancy at best and confusing at worst: could a regulator have a set point and yet not have a value? Or vice-versa? A future coder who is not you could rightly worry about such possibilities.

So if no set point ≡ "set point specification is empty," then an f64 for the value suffices, and Option is in any situation less simple than f64 when an f64 suffices.

Right now, our code enforces a simple, unconditional invariant (documented here):

set_point_spec is always a valid specification of set_point.

This invariant does lead to some redundancy, as you said: when set_point_spec says there's no set point, set_point has to reflect that information. I think the simplicity of the invariant makes the redundancy worth it. Enforcing the invariant does come with a maintenance cost, but I'll argue that weakening the invariant also comes with a maintenance cost.

The proposed conditional invariant

As for how to interpret said f64, the interpretation is: if there is no set point, the f64 is meaningless.

This would make the invariant above conditional:

If there is a set point, then set_point_spec is a valid specification of set_point.

We'd have to be careful to check the condition every time we use the invariant. This is feasible: we're already using conditional invariants elsewhere in the assembly module. However, I do think it makes the code trickier to reason about, and it opens a door to bugs in the way described above.

### Single source of truth about whether there's a set point > The simplification is precisely: only having one way to check whether there is a set point. I agree that it would be good to maintain a single approved way to check whether there's a set point. I propose making that a public method or read-only signal field of `Regulator`. (The latter would let us write effects like [this](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/assembly.rs#L283-L292) without cloning the whole `Regulator`.) We can't use visibility to prevent future developers from inventing other ways to check, because we can't avoid making the set point specification publicly readable, but we can emphasize in the documentation that this method or field is the only way to check which is guaranteed to work. ### Reflecting whether there's a set point in `set_point` helps keep out bugs > As for how to interpret said f64, the interpretation is: if there is no set point, the f64 is meaningless. if there is a set point, the f64 is the corresponding value for the Q-product of the subjects implied by the set point. Hence, there is no need to worry about what the f64 might or might not be when there is no set point; it's simply moot. #### An `Option` pushes you to check conditions When `set_point` is an `Option<f64>`, there are only two ways to get an `f64` out of it: - Explicitly handle the case where there's no set point using a [match expression](https://doc.rust-lang.org/reference/expressions/match-expr.html) or a method like [`unwrap_or`](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or). - Risk a panic by calling [`unwrap`](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap) or [`expect`](https://doc.rust-lang.org/std/option/enum.Option.html#method.expect), which come with big red flags in the documentation. These functions tell you, as you're typing them, that you'd better make sure the data you're trying to extract is really there. At worst, if you try to use the `f64` when there's no set point, the program will panic rather than misbehaving more subtly. #### A bare `f64` feels free to use When `set_point` is just an `f64`, using it without additional checks feels perfectly natural. It raises neither an error during compilation nor a red flag during code review. This makes it easier to use the `f64` in a situation where it's meaningless because there's no set point. Abstractly, the problem here is that conditional invariants are trickier to use correctly. That point of view is described next. ### Reflecting whether there's a set point in `set_point` simplifies invariants #### The current unconditional invariant > If that way is checking whether the specification of the set point is empty, […] then making the value of the set point be an Option<f64> is non-useful redundancy at best and confusing at worst: could a regulator have a set point and yet not have a value? Or vice-versa? A future coder who is not you could rightly worry about such possibilities. > > So if no set point ≡ "set point specification is empty," then an f64 for the value suffices, and Option<f64> is in any situation less simple than f64 when an f64 suffices. Right now, our code enforces a simple, unconditional invariant (documented [here](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/assembly.rs#L114-L121)): > `set_point_spec` is always a valid specification of `set_point`. This invariant does lead to some redundancy, as you said: when `set_point_spec` says there's no set point, `set_point` has to reflect that information. I think the simplicity of the invariant makes the redundancy worth it. Enforcing the invariant does come with a maintenance cost, but I'll argue that weakening the invariant also comes with a maintenance cost. #### The proposed conditional invariant > As for how to interpret said f64, the interpretation is: if there is no set point, the f64 is meaningless. This would make the invariant above conditional: > If there is a set point, then `set_point_spec` is a valid specification of `set_point`. We'd have to be careful to check the condition every time we use the invariant. This is feasible: we're already using conditional invariants [elsewhere](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/assembly.rs#L168-L177) in the `assembly` module. However, I do think it makes the code trickier to reason about, and it opens a door to bugs in the way described above.
Owner

I will respond to each of your major headings in a separate post, just because of length.
@Vectornaut wrote in glen/dyna3#48 (comment):

Single source of truth about whether there's a set point

I agree that it would be good to maintain a single approved way to check whether there's a set point.

It won't surprise you that I applaud this. It's the external/interface side of the single source of truth principle. But the internal side, that the truth only resides in one place, is important too for clarity/maintainability.

We can't use visibility to prevent future developers from inventing other ways to check, because we can't avoid making the set point specification publicly readable, but we can emphasize in the documentation that this method or field is the only way to check which is guaranteed to work.

Right, there are two ways to go here: either the emptiness of some string is the record of whether there's a set point, in which case clients should use that (because I don't think there's any efficiency issue suggesting we need to cache the emptiness of that string), or the Regulator reserves the right to have the string be nonempty and yet there to be no set point, in which case it must provide some way to get at that truth. I'm fine either way on this.

EDIT: P.S. I am even fine with it currently being the case in actual operational fact that the string is empty iff there is no set point, even though we have recorded the existence of a set point elsewhere and/or provided a different way of checking. The latter design choice(s) just mean that we don't want the design of the Regulator to enshrine the emptiness as the official record of whether a set point is set -- perhaps because we think where that truth resides is likely to change in future code revision.

I will respond to each of your major headings in a separate post, just because of length. @Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2151: > ### Single source of truth about whether there's a set point > I agree that it would be good to maintain a single approved way to check whether there's a set point. It won't surprise you that I applaud this. It's the external/interface side of the single source of truth principle. But the internal side, that the truth only resides in one place, is important too for clarity/maintainability. > We can't use visibility to prevent future developers from inventing other ways to check, because we can't avoid making the set point specification publicly readable, but we can emphasize in the documentation that this method or field is the only way to check which is guaranteed to work. Right, there are two ways to go here: either the emptiness of some string _is_ the record of whether there's a set point, in which case clients should use that (because I don't think there's any efficiency issue suggesting we need to cache the emptiness of that string), or the Regulator reserves the right to have the string be nonempty and yet there to be no set point, in which case it must provide some way to get at that truth. I'm fine either way on this. EDIT: P.S. I am even fine with it currently being the case in actual operational fact that the string is empty iff there is no set point, even though we have recorded the existence of a set point elsewhere and/or provided a different way of checking. The latter design choice(s) just mean that we don't want the design of the Regulator to enshrine the emptiness as the official record of whether a set point is set -- perhaps because we think where that truth resides is likely to change in future code revision.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

Reflecting whether there's a set point in set_point helps keep out bugs

Perhaps. But it also helps create bugs and confusion -- it is an instance of the same information being stored in two places, and not for the sake of caching. It also forces client code to unwrap an Option, even when they know, because they checked in the sanctioned way, that there is a set point.

A bare f64 feels free to use

This caution is an excellent point, identifying a potential source of confusion in client code. It means it might be worth the extra cycles to set the f64 to NaN when there is in fact no set point -- not as a semantic guarantee, but as a safety measure. That setting would certainly prevent any use of a non-meaningful float value.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2151: > ### Reflecting whether there's a set point in `set_point` helps keep out bugs Perhaps. But it also helps create bugs and confusion -- it is an instance of the same information being stored in two places, and not for the sake of caching. It also forces client code to unwrap an Option, even when they know, because they checked in the sanctioned way, that there is a set point. > A bare f64 feels free to use This caution is an excellent point, identifying a potential source of confusion in client code. It means it might be worth the extra cycles to set the f64 to NaN when there is in fact no set point -- not as a semantic guarantee, but as a safety measure. That setting would certainly prevent any use of a non-meaningful float value.
Owner

Reflecting whether there's a set point in set_point simplifies invariants

We'd have to be careful to check the condition every time we use the invariant. This is feasible: we're already using conditional invariants

It's also obligatory: per your first major heading, there is only one valid/sanctioned way to tell whether there is a set point. We certainly don't want to force code that is complying with the documented interface by checking in the approved way to also have to go to the trouble of unwrapping an Option that it already knows must be a "Some" -- it's a distracting waste of code-reading time/effort and presumably of a couple cycles of the CPU.

This sort of redundant checking comes up all the time when writing typescript code: I have to do instanceof checks that I know from the logic of the code must be true in order to get the code to compile, because of course the TypeScript compiler is not a full-blown theorem prover about your code. It makes a lot of type inferences, but it doesn't, for example, check that the only events that are enabled on some element are ones where the target is an InputElement, say, so it barfs when you actually quite correctly invoke an InputElement method in the event handler. It's very frustrating. I definitely want to avoid this kind of redundancy in our code.

> ## Reflecting whether there's a set point in set_point simplifies invariants > We'd have to be careful to check the condition every time we use the invariant. This is feasible: we're already using conditional invariants It's also obligatory: per your first major heading, there is only one valid/sanctioned way to tell whether there is a set point. We certainly don't want to force code that is complying with the documented interface by checking in the approved way to also have to go to the trouble of unwrapping an Option that it already knows must be a "Some" -- it's a distracting waste of code-reading time/effort and presumably of a couple cycles of the CPU. This sort of redundant checking comes up all the time when writing typescript code: I have to do `instanceof` checks that I **know** from the logic of the code must be true in order to get the code to compile, because of course the TypeScript compiler is not a full-blown theorem prover about your code. It makes a lot of type inferences, but it doesn't, for example, check that the only events that are enabled on some element are ones where the target is an InputElement, say, so it barfs when you actually quite correctly invoke an InputElement method in the event handler. It's very frustrating. I definitely want to avoid this kind of redundancy in our code.
Author
Member

It also forces client code to unwrap an Option, even when they know, because they checked in the sanctioned way, that there is a set point.

That's a good point: the assembly module's interface shouldn't push outside code to call unwrap or expect. Here's are two alternate proposals which address this problem.

In these proposals, I'll use the set point specification to mean the source of truth about the set point, and set point data to mean data that depends only on the set point (obtained by parsing, evaluating, or otherwise processing the set point).

Proposal 1: Option everywhere

This caution is an excellent point, identifying a potential source of confusion in client code. It means it might be worth the extra cycles to set the f64 to NaN when there is in fact no set point -- not as a semantic guarantee, but as a safety measure. That setting would certainly prevent any use of a non-meaningful float value.

I like this idea. In fact, I think we should do it for every piece of set point data, including the set point specification. Wrapping all set point data in Option is a simple and consistent way to do this. It makes the safety measure stronger by panicking if we ever try to use a non-meaningful value. The Option::map method makes it easy—almost automatic—to guarantee that every piece of set point data is the same Option variant as the set point. This leads to the following proposal.

  • The set point specification is wrapped in an Option. We enforce the following invariant:
    • The set point specification is Some(_) if and only if there's a set point.
  • When we derive set point data from the set point specification within the assembly module, we always do it using Option::map, which acts on the contents of Some(_) while preserving None. As a result, set point data always comes wrapped in an Option, and we have the following invariant:
    • A piece of set point data is Some(_) if and only if there's a set point.
  • Every public signal that holds set point data and every method that returns set point data keeps it wrapped in the Option.
  • The approved way of distinguishing regulators with and without set points is to process set point data in a way that distinguishes Some(_) and None. This code is an example of such processing.

Proposal 2: specification only

  • The regulator only stores the set point specification.
  • The approved way to tell whether there's a set point is to look at the set point specification.
  • Anything that needs other set point data should derive it from the set point specification on the spot.

If we do this, we lose a desired feature you mentioned earlier:

(And yes, of course we do need a way to get that f64 that doesn't mean re-interpreting the authoritative set point every time.)

However, given the choice between

  • spending extra processor cycles to re-interpret the set point specification and
  • allowing a piece of set point data to have a value when there's no set point,

I'd lean toward spending the extra cycles.

> It also forces client code to unwrap an Option, even when they know, because they checked in the sanctioned way, that there is a set point. That's a good point: the `assembly` module's interface shouldn't push outside code to call `unwrap` or `expect`. Here's are two alternate proposals which address this problem. In these proposals, I'll use *the set point specification* to mean the source of truth about the set point, and *set point data* to mean data that depends only on the set point (obtained by parsing, evaluating, or otherwise processing the set point). ### Proposal 1: `Option` everywhere > This caution is an excellent point, identifying a potential source of confusion in client code. It means it might be worth the extra cycles to set the f64 to NaN when there is in fact no set point -- not as a semantic guarantee, but as a safety measure. That setting would certainly prevent any use of a non-meaningful float value. I like this idea. In fact, I think we should do it for every piece of set point data, including the set point specification. Wrapping all set point data in `Option` is a simple and consistent way to do this. It makes the safety measure stronger by panicking if we ever try to use a non-meaningful value. The [`Option::map`](https://doc.rust-lang.org/std/option/enum.Option.html#method.map) method makes it easy—almost automatic—to guarantee that every piece of set point data is the same `Option` variant as the set point. This leads to the following proposal. - The set point specification is wrapped in an `Option`. We enforce the following invariant: - *The set point specification is `Some(_)` if and only if there's a set point.* - When we derive set point data from the set point specification within the `assembly` module, we always do it using [`Option::map`](https://doc.rust-lang.org/std/option/enum.Option.html#method.map), which acts on the contents of `Some(_)` while preserving `None`. As a result, set point data always comes wrapped in an `Option`, and we have the following invariant: - *A piece of set point data is `Some(_)` if and only if there's a set point.* - Every public signal that holds set point data and every method that returns set point data keeps it wrapped in the `Option`. - The approved way of distinguishing regulators with and without set points is to process set point data in a way that distinguishes `Some(_)` and `None`. [This code](https://code.studioinfinity.org/Vectornaut/dyna3/src/commit/befadd25c9d35c6a9e782d9ada981002ebc91fc4/app-proto/src/assembly.rs#L311-L319) is an example of such processing. ### Proposal 2: specification only * The regulator only stores the set point specification. * The approved way to tell whether there's a set point is to look at the set point specification. * Anything that needs other set point data should derive it from the set point specification on the spot. If we do this, we lose a desired feature you mentioned earlier: > (And yes, of course we do need a way to get that f64 that doesn't mean re-interpreting the authoritative set point every time.) However, given the choice between - spending extra processor cycles to re-interpret the set point specification and - allowing a piece of set point data to have a value when there's no set point, I'd lean toward spending the extra cycles.
Owner

OK, we'll discuss and attempt to choose between these proposals or some other consistent variation thereof today. Thanks for continuing to brainstorm here!

OK, we'll discuss and attempt to choose between these proposals or some other consistent variation thereof today. Thanks for continuing to brainstorm here!
Author
Member

On Thursday (February 20), we decided to implement a variant of Proposal 1, which we called Proposal 1a. The idea was to make the set point an Option<PresentSpecifiedValue>, where PresentSpecifiedValue would be a structure with private fields holding the specification and value of a real number.

When I started to implement this proposal, I realized that it felt more natural to implement what I'll call Proposal 1b. I've made the set point a SpecifiedValue, which is an enum with two variants:

Proposal 1b shares the main safety advantage of Proposal 1a: code that uses the set point will only compile if it explicitly handles the absent case. It streamlines our code, relative to Proposal 1a, by reducing the number of "onion layers" around the set point, like you've been pushing for. It has three main disadvantages relative to Proposal 1a.

  • The only approved way to create a SpecifiedValue that might be Present is to call the associated function try_from on a specification string. This ensures consistency between the spec and value fields. However, I don't think we can reasonably force client code to use the approved way: my understanding is that if we allow client code to match on the Present variant, we also have to allow client code to create the Present variant by hand.
    • Right now, fortunately, I don't foresee much temptation to create the Present variant by hand. That could change, though.
  • In Proposal 1a, when an Option<PresentSpecifiedValue> matches Some(pres_spec_val), we can pass down pres_spec_val to avoid redundant handling of None. In Proposal 1b, we can only pass down the individual fields of the Present variant.
    • If this gets awkward, we could introduce a PresentSpecifiedValue structure that converts easily to and from the Present variant.
  • We lose out on the ecosystem of helper methods that come with Option.
    • This shouldn't be a big problem. There aren't many places I'd want to use the helper methods right now, and it sounds like you appreciate the transparency of matching anyway.
On Thursday (February 20), we decided to implement a variant of [Proposal 1](#user-content-proposal-1-option-everywhere), which we called Proposal 1a. The idea was to make the set point an `Option<PresentSpecifiedValue>`, where `PresentSpecifiedValue` would be a structure with private fields holding the specification and value of a real number. When I started to implement this proposal, I realized that it felt more natural to [implement](../../Vectornaut/dyna3/commit/6c31a25822ca9d5b8dc3a50dc7ffac4e94f19afc) what I'll call Proposal 1b. I've made the set point a `SpecifiedValue`, which is an enum with two variants: * A [unit variant](https://doc.rust-lang.org/reference/items/enumerations.html#r-items.enum.unit-only) `Absent`, which indicates no value. * A [struct variant](https://rust-lang.github.io/rfcs/0418-struct-variants.html) `Present`, which holds a value and its specification. Proposal 1b shares the main safety advantage of Proposal 1a: code that uses the set point will only compile if it explicitly handles the absent case. It streamlines our code, relative to Proposal 1a, by reducing the number of "onion layers" around the set point, like you've been pushing for. It has three main disadvantages relative to Proposal 1a. * The only approved way to create a `SpecifiedValue` that might be `Present` is to call the associated function `try_from` on a specification string. This ensures consistency between the `spec` and `value` fields. However, I don't think we can reasonably force client code to use the approved way: my understanding is that if we allow client code to match on the `Present` variant, we also have to allow client code to create the `Present` variant by hand. * *Right now, fortunately, I don't foresee much temptation to create the `Present` variant by hand. That could change, though.* * In Proposal 1a, when an `Option<PresentSpecifiedValue>` matches `Some(pres_spec_val)`, we can pass down `pres_spec_val` to avoid redundant handling of `None`. In Proposal 1b, we can only pass down the individual fields of the `Present` variant. * *If this gets awkward, we could introduce a `PresentSpecifiedValue` structure that converts easily to and from the `Present` variant.* * We lose out on the ecosystem of helper methods that come with `Option`. * *This shouldn't be a big problem. There aren't many places I'd want to use the helper methods right now, and it sounds like you appreciate the transparency of matching anyway.*
Owner

Let's see if I understand. 1b replaces Option<SetPointStruc> in which the Some() variant, as must be the case, has a single item payload which is a struct with spec and value, by basically a roll-our-own option in which the Present variant directly has two fields, spec and value. So in 1a, in the Some(setp) arm of a match, you must write setp.spec or setp.value to get at those things, whereas in 1b, in the Present{value,..} arm of a match, you have value directly and you can ignore the spec altogether if you like. Is that about it?

If the 1b access feels more comfortable/streamlined, I am perfectly happy with it.

Not that I am advocating it, but could you get the enforced consistency back by making a setpoint a struct with one data member that was one of these present/absent gizmos and then making that data readonly from the outside, thereby forcing use of the setter method provided? That may of course be just as cumbersome as the option was, but I was just asking in principle could it be done that way.

Thanks for pursuing the optimal design here! I do hope it will pay dividends in dealing with the code as it evolves in the future.

Let's see if I understand. 1b replaces `Option<SetPointStruc>` in which the Some() variant, as must be the case, has a single item payload which is a struct with spec and value, by basically a roll-our-own option in which the Present variant directly has two fields, spec and value. So in 1a, in the Some(setp) arm of a match, you must write setp.spec or setp.value to get at those things, whereas in 1b, in the Present{value,..} arm of a match, you have value directly and you can ignore the spec altogether if you like. Is that about it? If the 1b access feels more comfortable/streamlined, I am perfectly happy with it. Not that I am advocating it, but could you get the enforced consistency back by making a setpoint a struct with one data member that was one of these present/absent gizmos and then making that data readonly from the outside, thereby forcing use of the setter method provided? That may of course be just as cumbersome as the option was, but I was just asking in principle could it be done that way. Thanks for pursuing the optimal design here! I do hope it will pay dividends in dealing with the code as it evolves in the future.
Author
Member

1b replaces Option<SetPointStruc> in which the Some() variant, as must be the case, has a single item payload which is a struct with spec and value, by basically a roll-our-own option in which the Present variant directly has two fields, spec and value. So in 1a, in the Some(setp) arm of a match, you must write setp.spec or setp.value to get at those things, whereas in 1b, in the Present{value,..} arm of a match, you have value directly and you can ignore the spec altogether if you like. Is that about it?

Yes, that's one way Proposal 1b streamlines our code. However, there's another streamlining that I find more important: since the SpecifiedValue type is local to our crate, we can implement methods (spec and is_present) and external traits (TryFrom) for it.

Since Option is external to our crate, we can't directly implement methods or external traits for Option<PresentSpecifiedValue>. In my implementation of Proposal 1a, I use the newtype pattern, implementing the methods and traits mentioned above on a wrapper type:

struct OptionalSpecifiedValue(Option<PresentSpecifiedValue>);

impl OptionalSpecifiedValue {
    /* ... */
}

This has no performance cost, but in this case I think it adds a lot of noise to the code, because I'm writing stuff like this:

match set_pt {
    OptionalSpecifiedValue(Some(pt)) => /* ... */,
    OptionalSpecifiedValue(None) => /* ... */
}

This would look nicer if we came up with a shorter name for the wrapper type. However, if the single-layer type in Proposal 1b works well enough already, I figure we might as well spare ourselves the trouble.

Not that I am advocating it, but could you get the enforced consistency back by making a setpoint a struct with one data member that was one of these present/absent gizmos and then making that data readonly from the outside, thereby forcing use of the setter method provided?

If I'm understanding this correctly, it sounds like another way of applying the newtype pattern. It should work, but it would add noise in the same way as described above.

> 1b replaces `Option<SetPointStruc>` in which the Some() variant, as must be the case, has a single item payload which is a struct with spec and value, by basically a roll-our-own option in which the Present variant directly has two fields, spec and value. So in 1a, in the Some(setp) arm of a match, you must write setp.spec or setp.value to get at those things, whereas in 1b, in the Present{value,..} arm of a match, you have value directly and you can ignore the spec altogether if you like. Is that about it? Yes, that's one way Proposal 1b streamlines our code. However, there's another streamlining that I find more important: since the `SpecifiedValue` type is local to our crate, we can implement methods (`spec` and `is_present`) and external traits (`TryFrom`) for it. Since `Option` is external to our crate, we can't directly implement methods or external traits for `Option<PresentSpecifiedValue>`. In my implementation of Proposal 1a, I use the [newtype pattern](https://stackoverflow.com/a/35569079), implementing the methods and traits mentioned above on a wrapper type: ``` struct OptionalSpecifiedValue(Option<PresentSpecifiedValue>); impl OptionalSpecifiedValue { /* ... */ } ``` This has no performance cost, but in this case I think it adds a lot of noise to the code, because I'm writing stuff like this: ``` match set_pt { OptionalSpecifiedValue(Some(pt)) => /* ... */, OptionalSpecifiedValue(None) => /* ... */ } ``` This would look nicer if we came up with a shorter name for the wrapper type. However, if the single-layer type in Proposal 1b works well enough already, I figure we might as well spare ourselves the trouble. > Not that I am advocating it, but could you get the enforced consistency back by making a setpoint a struct with one data member that was one of these present/absent gizmos and then making that data readonly from the outside, thereby forcing use of the setter method provided? If I'm understanding this correctly, it sounds like another way of applying the newtype pattern. It should work, but it would add noise in the same way as described above.
Owner

OK, that's all meshing with my understanding. 1b is fine, I think we both prefer it at least slightly. See you later.

OK, that's all meshing with my understanding. 1b is fine, I think we both prefer it at least slightly. See you later.
Vectornaut added 7 commits 2025-02-24 21:38:35 +00:00
This lets us infer a regulator's role from whether it has a set point
and what text specifies the set point.
When an invalid specification is entered into a regulator input, keep it
confined to that input. Reset a regulator input by pressing *escape*.
Make a regulator's set point specification private, and split the set
point into a private writable signal and a public read-only signal. The
set point can now be initialized only through the factory method
`insert_new_regulator` and changed only through the setter method
`try_specify_set_point`, which both ensure that the set point
specification is valid and consistent with the set point.
Invalid attempts to specify a regulator's set point are now local to
each view of the regulator, and don't affect the regulator model. In
particular, a regulator will be valid and in force even when one of its
regulator input views is showing an invalid specification attempt. The
invalidity indicator should therefore be tied to the input showing
the invalid specification, not to the whole regulator outline item.
Author
Member

I've fast-forwarded the pull request branch to Proposal 1b, so you can review and merge if you'd like.

I've fast-forwarded the pull request branch to Proposal 1b, so you can review and merge if you'd like.
Owner

This review is going to be big, just simply because this is sort of the first time that the app really "does something". So I am going to make several observations in this comment, and some of them should be addressed in this PR perhaps and others should become new issues once this is merged, or new issues right now if they don't really have to do with this PR. But I think they all need to be addressed in some way at some point.

  1. If you add an inversive distance regulator between A and B, and then add one between A and C, and then select B and C and add a regulator, presuming that neither of the dropdowns for B or C is open, there is precisely zero feedback that the operation occurred. That seems like a ui bug. Perhaps the (first? both?) subject(s) of a new regulator should auto-open on creation?
  2. The interface allows me to select A and B, and add an inversive distance regulator, and then set that regulator, and then select A and B again and add another inversive distance regulator (...and then I can see how close it actually got, as a side effect...) and then I can set that second regulator to a different value, and the realization updates, and there is no indication of any error along the way, and then I can select A and B again and add a third inversive distance regulator and see that the value the realization achieved is almost precisely the second value. So the two are not even acting as independent constraints (in which case I would have expected it to achieve the midpoint). I understand from the code why that is, but this whole scenario suggests that the identity of an inversive distance regulator should be its (unordered!) pair of subjects and that it should be impossible to create two identical inversive distance regulators (notwithstanding the nice side effect at the moment to be able to show the realized value -- we know we want to allow that in some way in the future...) I cannot think of any use case for redundant identical regulators.
  3. The current optimization seems to prefer changing curvature over position: try from the default state making Castor and Pollux (externally) tangent, then making them both tangent to Ursa Major, and then making all three tangent to Deimos. I end up with a nearly-planar Ursa Major, which isn't the most cognitively comfortable realization starting with the initial configuration. I more-or-less expected them to just nestle in a tetrahedron, each relatively near its starting size.
  4. Need to file an issue to allow removal of entities, regulators and/or elements.
  5. More immediately pertinent, but inspired by (4) because likely we will need to address it at least by the time we implement (4): it's counterintuitive/asymmetric that I can select an element but I can't select a regulator. I found myself clicking on the text of the row for a regulator and wondering why nothing was happening. Perhaps when one is typing in the set point control of a regulator, that row should be highlighted similarly to the way an element is highlighted when it is selected? And perhaps either it should be considered selected, or its subjects should be considered selected? and conversely, when you click on the row of a regulator, it should go into this state and therefore the keyboard focus should go to the entry box? At some later point we could contemplate how a selected regulator should be displayed graphically, if at all -- that's of course tied up with whether and what the the graphical representations of regulators are. Or whether a regulator is "selected" could be more independent of whether its set point has focus, although my intuition at least is that at least, focusing the set point selects the regulator.
  6. Suppose you add an i.d. regulator to A and B, and set it to 2, and then it one of its input boxes (say with both showing), replace the set point with a and hit enter. You get an error indicator in that one input box, and the other still looks fine, and that is all good. But there now doesn't seem to be anything you can do to just "cancel" the failed update: going into the other box and hitting enter has no effect; going into the error box and deleting everything and hitting enter unsets the regulator, and I couldn't think of anything else to try. This does not seem to be a case of needing our "undo" system, because in fact, no actual action has taken place (and frankly I would be fine if save, close, and reload did not restore the input boxes into this error state). There needs to be some means of simply restoring that control to its current actual state. We may need to discuss what that interaction should be.
This review is going to be big, just simply because this is sort of the first time that the app really "does something". So I am going to make several observations in this comment, and some of them should be addressed in this PR perhaps and others should become new issues once this is merged, or new issues right now if they don't really have to do with this PR. But I think they all need to be addressed in some way at some point. 1) If you add an inversive distance regulator between A and B, and then add one between A and C, and then select B and C and add a regulator, presuming that neither of the dropdowns for B or C is open, there is precisely zero feedback that the operation occurred. That seems like a ui bug. Perhaps the (first? both?) subject(s) of a new regulator should auto-open on creation? 2) The interface allows me to select A and B, and add an inversive distance regulator, and then set that regulator, and then select A and B again and add another inversive distance regulator (...and then I can see how close it actually got, as a side effect...) and then I can set that second regulator to a different value, and the realization updates, and there is no indication of any error along the way, and then I can select A and B again and add a third inversive distance regulator and see that the value the realization achieved is almost precisely the second value. So the two are not even acting as independent constraints (in which case I would have expected it to achieve the midpoint). I understand from the code why that is, but this whole scenario suggests that the _identity_ of an inversive distance regulator should be its (unordered!) pair of subjects and that it should be impossible to create two identical inversive distance regulators (notwithstanding the nice side effect at the moment to be able to show the realized value -- we know we want to allow that in some way in the future...) I cannot think of any use case for redundant identical regulators. 3) The current optimization seems to prefer changing curvature over position: try from the default state making Castor and Pollux (externally) tangent, then making them both tangent to Ursa Major, and then making all three tangent to Deimos. I end up with a nearly-planar Ursa Major, which isn't the most cognitively comfortable realization starting with the initial configuration. I more-or-less expected them to just nestle in a tetrahedron, each relatively near its starting size. 4) Need to file an issue to allow removal of entities, regulators and/or elements. 5) More immediately pertinent, but inspired by (4) because likely we will need to address it at least by the time we implement (4): it's counterintuitive/asymmetric that I can select an element but I can't select a regulator. I found myself clicking on the text of the row for a regulator and wondering why nothing was happening. Perhaps when one is typing in the set point control of a regulator, that row should be highlighted similarly to the way an element is highlighted when it is selected? And perhaps either it should be considered selected, or its subjects should be considered selected? and conversely, when you click on the row of a regulator, it should go into this state and therefore the keyboard focus should go to the entry box? At some later point we could contemplate how a selected regulator should be displayed graphically, if at all -- that's of course tied up with whether and what the the graphical representations of regulators are. Or whether a regulator is "selected" could be more independent of whether its set point has focus, although my intuition at least is that at least, focusing the set point selects the regulator. 6) Suppose you add an i.d. regulator to A and B, and set it to 2, and then it one of its input boxes (say with both showing), replace the set point with `a` and hit enter. You get an error indicator in that one input box, and the other still looks fine, and that is all good. But there now doesn't seem to be anything you can do to just "cancel" the failed update: going into the other box and hitting enter has no effect; going into the error box and deleting everything and hitting enter unsets the regulator, and I couldn't think of anything else to try. This does not seem to be a case of needing our "undo" system, because in fact, no actual action has taken place (and frankly I would be fine if save, close, and reload did not restore the input boxes into this error state). There needs to be some means of simply restoring that control to its current actual state. We may need to discuss what that interaction should be.
glen reviewed 2025-02-26 01:29:32 +00:00
glen left a comment
Owner

OK, here's a batch of code-level review along with the behavior-level comments I just made. This is all a lot of stuff, so once it's triaged and decided what will be addressed now and what will be moved to issues and what will be left as is, I will likely need to review again... thanks for understanding.

OK, here's a batch of code-level review along with the behavior-level comments I just made. This is all a lot of stuff, so once it's triaged and decided what will be addressed now and what will be moved to issues and what will be left as is, I will likely need to review again... thanks for understanding.
@ -27,3 +31,3 @@
pub color: ElementColor,
pub representation: Signal<DVector<f64>>,
pub constraints: Signal<BTreeSet<ConstraintKey>>,
pub regulators: Signal<BTreeSet<RegulatorKey>>,
Owner

Is it worth commenting that this is intended to be the set of all regulators that have this Element as a subject? Or is that entirely clear? Similarly, I think it should be noted somewhere, perhaps as a comment here, the mechanism/responsibility for maintaining that information. When a regulator is inserted into an Assembly, is it the assembly's responsibility to tell each of the subjects? I imagine that's the design, but it would be good to have it written down somewhere.

Is it worth commenting that this is intended to be the set of all regulators that have this Element as a subject? Or is that entirely clear? Similarly, I think it should be noted somewhere, perhaps as a comment here, the mechanism/responsibility for maintaining that information. When a regulator is inserted into an Assembly, is it the assembly's responsibility to tell each of the subjects? I imagine that's the design, but it would be good to have it written down somewhere.
Owner

I mean, of course by now I have gotten to that section of the code and seen that what I surmised is exactly what's happening, but I think the obligations of the various structures should be made clear not just in the implementing code in case some later coder comes along and doesn't realize that if the Assembly modifies regulators in some way, it's the Assembly's responsibility to update this information on the elements...

I mean, of course by now I have gotten to that section of the code and seen that what I surmised is exactly what's happening, but I think the obligations of the various structures should be made clear not just in the implementing code in case some later coder comes along and doesn't realize that if the Assembly modifies regulators in some way, it's the Assembly's responsibility to update this information on the elements...
Author
Member

I've added a comment to this effect (7cbd926). Let me know if I should make it more detailed.

I've added a comment to this effect (7cbd926). Let me know if I should make it more detailed.
Owner

Detail level was fine. I just tweaked grammar/explicitness level. Resolving.

Detail level was fine. I just tweaked grammar/explicitness level. Resolving.
glen marked this conversation as resolved
@ -115,1 +118,3 @@
pub struct Constraint {
// to construct a `SpecifiedValue` that might be `Present`, use the associated
// function `try_from`. this ensures that `spec` is always a valid specification
// of `value` according to the format discussed above the implementation of
Owner

Tiny writing nit: this is a "garden path" phrasing -- I read "format discussed above" and started looking for the format specification earlier in the file, since I was pretty sure I hadn't seen one. Maybe reword just a bit to avoid that trap I fell into?

Tiny writing nit: this is a "garden path" phrasing -- I read "format discussed above" and started looking for the format specification earlier in the file, since I was pretty sure I hadn't seen one. Maybe reword just a bit to avoid that trap I fell into?
Author
Member

Nice catch! I've changed "above" to "at" to resolve the ambiguity (7cbd926). Does that read all right to you?

Nice catch! I've changed "above" to "at" to resolve the ambiguity (7cbd926). Does that read all right to you?
Owner

I agree unambiguous now. And hopefully this will change to an explicit anchor link when we get the doc system set up. So resolving.

I agree unambiguous now. And hopefully this will change to an explicit anchor link when we get the doc system set up. So resolving.
glen marked this conversation as resolved
@ -116,0 +137,4 @@
//
pub fn spec(&self) -> String {
match self {
Absent => String::new(),
Owner

Note here we are baking into this design the notion that the only specification of an Absent SpecifiedValue that we ever care to remember is ''. Is that OK? I worry in particular that when we want to allow disabling while remembering the un-disabled spec, this could bite us. Or maybe in that situation, the bit as to whether a regulator is active goes into the Regulator, and an inactive Regulator with a Present SpecifiedValue represents that state. So this is probably OK, but thought I'd just raise the point before we merge this.

Note here we are baking into this design the notion that the only specification of an Absent SpecifiedValue that we ever care to remember is `''`. Is that OK? I worry in particular that when we want to allow disabling while remembering the un-disabled spec, this could bite us. Or maybe in that situation, the bit as to whether a regulator is active goes into the Regulator, and an inactive Regulator with a Present SpecifiedValue represents that state. So this is probably OK, but thought I'd just raise the point before we merge this.
Author
Member

I definitely think we should give the Absent variant a specification field if we end up with more than one way to specify absence. However, I'm not sure it makes sense to add that field now. It feels weird to have a field whose value should always be an empty string.

I definitely think we should give the `Absent` variant a specification field if we end up with more than one way to specify absence. However, I'm not sure it makes sense to add that field now. It feels weird to have a field whose value should always be an empty string.
Owner

I think it would be weirder to have

pub enum SpecifiedValue {
    Absent(absentSpec: String),
    Present {
        spec: String,
        value: f64
    }
}

-- I mean, the spec should be the spec and stored in the same place whether it corresponds to an absent or a present SpecifiedValue. So if you think that's likely where we're headed, we should redesign this now, shouldn't we? Thoughts?

I think it would be weirder to have ``` pub enum SpecifiedValue { Absent(absentSpec: String), Present { spec: String, value: f64 } } ``` -- I mean, the spec should be the spec and stored in the same place whether it corresponds to an absent or a present SpecifiedValue. So if you think that's likely where we're headed, we should redesign this now, shouldn't we? Thoughts?
Author
Member

To me, it seems perfectly natural to have this:

pub enum SpecifiedValue {
    Absent(String),
    Present {
        spec: String,
        value: f64
    }
}

An absent SpecifiedValue has only a specification. A present SpecifiedValue has both a specification and a value. The specification is always stored in the same place. When I write something like

if let Ok(spec_val) = SpecifiedValue::try_from(my_spec) {
    print!("{}", spec_val.spec());
}

the string my_spec is stored in spec_val and then returned by spec_val.spec(), regardless of whether spec_val is Absent or Present.

I thought the main point of the SpecifiedValue type was to store a specification together with all the value data derived from it, making it easy to ensure that the specification and the data always correspond. It's harder to do that if a SpecifiedValue doesn't carry its specification along with it.

To me, it seems perfectly natural to have this: ```rust pub enum SpecifiedValue { Absent(String), Present { spec: String, value: f64 } } ``` An absent `SpecifiedValue` has only a specification. A present `SpecifiedValue` has both a specification and a value. The specification is always stored in the same place. When I write something like ``` if let Ok(spec_val) = SpecifiedValue::try_from(my_spec) { print!("{}", spec_val.spec()); } ``` the string `my_spec` is stored in `spec_val` and then returned by `spec_val.spec()`, regardless of whether `spec_val` is `Absent` or `Present`. I thought the main point of the `SpecifiedValue` type was to store a specification together with all the value data derived from it, making it easy to ensure that the specification and the data always correspond. It's harder to do that if a `SpecifiedValue` doesn't carry its specification along with it.
Owner

Wow, I really don't understand why this detail of our design/implementation continues to be such a sticking point for us. It seems like it really should be simple:

I thought the main point of the SpecifiedValue type was to store a specification together with all the value data derived from it, making it easy to ensure that the specification and the data always correspond.

We totally agree on this. So what is the best, clearest, simplest, easiest-to-use design of the SpecifiedValue type that embodies this principle? Feel free to start again from scratch if need be, I want to get this right.

It's harder to do that if a SpecifiedValue doesn't carry its specification along with it.

We agree on this, too. Of course a SpecifiedValue should carry its specification with it -- it should be its primary data.

The specification is always stored in the same place.

But to me this is patently not true in your corrected version of this potential way of implementing a SpecifiedValue with multiple ways of being Absent (sorry, I forgot tuple fields can't be named). In the Absent case, it's the 0th tuple entry of the payload. In the Present case, it's the spec field of the payload. Two different places for the "same" string -- to my eyes, very disorienting.

Since SpecifiedValue is moving to a separate file anyway, should it be entirely opaque data-wise and have a method-only interface (and not be matchable upon, for example, in case at some point the best implementation isn't an enum)? The methods would I guess be something like is_present() and spec() and value(), with the latter I suppose returning NaN in case is_present() is false? Or perhaps value() panics if you call it when is_present() is false? That design change would immediately make the following conversation, about whether to have an is_present() method or use matching, moot and resolvable, because there would only be the methods.

As always, looking forward to your thoughts. And I do truly apologize (and am puzzled) that something seemingly so simple is being so recalcitrant of a mutually satisfying design/implementation.

Wow, I really don't understand why this detail of our design/implementation continues to be such a sticking point for us. It seems like it really should be simple: > I thought the main point of the SpecifiedValue type was to store a specification together with all the value data derived from it, making it easy to ensure that the specification and the data always correspond. We totally agree on this. So what is the best, clearest, simplest, easiest-to-use design of the SpecifiedValue type that embodies this principle? Feel free to start again from scratch if need be, I want to get this right. > It's harder to do that if a SpecifiedValue doesn't carry its specification along with it. We agree on this, too. Of course a SpecifiedValue should carry its specification with it -- it should be its _primary_ data. > The specification is always stored in the same place. But to me this is patently not true in your corrected version of this potential way of implementing a SpecifiedValue with multiple ways of being Absent (sorry, I forgot tuple fields can't be named). In the Absent case, it's the 0th tuple entry of the payload. In the Present case, it's the `spec` field of the payload. Two different places for the "same" string -- to my eyes, very disorienting. Since SpecifiedValue is moving to a separate file anyway, should it be entirely opaque data-wise and have a method-only interface (and _not_ be matchable upon, for example, in case at some point the best implementation isn't an enum)? The methods would I guess be something like is_present() and spec() and value(), with the latter I suppose returning `NaN` in case is_present() is false? Or perhaps value() panics if you call it when is_present() is false? That design change would immediately make the following conversation, about whether to have an is_present() method or use matching, moot and resolvable, because there would only be the methods. As always, looking forward to your thoughts. And I do truly apologize (and am puzzled) that something seemingly so simple is being so recalcitrant of a mutually satisfying design/implementation.
Author
Member

In the Absent case, it's the 0th tuple entry of the payload. In the Present case, it's the spec field of the payload.

If you find it less disorienting, we could do this:

pub enum SpecifiedValue {
    Absent {
        spec: String
    },
    Present {
        spec: String,
        value: f64
    }
}

Now both variants have a field called spec, and the Present variant also has a field called value. This would make match arms that handle Absent and Present values look more similar.

To access the specification of a general SpecifiedValue, we'd still need to call the spec() method, which does the matching internally. As far as I know, different variants of an enum are always handled separately in the end. We can hide the separate handling within methods of the enum, but we can't get rid of it.

The answers to this StackExchange question discuss various approaches to creating data types that have multiple variants with common fields.

  • The current Proposal 1b approach, which uses the spec() method to access the specification field across all variants, is similar in spirit to this answer.
  • When I was implementing Proposal 1a, I tried implementing the Deref trait for OptionalSpecifiedValue, kind of like in this answer. I switched to Proposal 1b before getting the trait working, though.
> In the Absent case, it's the 0th tuple entry of the payload. In the Present case, it's the spec field of the payload. If you find it less disorienting, we could do this: ```rust pub enum SpecifiedValue { Absent { spec: String }, Present { spec: String, value: f64 } } ``` Now both variants have a field called `spec`, and the `Present` variant also has a field called `value`. This would make `match` arms that handle `Absent` and `Present` values look more similar. To access the specification of a general `SpecifiedValue`, we'd still need to call the `spec()` method, which does the matching internally. As far as I know, different variants of an enum are always handled separately in the end. We can hide the separate handling within methods of the enum, but we can't get rid of it. The answers to [this StackExchange question](https://stackoverflow.com/questions/49186751/sharing-a-common-value-in-all-enum-values) discuss various approaches to creating data types that have multiple variants with common fields. - The current Proposal 1b approach, which uses the `spec()` method to access the specification field across all variants, is similar in spirit to [this answer](https://stackoverflow.com/a/77559109). - When I was implementing Proposal 1a, I tried implementing the `Deref` trait for `OptionalSpecifiedValue`, kind of like in [this answer](https://stackoverflow.com/a/67467313). I switched to Proposal 1b before getting the trait working, though.
Owner
  • Your thoughts on just a method-only interface with an opaque underlying datatype that we can iterate on if we see fit without changing any client code? Maybe our real sticking point is that the proposals so far are exposing too much of the implementation? Given this enduring debate, I am warming to such a way of ending it: I think we really ought to be able to agree on what such an interface would be. Then the data layout of the underlying implementation is much less critical -- basically anything reasonable that supports the interface.

The current Proposal 1b approach, which uses the spec() method to access the specification field across all variants, is similar in spirit to this answer.

Hmm; it would seem to me that something like

struct SpecifiedValue {
   spec: String,
   value: Option<f64>
}

would be much closer to the spirit of that answer. There is always a spec, that's the primary data, and there may be a value, if the spec is a "Present" spec. Then we just need to make it clear what is the primary location/means of testing whether a SpecifiedValue is "present". I realize this may be full-circle...

* Your thoughts on just a method-only interface with an opaque underlying datatype that we can iterate on if we see fit without changing any client code? Maybe our real sticking point is that the proposals so far are exposing too much of the implementation? Given this enduring debate, I am warming to such a way of ending it: I think we really _ought_ to be able to agree on what such an interface would be. Then the data layout of the underlying implementation is _much_ less critical -- basically anything reasonable that supports the interface. > The current Proposal 1b approach, which uses the spec() method to access the specification field across all variants, is similar in spirit to this answer. Hmm; it would seem to me that something like ``` struct SpecifiedValue { spec: String, value: Option<f64> } ``` would be much closer to the spirit of that answer. There is always a spec, that's the primary data, and there may be a value, if the spec is a "Present" spec. Then we just need to make it clear what is **the** primary location/means of testing whether a SpecifiedValue is "present". I realize this may be full-circle...
Author
Member

Your thoughts on just a method-only interface with an opaque underlying datatype that we can iterate on if we see fit without changing any client code?

At this point, I can't predict whether that would make things cleaner or messier. I'd want value, and other methods that fetch derived data, to fail gracefully when the set point is absent—for example, by having an Option return type and returning None when the set point is absent. This might make it annoying to write client code that uses several pieces of derived data at once, but we don't have that problem yet, since there's only one piece of derived data.

Hmm; it would seem to me that something like

struct SpecifiedValue {
    spec: String,
    value: Option<f64>
}

would be much closer to the spirit of that answer. […] I realize this may be full-circle...

I agree that this—let's call it Proposal 1c—would be close to the original Proposal 1. The big differences I see would be:

  • All the specified value data would be encapsulated in the SpecifiedValue type, instead of being loose in the regulator.
  • When we only want to know whether the value is present, we'd call a method, which might internally check whether an arbitrary piece of derived data is None or Some(_).

By the way, I think it's likely that we'll revisit the specified value data type when we generalize from decimal numbers to more generalized expressions. At that point, we'll probably be using specified values more, so we'll have a better idea of what we want from them.

> Your thoughts on just a method-only interface with an opaque underlying datatype that we can iterate on if we see fit without changing any client code? At this point, I can't predict whether that would make things cleaner or messier. I'd want `value`, and other methods that fetch derived data, to fail gracefully when the set point is absent—for example, by having an `Option` return type and returning `None` when the set point is absent. This might make it annoying to write client code that uses several pieces of derived data at once, but we don't have that problem yet, since there's only one piece of derived data. > Hmm; it would seem to me that something like > > ``` > struct SpecifiedValue { > spec: String, > value: Option<f64> > } > ``` > > would be much closer to the spirit of that answer. […] I realize this may be full-circle... I agree that this—let's call it Proposal 1c—would be close to the original Proposal 1. The big differences I see would be: - All the specified value data would be encapsulated in the `SpecifiedValue` type, instead of being loose in the regulator. - When we only want to know whether the value is present, we'd call a method, which might internally check whether an arbitrary piece of derived data is `None` or `Some(_)`. By the way, I think it's likely that we'll revisit the specified value data type when we generalize from decimal numbers to more generalized expressions. At that point, we'll probably be using specified values more, so we'll have a better idea of what we want from them.
Author
Member

I've switched to Proposal 1c (84bfdef), which we adopted during today's meeting. The SpecifiedValue structure is read-only, courtesy of the readonly crate, so nothing you do with it outside the specified module should be able to break the consistency between its fields. I've confirmed, for example, that outside the specified module:

  • You can't construct a SpecifiedValue by manually initializing its fields.
  • The fields of a SpecifiedValue can't be assigned to or borrowed as mutable.
I've switched to Proposal 1c (84bfdef), which we adopted during today's meeting. The `SpecifiedValue` structure is read-only, courtesy of the [readonly](https://docs.rs/readonly/latest/readonly/) crate, so nothing you do with it outside the `specified` module should be able to break the consistency between its fields. I've confirmed, for example, that outside the `specified` module: - You can't construct a `SpecifiedValue` by manually initializing its fields. - The fields of a `SpecifiedValue` can't be assigned to or borrowed as mutable.
Owner

Looks good. Let me just see if I now understand: currently in the code the only way to produce a SpecifiedValue is via the try_from operation on a string; and if one wants to change the set point of a regulator, it must be done by creating a fresh SpecifiedValue and replacing the prior set point with the fresh one (not by modifying the SpecifiedValue that's in place). Please let me know either way if I've got that all straight. Thanks.

Looks good. Let me just see if I now understand: currently in the code the _only_ way to produce a SpecifiedValue is via the try_from operation on a string; and if one wants to change the set point of a regulator, it must be done by creating a fresh SpecifiedValue and replacing the prior set point with the fresh one (not by modifying the SpecifiedValue that's in place). Please let me know either way if I've got that all straight. Thanks.
Author
Member

currently in the code the only way to produce a SpecifiedValue is via the try_from operation on a string;

There's one more way: the from_empty_spec method is public too.

if one wants to change the set point of a regulator, it must be done by creating a fresh SpecifiedValue and replacing the prior set point with the fresh one (not by modifying the SpecifiedValue that's in place).

Yup!

> currently in the code the only way to produce a SpecifiedValue is via the try_from operation on a string; There's one more way: the `from_empty_spec` method is public too. > if one wants to change the set point of a regulator, it must be done by creating a fresh SpecifiedValue and replacing the prior set point with the fresh one (not by modifying the SpecifiedValue that's in place). Yup!
glen marked this conversation as resolved
@ -116,0 +143,4 @@
}
fn is_present(&self) -> bool {
match self {
Owner

Is it worthwhile/idiomatic to provide such a predicate when clients of SpecifiedValue could just (a) match themselves, (b) use if let, (c) we could just derive PartialEq and let clients test sv != SpecifiedValue::Absent, or (d) use the matches!() macro, any one of which might be totally readable and just as easy? In other words, is this method reinventing the wheel? Not saying it's necessarily extraneous, just wanted to raise the issue in case we can basically save some code by using some preexisting standard idiom.

Is it worthwhile/idiomatic to provide such a predicate when clients of SpecifiedValue could just (a) match themselves, (b) use if let, (c) we could just derive PartialEq and let clients test sv != SpecifiedValue::Absent, or (d) use the matches!() macro, any one of which might be totally readable and just as easy? In other words, is this method reinventing the wheel? Not saying it's necessarily extraneous, just wanted to raise the issue in case we can basically save some code by using some preexisting standard idiom.
Author
Member

Several enums from the standard library have similar methods.

In the tracking issue for those Cow methods, there's a hearty debate about whether such methods are a good idea. Pattern-matching has improved since Option and Result were introduced, and it may eventually improve enough to make variant-checking methods obsolete. For now, though, I personally think that these methods pay for themselves in readability.

Several enums from the standard library have similar methods. - `Option`: [`is_some`](https://doc.rust-lang.org/std/option/enum.Option.html#method.is_some) / [`is_none`](https://doc.rust-lang.org/std/option/enum.Option.html#method.is_none) - `Result`: [`is_ok`](https://doc.rust-lang.org/std/result/enum.Result.html#method.is_ok) / [`is_err`](https://doc.rust-lang.org/std/result/enum.Result.html#method.is_err) - `Cow`: [`is_borrowed`](https://doc.rust-lang.org/std/borrow/enum.Cow.html#method.is_borrowed) / [`is_owned`](https://doc.rust-lang.org/std/borrow/enum.Cow.html#method.is_owned) In the [tracking issue](https://github.com/rust-lang/rust/issues/65143) for those `Cow` methods, there's a hearty debate about whether such methods are a good idea. Pattern-matching has [improved](https://github.com/rust-lang/rust/issues/65143#issuecomment-543955294) since `Option` and `Result` were introduced, and it may eventually [improve enough](https://github.com/rust-lang/rfcs/pull/3573) to make variant-checking methods obsolete. For now, though, I personally think that these methods pay for themselves in readability.
Owner

So sounds like you are in the camp that would vote for adoption of RFC 3573? It appears mired in non-converging conversation, and it's not clear to me that the Rust leadership has any mechanism for creating a predictable period in which to expect action on a given RFC. If indeed you agree that it would be a good feature, then I think we should (a) commit to adopting the is syntax in Husht, and (b) in the meantime not have this method (to avoid repeating was is now seen as a wart in the standard library proliferation of these methods) and instead use one of the two forms in the RFC discussion that are said to map one-to-one to is, either an if let or a matches!() so that as Husht matures, the conversion will at some point rewrite this to the is syntax we both like better. Thoughts?

So sounds like you are in the camp that would vote for adoption of RFC 3573? It appears mired in non-converging conversation, and it's not clear to me that the Rust leadership has any mechanism for creating a predictable period in which to expect action on a given RFC. If indeed you agree that it would be a good feature, then I think we should (a) commit to adopting the `is` syntax in Husht, and (b) in the meantime not have this method (to avoid repeating was is now seen as a wart in the standard library proliferation of these methods) and instead use one of the two forms in the RFC discussion that are said to map one-to-one to is, either an `if let` or a `matches!()` so that as Husht matures, the conversion will at some point rewrite this to the `is` syntax we both like better. Thoughts?
Author
Member

I think it's best to keep Husht as close as possible to Rust in terms of features, for two reasons.

  • If a proposed Rust feature gets adopted by Husht but not by Rust, there's a danger that Rust will eventually adopt a conflicting version of the same feature.
  • I think that the more an "associated language" diverges from the base language, the harder it is for people to transfer skills in either direction. For example, Processing diverges substantially from Java. That made it harder for me to transfer my Java skills to Processing, and I think it also makes it hard for people to transfer their Processing skills to Java.

[…] to avoid repeating was is now seen as a wart in the standard library proliferation of these methods […]

I don't get the impression that it's seen as a wart. Lots of people are commenting on that tracking issue to say that they appreciate these methods and prefer them over existing alternatives.

I think it's best to keep Husht as close as possible to Rust in terms of features, for two reasons. - If a proposed Rust feature gets adopted by Husht but not by Rust, there's a danger that Rust will eventually adopt a conflicting version of the same feature. - I think that the more an "associated language" diverges from the base language, the harder it is for people to transfer skills in either direction. For example, Processing diverges substantially from Java. That made it harder for me to transfer my Java skills to Processing, and I think it also makes it hard for people to transfer their Processing skills to Java. > […] to avoid repeating was is now seen as a wart in the standard library proliferation of these methods […] I don't get the impression that it's seen as a wart. Lots of people are commenting on that [tracking issue](https://github.com/rust-lang/rust/issues/65143) to say that they appreciate these methods and prefer them over existing alternatives.
Owner

prefer them over existing alternatives [emphasis mine]

Exactly: the gist of the majority of such comments, in my relatively diligent reading of the thread (I mean, I did not read but a tiny fraction of the hundreds of hidden comments, but I did read all unhidden ones) is that the need for such methods exists because there is not a clean general is operator, and if there were, they would prefer that over the proliferation of is_borrowed etc.

> prefer them over _existing_ alternatives [emphasis mine] Exactly: the gist of the majority of such comments, in my relatively diligent reading of the thread (I mean, I did not read but a tiny fraction of the hundreds of hidden comments, but I did read all unhidden ones) is that the need for such methods exists _because_ there is not a clean general `is` operator, and if there were, they would prefer that over the proliferation of `is_borrowed` etc.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

I think it's best to keep Husht as close as possible to Rust in terms of features, for two reasons.

This is not the place to debate Husht, we have a whole repo for that, but my motivation for it is to provide a concise, readable, convenient Rust. I think I've made it clear that Civet:TypeScript::Husht:Rust is roughly my vision. One of the more minor alterations Civet does, and that I would expect Husht to do, is early adoption of JavaScript/TypeScript feature proposals. And so what if the feature ends up officially Rust-adopted slightly differently than we implemented it in Husht? We drop it from Husht and refactor our code. These things are just not big deals, in my experience. I was an early adopter of Civet and so went through several such refactors as Civet evolved. It was not particularly painful, and the comfort of coding in a concise, expressive, DRY language was well worth the effort.

In short, Husht is for my understandability and our productivity. I am not so worried about transference from Rust. CoffeeScript (sort of the same thing 4-8 years ago) became famous as a laboratory for JavaScript features, many of which were adopted into future JavaScript versions. I don't really expect that for Husht because Rust seems to attract folks on the somewhat pedantic/rigid side, who really seem to want a style monoculture, but it's always a remote possibility that if we really flesh out Husht, it could attract some following of people that want a style-variation-tolerant, concise language with the safety/webassemblable/whatever features of Rust. But I don't really care whether it does or not: For this project long term I need a comfortable language to read and develop in. It's even possible that some of our debates that we're getting hung up on are driven in part by Rust's verbose, rigid syntax.

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2271: > I think it's best to keep Husht as close as possible to Rust in terms of features, for two reasons. This is not the place to debate Husht, we have a whole repo for that, but my motivation for it is to provide a concise, readable, convenient Rust. I think I've made it clear that Civet:TypeScript::Husht:Rust is roughly my vision. One of the more minor alterations Civet does, and that I would expect Husht to do, is early adoption of JavaScript/TypeScript feature proposals. And so what if the feature ends up officially Rust-adopted slightly differently than we implemented it in Husht? We drop it from Husht and refactor our code. These things are just not big deals, in my experience. I was an early adopter of Civet and so went through several such refactors as Civet evolved. It was not particularly painful, and the comfort of coding in a concise, expressive, DRY language was well worth the effort. In short, Husht is for my understandability and our productivity. I am not so worried about transference from Rust. CoffeeScript (sort of the same thing 4-8 years ago) became famous as a laboratory for JavaScript features, _many_ of which were adopted into future JavaScript versions. I don't really expect that for Husht because Rust seems to attract folks on the somewhat pedantic/rigid side, who _really_ seem to want a style monoculture, but it's always a remote possibility that if we really flesh out Husht, it could attract some following of people that want a style-variation-tolerant, concise language with the safety/webassemblable/whatever features of Rust. But I don't really care whether it does or not: For this project long term I need a comfortable language to read and develop in. It's even possible that some of our debates that we're getting hung up on are driven in part by Rust's verbose, rigid syntax.
Author
Member

This is not the place to debate Husht […]

Agreed. I think you get the tradeoff at this point, so let me know whether you want SpecifiedValue to have an is_present method in this pull request.

> This is not the place to debate Husht […] Agreed. I think you get the tradeoff at this point, so let me know whether you want `SpecifiedValue` to have an `is_present` method in this pull request.
Owner

Well to me Occam says no "is_present," so that's my preference, so please go with that if you feel only a mild preference (or less) for having it. Since you're doing essentially all of the implementation at the moment, if you have more than a mild preference for having it, leave it in. Thanks for discussing the pros and cons.

Well to me Occam says no "is_present," so that's my preference, so please go with that if you feel only a mild preference (or less) for having it. Since you're doing essentially all of the implementation at the moment, if you have more than a mild preference for having it, leave it in. Thanks for discussing the pros and cons.
Author
Member

I've removed if_present, replacing it with matches!(/* ... */, Present { .. }) where it was used (c58fed0).

I've removed `if_present`, replacing it with `matches!(/* ... */, Present { .. })` where it was used (c58fed0).
glen marked this conversation as resolved
@ -116,0 +154,4 @@
// specification is empty, the `SpecifiedValue` is `Absent`. if the
// specification parses to a floating-point value `x`, the `SpecifiedValue` is
// `Present`, with a `value` of `x`, and the specification is stored in `spec`.
// these are the only valid specifications; any other produces an error
Owner

Maybe add "currently" -- "These are currently the only valid..." -- since we are definitely expecting to add other specifications (if we weren't, we would likely not go to this trouble).

Maybe add "currently" -- "These are currently the only valid..." -- since we are definitely expecting to add other specifications (if we weren't, we would likely not go to this trouble).
Author
Member

Done (7cbd926).

Done (7cbd926).
glen marked this conversation as resolved
@ -121,0 +177,4 @@
}
impl Regulator {
pub fn try_set(&self, set_pt_spec: String) -> bool {
Owner

Why the difference in the interface between SpecifiedValue::try_from and Regulator::try_set? I.e., it seems to me that they could both return a Result, or could both return a boolean success flag...

Why the difference in the interface between SpecifiedValue::try_from and Regulator::try_set? I.e., it seems to me that they could both return a Result, or could both return a boolean success flag...
Author
Member

We could indeed modify try_set so that it returns a clone of the Result<SpecifiedValue> computed from the given specification. This might be useful if the caller wants a detailed error report when the try_set fails. Right now, though, the code that calls try_set only wants to know whether the attempt succeeded.

The associated function try_from is a constructor, so it can't just return a success flag. Would revising the comment on the TryFrom implementation help communicate this? Readers already familiar with the TryFrom trait would know this already, but making documentation broadly accessible is often worthwhile.

We could indeed modify `try_set` so that it returns a clone of the `Result<SpecifiedValue>` computed from the given specification. This might be useful if the caller wants a detailed error report when the `try_set` fails. Right now, though, the code that calls `try_set` only wants to know whether the attempt succeeded. The associated function `try_from` is a constructor, so it can't just return a success flag. Would revising the comment on the `TryFrom` implementation help communicate this? Readers already familiar with the [`TryFrom`](https://doc.rust-lang.org/std/convert/trait.TryFrom.html) trait would know this already, but making documentation broadly accessible is often worthwhile.
Owner

OK, you're saying try_from generates a SpecifiedValue, so if it succeeds, we definitely need to provide that entity. That makes sense.

What seems a bit confusing is a different interface on what's almost the identical operation. Why would try_set have to return a clone? Doesn't Rust allow a read-only alias to a struct? Or doesn't the Regulator already have a ReadSignal for its SpecifiedValue? Couldn't a Result of that ReadSignal be provided in case of success or something like that? Presumably it's not hard to just check if what comes back is a Result rather than an Error, if that's all you care about?

OK, you're saying try_from generates a SpecifiedValue, so if it succeeds, we definitely need to provide that entity. That makes sense. What seems a bit confusing is a different interface on what's almost the identical operation. Why would try_set have to return a clone? Doesn't Rust allow a read-only alias to a struct? Or doesn't the Regulator already have a ReadSignal for its SpecifiedValue? Couldn't a Result of that ReadSignal be provided in case of success or something like that? Presumably it's not hard to just check if what comes back is a Result rather than an Error, if that's all you care about?
Author
Member

Or doesn't the Regulator already have a ReadSignal for its SpecifiedValue?

Yes: the set_point field is public. Thus, there's no need for try_set to return something that gives you access to the set point. If you have access to a regulator's try_set method, you also have access to its set_point field.

Why would try_set have to return a clone?

Here's the story as I vaguely understand it, based on my shaky knowledge of ownership in Rust. When the specification is valid, try_from returns a result of the form Ok(set_pt). We then call self.set_point.set(set_pt), which moves set_pt into the signal self.set_point. That means we can't return set_pt. I don't see a straightforward way to extract a reference to the value stored in a signal. I can use Signal::with to apply a closure to a reference to the stored value, but if I try to return that reference, the compiler can't infer an appropriate lifetime for it. Someone who understands lifetimes better might be able to make this work, but nothing I've tried has worked.

If your main goal is to return a detailed error report when try_set fails, we could have try_set return a Result<(), ParseFloatError>.

> Or doesn't the Regulator already have a ReadSignal for its SpecifiedValue? Yes: the `set_point` field is public. Thus, there's no need for `try_set` to return something that gives you access to the set point. If you have access to a regulator's `try_set` method, you also have access to its `set_point` field. > Why would try_set have to return a clone? Here's the story as I vaguely understand it, based on my shaky knowledge of ownership in Rust. When the specification is valid, `try_from` returns a result of the form `Ok(set_pt)`. We then call `self.set_point.set(set_pt)`, which *moves* `set_pt` into the signal `self.set_point`. That means we can't return `set_pt`. I don't see a straightforward way to extract a reference to the value stored in a signal. I can use `Signal::with` to apply a closure to a reference to the stored value, but if I try to return that reference, the compiler can't infer an appropriate lifetime for it. Someone who understands lifetimes better might be able to make this work, but nothing I've tried has worked. If your main goal is to return a detailed error report when `try_set` fails, we could have `try_set` return a `Result<(), ParseFloatError>`.
Owner

No, my goal is not to have two non-parallel interfaces for essentially the same operation. But you are right, currently the error info is simply lost when you use try_set rather than try_from. Which begs the question: if I already have a SpecifiedValue, is there a way to make it the set_point of a Regulator? Perhaps there should be; it seems as though it can't ever be wrong to make the set_point be any actual SpecifiedValue. And so maybe that would actually be the best solution to this nonparallelism, because there would only be one interface. To go from a possible string that might be a spec to changing the set_point of a Regulator, you first try_from it into a SpecifiedValue and then upon success set the set_point to that. Then you will at least receive the error info, which you can ignore if you like. Would such a refactoring actually be the cleanest interface? Looking forward to your thoughts.

No, my goal is not to have two non-parallel interfaces for essentially the same operation. But you are right, currently the error info is simply lost when you use try_set rather than try_from. Which begs the question: if I already have a SpecifiedValue, is there a way to make it the set_point of a Regulator? Perhaps there should be; it seems as though it can't ever be wrong to make the set_point be any actual SpecifiedValue. And so maybe that would actually be the best solution to this nonparallelism, because there would only be one interface. To go from a possible string that might be a spec to changing the set_point of a Regulator, you first try_from it into a SpecifiedValue and then upon success set the set_point to that. Then you will at least receive the error info, which you can ignore if you like. Would such a refactoring actually be the cleanest interface? Looking forward to your thoughts.
Author
Member

[…] if I already have a SpecifiedValue, is there a way to make it the set_point of a Regulator? Perhaps there should be […]

Yes, I think we should add a set method for this as soon as we have a reason to use it.

And so maybe that would actually be the best solution to this nonparallelism, because there would only be one interface.

Even if there were a set method, though, I would still want to keep try_set as a wrapper until we get to a point where we never have a reason to use it. I think that try_set is a concise way to express an operation that feels natural and self-contained.

> […] if I already have a SpecifiedValue, is there a way to make it the set_point of a Regulator? Perhaps there should be […] Yes, I think we should add a `set` method for this as soon as we have a reason to use it. > And so maybe that would actually be the best solution to this nonparallelism, because there would only be one interface. Even if there were a `set` method, though, I would still want to keep `try_set` as a wrapper until we get to a point where we never have a reason to use it. I think that `try_set` is a concise way to express an operation that feels natural and self-contained.
Owner

I am not yet fluent in Rust syntax, so I am sure the below are not correct, but why does something like

myReg.set(try_from('foobar'))

need an abbreviation

myReg.try_set('foobar')

Now a point might be that try_from doesn't give you a SpecifiedValue, it gives you a Result<SpecifiedValue, SomeKindaError>. But then wouldn't it be most natural just to overload set to also take such an entity, rather than create a new method? And then if you do want the error payload, you could first capture the result of try_from and match on it, and do whatever with the error if that's what matches? This factoring seems simpler and less redundant to me. I guess my point is, we are already setting Regulators, so we already have a reason for such a method; and we can already try to convert a string to a SpecifiedValue; and so we don't need a redundant combo of the two with a different interface, it seems to me...

I am not yet fluent in Rust syntax, so I am sure the below are not correct, but why does something like ``` myReg.set(try_from('foobar')) ``` need an abbreviation ``` myReg.try_set('foobar') ``` Now a point might be that try_from doesn't give you a SpecifiedValue, it gives you a `Result<SpecifiedValue, SomeKindaError>`. But then wouldn't it be most natural just to overload `set` to also take such an entity, rather than create a new method? And then if you do want the error payload, you could first capture the result of try_from and match on it, and do whatever with the error if that's what matches? This factoring seems simpler and less redundant to me. I guess my point is, we are _already_ `set`ting Regulators, so we already have a reason for such a method; and we can already try to convert a string to a SpecifiedValue; and so we don't need a redundant combo of the two with a different interface, it seems to me...
Author
Member

Now a point might be that try_from doesn't give you a SpecifiedValue, it gives you a Result<SpecifiedValue, SomeKindaError>.

Exactly.

But then wouldn't it be most natural just to overload set to also take such an entity, rather than create a new method?

As far as I know, Rust doesn't have function overloading, so the versions of set that take a SpecifiedValue and a Result<SpecifiedValue, ParseFloatError> would have to have different names. We could choose set and set_if_ok, for example. I could look through the standard library to find out what kind of naming might be idiomatic.

> Now a point might be that try_from doesn't give you a SpecifiedValue, it gives you a Result<SpecifiedValue, SomeKindaError>. Exactly. > But then wouldn't it be most natural just to overload set to also take such an entity, rather than create a new method? As far as I know, Rust doesn't have function overloading, so the versions of `set` that take a `SpecifiedValue` and a `Result<SpecifiedValue, ParseFloatError>` would have to have different names. We could choose `set` and `set_if_ok`, for example. I could look through the standard library to find out what kind of naming might be idiomatic.
Owner

Rust doesn't have function overloading

Wow that's kind of wild and seems to invite non-DRYness (repeating the type of the operand in the function name, in effect).

If this basic interface idea seems OK, then the names of the two flavors of set are up to you, and you can only implement the one you actually end up wanting to use if it's only one, but please do comment what you would plan to name the other one.

> Rust doesn't have function overloading Wow that's kind of wild and seems to invite non-DRYness (repeating the type of the operand in the function name, in effect). If this basic interface idea seems OK, then the names of the two flavors of set are up to you, and you can only implement the one you actually end up wanting to use if it's only one, but please do comment what you would plan to name the other one.
Owner

Rust doesn't have function overloading

Oh, this SO answer says the way to support myReg.set(aPlainSpecifiedValue) and myReg.set(aResultThatCouldBe) with the choice made at compile time based on the type of the argument is with a parametrized Trait, which is pretty much the same thing as supporting function overloading. So that would seem preferable to me to two different names; but if you strongly prefer to not use a trait and have two names in this case, and plan them out in this PR, I can live with that, too.

> Rust doesn't have function overloading Oh, [this SO answer](https://stackoverflow.com/a/56509699) says the way to support `myReg.set(aPlainSpecifiedValue)` and `myReg.set(aResultThatCouldBe)` with the choice made at compile time based on the type of the argument is with a parametrized Trait, which is pretty much the same thing as supporting function overloading. So that would seem preferable to me to two different names; but if you strongly prefer to not use a trait and have two names in this case, and plan them out in this PR, I can live with that, too.
Author
Member

The idea of using a parameterized trait is nice, but in this case I'd prefer to use methods named set and set_if_ok. The former always updates the set point, while the latter only updates the set point under a certain condition, and I like how the different names communicate that.

please do comment what you would plan to name the other one.

Just here, or in the code too?

The idea of using a parameterized trait is nice, but in this case I'd prefer to use methods named `set` and `set_if_ok`. The former always updates the set point, while the latter only updates the set point under a certain condition, and I like how the different names communicate that. > please do comment what you would plan to name the other one. Just here, or in the code too?
Owner

In the code, thanks.

In the code, thanks.
Author
Member

Done (894931a).

Done (894931a).
glen marked this conversation as resolved
@ -209,3 +275,1 @@
let key = self.constraints.update(|csts| csts.insert(constraint));
let subject_constraints = self.elements.with(
|elts| (elts[subjects.0].constraints, elts[subjects.1].constraints)
pub fn insert_regulator(&self, regulator: Regulator) {
Owner

Should this really be pub? Where would a client of Assembly get a "free-floating" regulator to insert? Seems, at least for the moment, like a private helper for insert_new_regulator, and indeed, at the moment it is only called from there.

Should this really be `pub`? Where would a client of Assembly get a "free-floating" regulator to insert? Seems, at least for the moment, like a private helper for `insert_new_regulator`, and indeed, at the moment it is only called from there.
Author
Member

Good point! I've made insert_regulator private for now (c368a38).

Good point! I've made `insert_regulator` private for now (c368a38).
glen marked this conversation as resolved
@ -212,0 +275,4 @@
pub fn insert_regulator(&self, regulator: Regulator) {
let subjects = regulator.subjects;
let key = self.regulators.update(|regs| regs.insert(regulator));
let subject_regulators = self.elements.with(
Owner

Isn't there a way in Rust to just do the equivalent of JavaScript's forEach on the two entries of subjects, looking the entry up in elts and updating the regulators of each by inserting key in them? I mean, rather than non-DRY-ly having to write two identical update calls?

Isn't there a way in Rust to just do the equivalent of JavaScript's `forEach` on the two entries of subjects, looking the entry up in elts and updating the regulators of each by inserting key in them? I mean, rather than non-DRY-ly having to write two identical update calls?
Author
Member

I think tuples are designed with arbitrary combinations of types in mind, so I'd be surprised to find utility methods to broadcast over tuples of the same type—and indeed, I haven't managed to find any.

If we change the subjects field from a tuple to a fixed-length array, as I'm expecting to do when we address issue #55, we can make this code less repetitive by iterating over the array.

I think tuples are designed with arbitrary combinations of types in mind, so I'd be surprised to find utility methods to broadcast over tuples of the same type—and indeed, I haven't managed to find any. If we change the `subjects` field from a tuple to a fixed-length array, as I'm expecting to do when we address issue #55, we can make this code less repetitive by iterating over the array.
Owner

Ah, this strongly suggests that the subjects field should be a fixed-length array, if there is no way to iterate over the entries of a tuple. So let's make that a definite part of the plan for #55, and I'll resolve this here.

Ah, this strongly suggests that the subjects field should be a fixed-length array, if there is no way to iterate over the entries of a tuple. So let's make that a definite part of the plan for #55, and I'll resolve this here.
glen marked this conversation as resolved
@ -215,0 +293,4 @@
reps.0.dot(&(&*Q * reps.1))
}
);
let set_point = create_signal(Absent);
Owner

Please illuminate me: Why do we not need to write SpecifiedValue::Absent here? I mean, I like the brevity, but I think I am unclear about when the bare variants are in scope...

Please illuminate me: Why do we not need to write `SpecifiedValue::Absent` here? I mean, I like the brevity, but I think I am unclear about when the bare variants are in scope...
Author
Member

The declaration

use SpecifiedValue::*;

puts all variants in scope. I currently have it just after the definition of SpecifiedValue, because it felt out of context elsewhere. For example, I usually expect use declarations at the top of the file to be external.

The declaration ``` use SpecifiedValue::*; ``` puts all variants in scope. I currently have it just after the definition of `SpecifiedValue`, because it felt out of context elsewhere. For example, I usually expect use declarations at the top of the file to be external.
Owner

Ah, the fact that use is sort of "lost" in the midst of the file suggests that SpecifiedValue should be in its own source file and imported here, so the relevant use is up where one would generally look. assembly.rs is getting pretty huge anyway... What are your thought about that factoring?

Ah, the fact that `use` is sort of "lost" in the midst of the file suggests that SpecifiedValue should be in its own source file and imported here, so the relevant `use` is up where one would generally look. assembly.rs is getting pretty huge anyway... What are your thought about that factoring?
Author
Member

Specified values and assemblies do seem like pretty independent concepts, so putting SpecifiedValue in its own module might be reasonable.

Specified values and assemblies do seem like pretty independent concepts, so putting `SpecifiedValue` in its own module might be reasonable.
Owner

Are you doing so in this PR? That's my (relatively mild) recommendation.

Are you doing so in this PR? That's my (relatively mild) recommendation.
Author
Member

Yes, I'll do this—I was just waiting for a go-ahead from you. I should've mentioned that in the last comment.

Yes, I'll do this—I was just waiting for a go-ahead from you. I should've mentioned that in the last comment.
Author
Member

Done (309b088).

Done (309b088).
glen marked this conversation as resolved
@ -215,0 +302,4 @@
/* DEBUG */
// print an updated list of regulators
console::log_1(&JsValue::from("Regulators:"));
Owner

Please refresh my memory: does this only fire when debugging is turned on in some way? If not, please file an issue to make debugging console logs compile in or out depending on something in the invocation.

Please refresh my memory: does this only fire when debugging is turned on in some way? If not, please file an issue to make debugging console logs compile in or out depending on something in the invocation.
Author
Member

Done, by filing issues #56 and #57.

Done, by filing issues #56 and #57.
glen marked this conversation as resolved
@ -215,0 +323,4 @@
console::log_1(&JsValue::from(
format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1)
));
if set_point.with(|set_pt| set_pt.is_present()) {
Owner

I take it the .with method is how we manipulate the "payload" (or whatever the correct term is) that is wrapped by a Signal?
And I further take it that since the argument 0-ary function to create_effect only accesses the Signal set_point, the effect will only "fire" when that signal's "payload" changes?

I take it the `.with` method is how we manipulate the "payload" (or whatever the correct term is) that is wrapped by a Signal? And I further take it that since the argument 0-ary function to create_effect only accesses the Signal `set_point`, the effect will only "fire" when that signal's "payload" changes?
Author
Member

I take it the .with method is how we manipulate the "payload" (or whatever the correct term is) that is wrapped by a Signal?

Yes: with applies a function to the signal's value and returns the result. It also causes the signal to be tracked when it's called in a reactive scope. (Its documentation seems unhelpfully terse to me.)

And I further take it that since the argument 0-ary function to create_effect only accesses the Signal set_point, the effect will only "fire" when that signal's "payload" changes?

Yup! I agree that this is a bit subtle; I'm on the fence about whether to spell it out in a comment.

> I take it the .with method is how we manipulate the "payload" (or whatever the correct term is) that is wrapped by a Signal? Yes: [`with`](https://docs.rs/sycamore/latest/sycamore/reactive/struct.ReadSignal.html#method.with) applies a function to the signal's value and returns the result. It also causes the signal to be tracked when it's called in a reactive scope. (Its documentation seems unhelpfully terse to me.) > And I further take it that since the argument 0-ary function to create_effect only accesses the Signal set_point, the effect will only "fire" when that signal's "payload" changes? Yup! I agree that this is a bit subtle; I'm on the fence about whether to spell it out in a comment.
Owner

No need to comment it if that is the standard MO of the reactive package we are using.

No need to comment it if that is the standard MO of the reactive package we are using.
glen marked this conversation as resolved
Vectornaut added 2 commits 2025-02-27 08:28:33 +00:00
Author
Member

Suppose you add an i.d. regulator to A and B, and set it to 2, and then it one of its input boxes (say with both showing), replace the set point with a and hit enter. You get an error indicator in that one input box, and the other still looks fine, and that is all good. But there now doesn't seem to be anything you can do to just "cancel" the failed update […]

You can press escape to cancel the failed update, as described at the end of this comment. I've added a comment about this to issue #38, which mentions other hard-to-discover keyboard controls.

> Suppose you add an i.d. regulator to A and B, and set it to 2, and then it one of its input boxes (say with both showing), replace the set point with a and hit enter. You get an error indicator in that one input box, and the other still looks fine, and that is all good. But there now doesn't seem to be anything you can do to just "cancel" the failed update […] You can press *escape* to cancel the failed update, as described at the end of [this comment](#issuecomment-2143). I've added a [comment](issues/38#issuecomment-2202) about this to issue #38, which mentions other hard-to-discover keyboard controls.
Author
Member

If you add an inversive distance regulator between A and B, and then add one between A and C, and then select B and C and add a regulator, presuming that neither of the dropdowns for B or C is open, there is precisely zero feedback that the operation occurred.

I've added your proposed fix as issue #59.

The interface allows me to select A and B, and add an inversive distance regulator, and then set that regulator, and then select A and B again and add another inversive distance regulator […]

I've proposed a fix as issue #60.

I don't think these issues are relevant to this pull request. They arose long before the pull request branch, and I don't think they can be fixed in passing by changes this pull request would naturally make.

> If you add an inversive distance regulator between A and B, and then add one between A and C, and then select B and C and add a regulator, presuming that neither of the dropdowns for B or C is open, there is precisely zero feedback that the operation occurred. I've added your proposed fix as issue #59. > The interface allows me to select A and B, and add an inversive distance regulator, and then set that regulator, and then select A and B again and add another inversive distance regulator […] I've proposed a fix as issue #60. I don't think these issues are relevant to this pull request. They arose long before the pull request branch, and I don't think they can be fixed in passing by changes this pull request would naturally make.
Author
Member

The current optimization seems to prefer changing curvature over position […]

I've brought this up in issue #61.

> The current optimization seems to prefer changing curvature over position […] I've brought this up in issue #61.
Author
Member

Need to file an issue to allow removal of entities, regulators and/or elements.

Done (#62).

> Need to file an issue to allow removal of entities, regulators and/or elements. Done (#62).
Author
Member

More immediately pertinent, but inspired by (4) because likely we will need to address it at least by the time we implement (4): it's counterintuitive/asymmetric that I can select an element but I can't select a regulator.

I've opened issue #63 to spotlight this, with references to relevant remarks in issue #44.

> More immediately pertinent, but inspired by (4) because likely we will need to address it at least by the time we implement (4): it's counterintuitive/asymmetric that I can select an element but I can't select a regulator. I've opened issue #63 to spotlight this, with references to relevant remarks in issue #44.
Author
Member

I think I've finished responding to this comment and addressing or responding to all the code-level comments. Please review again at your convenience! I'll be working on issue #64 in the meantime.

I think I've finished responding to [this comment](#issuecomment-2180) and addressing or responding to all the code-level comments. Please review again at your convenience! I'll be working on issue #64 in the meantime.
glen added 1 commit 2025-03-01 01:27:48 +00:00
Owner

OK, I agree the "six-item" comment is handled, and I have gone through the outstanding conversations and resolved or replied to each one. Let's get them all to the point of resolution -- even if it is say the filing of another issue -- before I give the whole thing another full review. Thanks!

OK, I agree the "six-item" comment is handled, and I have gone through the outstanding conversations and resolved or replied to each one. Let's get them all to the point of resolution -- even if it is say the filing of another issue -- before I give the whole thing another full review. Thanks!
Vectornaut added 3 commits 2025-03-03 21:14:04 +00:00
Vectornaut added 1 commit 2025-03-04 04:45:55 +00:00
Vectornaut added 1 commit 2025-03-04 07:14:03 +00:00
Author
Member

I've pushed changes that should resolve all the remaining conversations. If you agree, you can go ahead with another full review! Otherwise, let me know what's next for resolving the conversations.

I've pushed changes that should resolve all the remaining conversations. If you agree, you can go ahead with another full review! Otherwise, let me know what's next for resolving the conversations.
glen reviewed 2025-03-04 16:00:16 +00:00
@ -29,0 +58,4 @@
bind:value=value,
on:change=move |_| {
let set_pt_result = SpecifiedValue::try_from(value.get_clone_untracked());
valid.set(set_pt_result.is_ok());
Owner

This looks like the code is checking the OK-ness of the try_from twice. Is there a concise clear way to bind a variable to the SpecifiedValue payload in the OK return case and set valid to true and call the as-yet-unwritten regulator.set with that specified value, in that case, and in the alternative (Error) case, just set valid to false and do nothing to the regulator? That organization would seem to read more crisply, as long as it isn't too cumbersome to write... And in fact, if I recall, isn't that sort of thing what if let OK(blah) = try_from { ... } else {valid.set(false)} is for?

P.S. If the code here switched to something like this suggestion, then as this is the only instance of set_if_ok, you could remove that method in favor of a simpler set method, or in fact maybe just a direct call of regulator.set_point.set(...), eliminating the entire impl Regulator block at lines 127-135 of assembly.rs, which seems like a win.

So I guess I am specifically suggesting replacing this block with

if let Ok(spec) = SpecifiedValue::try_from(value.get_clone_untracked()) {
   valid.set(true)
   regulator.set_point.set(spec)
} else { valid.set(false) }

-- presuming the syntax is OK -- and just removing that impl Regulator block in assembly.rs altogether.

This looks like the code is checking the OK-ness of the try_from twice. Is there a concise clear way to bind a variable to the SpecifiedValue payload in the OK return case and set `valid` to true and call the as-yet-unwritten regulator.set with that specified value, in that case, and in the alternative (Error) case, just set `valid` to false and do nothing to the regulator? That organization would seem to read more crisply, as long as it isn't too cumbersome to write... And in fact, if I recall, isn't that sort of thing what `if let OK(blah) = try_from { ... } else {valid.set(false)}` is for? P.S. If the code here switched to something like this suggestion, then as this is the only instance of `set_if_ok`, you could remove that method in favor of a simpler `set` method, or in fact maybe just a direct call of `regulator.set_point.set(...)`, eliminating the entire `impl Regulator` block at lines 127-135 of assembly.rs, which seems like a win. So I guess I am specifically suggesting replacing this block with ``` if let Ok(spec) = SpecifiedValue::try_from(value.get_clone_untracked()) { valid.set(true) regulator.set_point.set(spec) } else { valid.set(false) } ``` -- presuming the syntax is OK -- and just removing that `impl Regulator` block in assembly.rs altogether.
Author
Member

So I guess I am specifically suggesting replacing this block with

if let Ok(spec) = SpecifiedValue::try_from(value.get_clone_untracked()) {
    valid.set(true)
    regulator.set_point.set(spec)
} else { valid.set(false) }

That sounds great to me. I'd reorganize the block to avoid the repetition of valid.set. Depending on whether you prefer if let or match, we could do either of these:

valid.set(
    if let Ok(set_pt) = SpecifiedValue::try_from(value.get_clone_untracked()) {
        regulator.set_point.set(set_pt);
        true
    } else { false }
)
valid.set(
    match SpecifiedValue::try_from(value.get_clone_untracked()) {
        Ok(set_pt) => {
            regulator.set_point.set(set_pt);
            true
        }
        Err(_) => false
    }
)

In the match version, the argument of valid.set is just an inlined version of the old Regulator::try_set method. If we want to go this route, and you don't have a strong preference for inlining, I'd recommend just reverting commit 894931a, which replaced Regulator::try_set with Regulator::set_if_ok. We can also rewrite try_set to use if let instead of match, of course.

> So I guess I am specifically suggesting replacing this block with > > ```rust > if let Ok(spec) = SpecifiedValue::try_from(value.get_clone_untracked()) { > valid.set(true) > regulator.set_point.set(spec) > } else { valid.set(false) } That sounds great to me. I'd reorganize the block to avoid the repetition of `valid.set`. Depending on whether you prefer `if let` or `match`, we could do either of these: ```rust valid.set( if let Ok(set_pt) = SpecifiedValue::try_from(value.get_clone_untracked()) { regulator.set_point.set(set_pt); true } else { false } ) ``` ```rust valid.set( match SpecifiedValue::try_from(value.get_clone_untracked()) { Ok(set_pt) => { regulator.set_point.set(set_pt); true } Err(_) => false } ) ``` In the `match` version, the argument of `valid.set` is just an inlined version of the old `Regulator::try_set` method. If we want to go this route, and you don't have a strong preference for inlining, I'd recommend just reverting commit 894931a, which replaced `Regulator::try_set` with `Regulator::set_if_ok`. We can also rewrite `try_set` to use `if let` instead of `match`, of course.
glen marked this conversation as resolved
Owner

Either of the last two is fine by me; I am not experienced enough with Rust to have formed a preference. So happy to defer to yours. All else being equal, the if let seems more concise. And as far as conciseness goes, what does the set_point.set() method return? If it happened to be "true" that would save two lines and a pair of braces in the match version, which would make that briefer. But this is a trivial point either way.

As far as whether to revive try_set: my philosophy is that if this is the only occurrence, then Occam says leave it inline; as soon as there are two (or more), DRYness says factor it into a regulator method. So unless you have a specific reason to (re-)create that regulator method, just leave it be.

Either of the last two is fine by me; I am not experienced enough with Rust to have formed a preference. So happy to defer to yours. All else being equal, the if let seems more concise. And as far as conciseness goes, what does the set_point.set() method return? If it happened to be "true" that would save two lines and a pair of braces in the match version, which would make that briefer. But this is a trivial point either way. As far as whether to revive try_set: my philosophy is that if this is the only occurrence, then Occam says leave it inline; as soon as there are two (or more), DRYness says factor it into a regulator method. So unless you have a specific reason to (re-)create that regulator method, just leave it be.
Owner

p.s. it would be sort of nice, and reasonably intuitive, if the default else-branch of an if let was just else {false}.

p.s. it would be sort of nice, and reasonably intuitive, if the default else-branch of an if let was just `else {false}`.
Vectornaut added 1 commit 2025-03-05 23:05:15 +00:00
This essentially reverts commit 894931a and then inlines `try_set` in
the one place where it's used. If we ever want to reuse that code, we'll
factor it out again.
Author
Member

Either of the last two is fine by me

Done (6eeeb1c).

p.s. it would be sort of nice, and reasonably intuitive, if the default else-branch of an if let was just else {false}.

If this is a Husht proposal, let's discuss it in the Husht repository.

> Either of the last two is fine by me Done (6eeeb1c). > p.s. it would be sort of nice, and reasonably intuitive, if the default else-branch of an if let was just else {false}. If this is a Husht proposal, let's discuss it in the Husht repository.
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

If this is a Husht proposal, let's discuss it in the Husht repository.

Not necessarily, mostly just musing/wondering what Rust's behavior actually is when there is no else clause... if every expression/statement has a value, what is the value of an if let when there is no else and the match fails?

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2303: > If this is a Husht proposal, let's discuss it in the Husht repository. Not necessarily, mostly just musing/wondering what Rust's behavior actually is when there is no else clause... if every expression/statement has a value, what _is_ the value of an if let when there is no else and the match fails?
Author
Member

Not necessarily, mostly just musing/wondering what Rust's behavior actually is when there is no else clause... if every expression/statement has a value, what is the value of an if let when there is no else and the match fails?

The value is (), as described in the Rust reference:

An if [or if let] expression evaluates to the same value as the executed block, or () if no block is evaluated. An if expression must have the same type in all situations.

The compiler seems to treat every block as having the potential to be evaluated, forcing every non-unit if or if let expression to have an else block. For example, the expression if true { 3 } won't compile unless you add an else block to convince the compiler that it has the same type in all situations.

> Not necessarily, mostly just musing/wondering what Rust's behavior actually is when there is no else clause... if every expression/statement has a value, what _is_ the value of an if let when there is no else and the match fails? The value is `()`, as [described](https://doc.rust-lang.org/reference/expressions/if-expr.html) in the Rust reference: > An `if` [or `if let`] expression evaluates to the same value as the executed block, or `()` if no block is evaluated. An `if` expression must have the same type in all situations. The compiler seems to treat every block as having the potential to be evaluated, forcing every non-unit `if` or `if let` expression to have an `else` block. For example, the expression `if true { 3 }` won't compile unless you add an `else` block to convince the compiler that it has the same type in all situations.
Author
Member

For example, the expression if true { 3 } won't compile unless you add an else block to convince the compiler that it has the same type in all situations.

This is one advantage of using enums, by the way. To make the code below compile, you have to either change one of the matching blocks to an unconditional else or add an unreachable else block. I think both options make the expression harder to read and maintain.

const ANIMAL: u8 = 0;
const VEGETABLE: u8 = 1;
const MINERAL: u8 = 2;

let pet_type = MINERAL;

/* ⚠️ won't compile ⚠️ */
let pet_example = if pet_type == ANIMAL {
    "puppy"
} else if pet_type == VEGETABLE {
    "cactus"
} else if pet_type == MINERAL {
    "pet rock"
};

With an enum, you can write exactly what you mean.

enum PetType {
    Animal,
    Vegetable,
    Mineral
}
use PetType::*;

let pet_type = Mineral;

let pet_example = match pet_type {
    Animal => "puppy",
    Vegetable => "cactus",
    Mineral => "pet rock",
};
> For example, the expression `if true { 3 }` won't compile unless you add an `else` block to convince the compiler that it has the same type in all situations. This is one advantage of using enums, by the way. To make the code below compile, you have to either change one of the matching blocks to an unconditional `else` or add an unreachable `else` block. I think both options make the expression harder to read and maintain. ```rust const ANIMAL: u8 = 0; const VEGETABLE: u8 = 1; const MINERAL: u8 = 2; let pet_type = MINERAL; /* ⚠️ won't compile ⚠️ */ let pet_example = if pet_type == ANIMAL { "puppy" } else if pet_type == VEGETABLE { "cactus" } else if pet_type == MINERAL { "pet rock" }; ``` With an enum, you can write exactly what you mean. ```rust enum PetType { Animal, Vegetable, Mineral } use PetType::*; let pet_type = Mineral; let pet_example = match pet_type { Animal => "puppy", Vegetable => "cactus", Mineral => "pet rock", }; ```
Owner

@Vectornaut wrote in glen/dyna3#48 (comment):

An if expression must have the same type in all situations.

Ah, right, because Rust doesn't have plain (non-discriminated) union types, and the if expression has to type. So if you wanted to return a u8 from one branch and a boolean from another branch, I guess you could define

enum MyBranches {
  Num(u8),
  Flag(boolean)
}
let result = if (condition) {Num(1)} else {Flag(false)}

(modulo some namespacing issues, likely) But I don't think you can pass a Flag() to something that wants a boolean, so this isn't too useful...

@Vectornaut wrote in https://code.studioinfinity.org/glen/dyna3/pulls/48#issuecomment-2305: > An `if` expression must have the same type in all situations. Ah, right, because Rust doesn't have plain (non-discriminated) union types, and the if expression has to type. So if you wanted to return a u8 from one branch and a boolean from another branch, I guess you could define ``` enum MyBranches { Num(u8), Flag(boolean) } let result = if (condition) {Num(1)} else {Flag(false)} ``` (modulo some namespacing issues, likely) But I don't think you can pass a Flag() to something that wants a boolean, so this isn't too useful...
Author
Member

But I don't think you can pass a Flag() to something that wants a boolean, so this isn't too useful...

There are various ways to streamline the use of MyBranches values in conditionals. Which one I'd choose, if any, would depends on what the type is supposed to represent. Let's start with the base definition of MyBranches:

enum MyBranches {
  Num(u8),
  Flag(bool)
}
use MyBranches::*;

let narrator_belief = Flag(false);

With this, we can already test Flag(_) values using identifier patterns:

if let Flag(_value @ true) = narrator_belief {
    print!("So, it's true...");
}

If we want a MyBranches to be broadly interpretable as a boolean, we could implement Deref<Target = bool> for it:

use std::ops::Deref;
impl Deref for MyBranches {
    type Target = bool;
    
    fn deref(&self) -> &Self::Target {
        match self {
            Num(_) => &false,
            Flag(value) => &value,
        }
    }
}

Then deref coercion would allow the compiler to see a MyBranches as a bool under some circumstances. However, we'd still have to dereference explicitly to use a MyBranches in an if expression:

if *narrator_belief {
    print!("So, it's true...");
}
> But I don't think you can pass a Flag() to something that wants a boolean, so this isn't too useful... There are various ways to streamline the use of `MyBranches` values in conditionals. Which one I'd choose, if any, would depends on what the type is supposed to represent. Let's start with the base definition of `MyBranches`: ```rust enum MyBranches { Num(u8), Flag(bool) } use MyBranches::*; let narrator_belief = Flag(false); ``` With this, we can already test `Flag(_)` values using [identifier patterns](https://doc.rust-lang.org/reference/patterns.html#identifier-patterns): ```rust if let Flag(_value @ true) = narrator_belief { print!("So, it's true..."); } ``` If we want a `MyBranches` to be broadly interpretable as a boolean, we could implement `Deref<Target = bool>` for it: ```rust use std::ops::Deref; impl Deref for MyBranches { type Target = bool; fn deref(&self) -> &Self::Target { match self { Num(_) => &false, Flag(value) => &value, } } } ``` Then [deref coercion](https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion) would allow the compiler to see a `MyBranches` as a `bool` under some circumstances. However, we'd still have to dereference explicitly to use a `MyBranches` in an `if` expression: ```rust if *narrator_belief { print!("So, it's true..."); } ```
Owner

I mean, a MyBranches value isn't like a reference to a bool, it's like a solid bool, with sometimes more information. So what we might want is to define an automatic type conversion (rather than a dereference). I don't think Rust goes in for that, although if I recall, C++ for example does. UPDATE: I have added a Husht item concerning this, and there's no need to discuss it further here.

I mean, a MyBranches value isn't like a _reference_ to a bool, it's like a solid bool, with sometimes more information. So what we might want is to define an automatic type conversion (rather than a dereference). I don't think Rust goes in for that, although if I recall, C++ for example does. UPDATE: I have added a [Husht item](https://code.studioinfinity.org/glen/husht/wiki/Other) concerning this, and there's no need to discuss it further here.
glen reviewed 2025-03-08 00:23:06 +00:00
@ -5,0 +5,4 @@
engine,
AppState,
assembly::{
Assembly,
Owner

In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line, like

    assembly::{Assembly, Element}

whereas here this has been spread across three lines. They should be consistent in format. If it's the same to you, I prefer a formats that balances the desire for fewer linebreaks (to keep a good amount of information visible on screen at one time) with the need for clear organization and readability (which too few linebreaks can engender). That is, I prefer the assembly.rs format.

In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line, like ``` assembly::{Assembly, Element} ``` whereas here this has been spread across three lines. They should be consistent in format. If it's the same to you, I prefer a formats that balances the desire for fewer linebreaks (to keep a good amount of information visible on screen at one time) with the need for clear organization and readability (which too few linebreaks can engender). That is, I prefer the assembly.rs format.
Author
Member

In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line […], whereas here this has been spread across three lines. They should be consistent in format.

Good catch. My convention has been to start with each use declaration on one line. If that line gets too long, I switch the outermost braced list from space-separated to newline-separated. Then I do the same thing recursively at lower list levels. The use declarations you noticed break this convention; I've corrected them In commit b9db7a5. I think the convention is followed everywhere else, but I'll keep an eye on it whenever I revise use declarations in future commits.

> In assembly.rs, you put all of the sub-namespaces under a top-level item on a single line […], whereas here this has been spread across three lines. They should be consistent in format. Good catch. My convention has been to start with each `use` declaration on one line. If that line gets too long, I switch the outermost braced list from space-separated to newline-separated. Then I do the same thing recursively at lower list levels. The `use` declarations you noticed break this convention; I've corrected them In commit b9db7a5. I think the convention is followed everywhere else, but I'll keep an eye on it whenever I revise `use` declarations in future commits.
glen marked this conversation as resolved
glen reviewed 2025-03-08 00:26:29 +00:00
@ -12,0 +10,4 @@
AppState,
assembly,
assembly::{
ElementKey,
Owner

Another use that needs to be harmonized with all our uses

Another `use` that needs to be harmonized with all our `uses`
Author
Member

Corrected, as discussed above.

Corrected, as discussed [above](#issuecomment-2317).
glen marked this conversation as resolved
glen reviewed 2025-03-08 00:40:05 +00:00
@ -0,0 +18,4 @@
impl SpecifiedValue {
pub fn from_empty_spec() -> SpecifiedValue {
SpecifiedValue { spec: String::new(), value: None }
Owner
  1. Would "none" or "empty" or "absent" be a simpler name for this function, and perhaps also closer to the actual semantics? It seems to me that what one wants is a a SpecifiedValue that is not present, rather than being tied to the representation as an empty string.
  2. Do you feel some non-DRYness between this and try_from("")? Wouldn't it be cleaner reuse either to implement this as the appropriate unwrapping of try_from(String::new()), or else in the try_from case implement the empty string case by returning the result of this function? We really only want the format of the canonical absent SpecifiedValue to be contained in one place, it seems to me.
1) Would "none" or "empty" or "absent" be a simpler name for this function, and perhaps also closer to the actual semantics? It seems to me that what one wants is a a SpecifiedValue that is _not_ present, rather than being tied to the representation as an empty string. 2) Do you feel some non-DRYness between this and `try_from("")`? Wouldn't it be cleaner reuse either to implement this as the appropriate unwrapping of try_from(String::new()), or else in the try_from case implement the empty string case by returning the result of this function? We really only want the format of the canonical absent SpecifiedValue to be contained in one place, it seems to me.
Author
Member
  1. The meaning I intended was "the value specified by the empty string," rather than "the canonically specified absent value." I think this works fine in both of the places where the function is used as of commit 08ec838. If we eventually have more than one way to specify an absent value, it might make sense to switch to a "canonically specified absent value" function, but ensuring consistency with try_from would get more complicated. For that reason, I'd want to make that switch in the context of actually wanting it, rather than guessing at what we might want in the future.
  2. Yes, from_empty_spec did have potential for inconsistency with try_from. Commit 08ec838 implements your suggestion of having try_from call from_empty_spec when the specification is empty.
1. The meaning I intended was "the value specified by the empty string," rather than "the canonically specified absent value." I think this works fine in both of the places where the function is used as of commit 08ec838. If we eventually have more than one way to specify an absent value, it might make sense to switch to a "canonically specified absent value" function, but ensuring consistency with `try_from` would get more complicated. For that reason, I'd want to make that switch in the context of actually wanting it, rather than guessing at what we might want in the future. 2. Yes, `from_empty_spec` did have potential for inconsistency with `try_from`. Commit 08ec838 implements your suggestion of having `try_from` call `from_empty_spec` when the specification is empty.
Owner

All right. Having the syntactic "trivial" generator of an unspecified SpecifiedValue (i.e. the one specified by an empty string) rather than a semantic one like "the canonical absent SpecifiedValue" seems a slightly odd emphasis to me (on syntax over semantics), but as you say, at the moment they are more or less equivalent because in fact there is only one syntactically correct specification of an absent SpecifiedValue. Hence resolving.

All right. Having the _syntactic_ "trivial" generator of an unspecified SpecifiedValue (i.e. the one specified by an empty string) rather than a _semantic_ one like "the canonical absent SpecifiedValue" seems a slightly odd emphasis to me (on syntax over semantics), but as you say, at the moment they are more or less equivalent because in fact there is only one syntactically correct specification of an absent SpecifiedValue. Hence resolving.
glen marked this conversation as resolved
Vectornaut added 2 commits 2025-03-08 23:12:21 +00:00
To reduce the potential for inconsistency, we should only have one piece
of code that constructs a `SpecifiedValue` from the empty specification.
glen reviewed 2025-03-10 19:36:54 +00:00
@ -27,2 +40,2 @@
Err(_) => constraint.lorentz_prod_valid.set(false)
};
class=move || {
if valid.get() {
Owner

Please could you explain to me the three moves in the three closures in this view! macro, the one that specifies the class of the input element, the one that specifies its change handler, and the one that specifies its keydown handler?

If I am understanding Rust correctly, the move annotation specifies that the closure should take ownership of any free variables that appear therein. So that would mean that the function that computes the class of the input takes ownership of the regulator. Why is that desired? Is it because the input element may not actually be realized until after the RegulatorInput function returns, and the ownership transfer extends the lifetime of the regulator until the closure has a chance to run and grab the info it needs to set the class (or perhaps actually, it has to persist that regulator indefinitely and then it can react to all future changes to the regulator)? The class-calculating closure of the input element seems like a slightly odd place for the long-term ownership of the regulator to reside, but maybe it doesn't really matter what owns it, as long as it persists indefinitely?

But it seems my suggestions in the previous paragraph can't possibly be correct, because regulator also occurs free in the closure passed as on:change, but the ownership of one entity can't exist in two places, if I am understanding correctly. So I guess please just explain to me what's going on here.

And finally, the third move seems the most confusing. For the life of me, I can't see any free variables in the closure passed as on:keydown, so there doesn't seem to be anything to move, and so the move shouldn't be there.

Thanks in advance for illuminating these things for me.

Please could you explain to me the three `move`s in the three closures in this `view!` macro, the one that specifies the class of the input element, the one that specifies its change handler, and the one that specifies its keydown handler? If I am understanding Rust correctly, the `move` annotation specifies that the closure should take ownership of any free variables that appear therein. So that would mean that the function that computes the class of the input takes ownership of the regulator. Why is that desired? Is it because the input element may not actually be realized until after the RegulatorInput function returns, and the ownership transfer extends the lifetime of the regulator until the closure has a chance to run and grab the info it needs to set the class (or perhaps actually, it has to persist that regulator indefinitely and then it can react to all future changes to the regulator)? The class-calculating closure of the input element seems like a slightly odd place for the long-term ownership of the regulator to reside, but maybe it doesn't really matter what owns it, as long as it persists indefinitely? But it seems my suggestions in the previous paragraph can't possibly be correct, because `regulator` also occurs free in the closure passed as `on:change`, but the ownership of one entity can't exist in two places, if I am understanding correctly. So I guess please just explain to me what's going on here. And finally, the third `move` seems the most confusing. For the life of me, I can't see _any_ free variables in the closure passed as `on:keydown`, so there doesn't seem to be anything _to_ move, and so the `move` shouldn't be there. Thanks in advance for illuminating these things for me.
Owner

Replying to your latest post here, to keep it in this resolvable conversation. Ah, the fact that Signal has Copy semantics makes this code much clearer. I didn't know that, and it seems a weird quirk of Rust to me that with ownership such a crucial concept to Rust, it is not clear whether copy or ownership transfer is in effect and I don't see any way you can know without consulting the source/docs for Signal.

Anyhow, I now get the first two moves. And now I see that reset_value is just a local closure, and that's what's being moved by the third move (for the keydown event). So that's good. Resolving.

Replying to your latest post here, to keep it in this resolvable conversation. Ah, the fact that Signal has Copy semantics makes this code much clearer. I didn't know that, and it seems a weird quirk of Rust to me that with ownership such a crucial concept to Rust, it is not clear whether copy or ownership transfer is in effect and I don't see any way you can know without consulting the source/docs for Signal. Anyhow, I now get the first two `move`s. And now I see that `reset_value` is just a local closure, and that's what's being `move`d by the third `move` (for the keydown event). So that's good. Resolving.
glen marked this conversation as resolved
Owner

Don't think I ever saw a response to:

Let me just see if I now understand: currently in the code the only way to produce a SpecifiedValue is via the try_from operation on a string; and if one wants to change the set point of a regulator, it must be done by creating a fresh SpecifiedValue and replacing the prior set point with the fresh one (not by modifying the SpecifiedValue that's in place). Please let me know either way if I've got that all straight. Thanks.

Don't think I ever saw a response to: > Let me just see if I now understand: currently in the code the only way to produce a SpecifiedValue is via the try_from operation on a string; and if one wants to change the set point of a regulator, it must be done by creating a fresh SpecifiedValue and replacing the prior set point with the fresh one (not by modifying the SpecifiedValue that's in place). Please let me know either way if I've got that all straight. Thanks.
Owner

OK, I think those two things are the only unresolved conversations, as far as I can see. We'll get them wrapped up, I will try running the latest version again, and assuming I don't see anything odd, we'll get this merged shortly. Thanks!

OK, I think those two things are the only unresolved conversations, as far as I can see. We'll get them wrapped up, I will try running the latest version again, and assuming I don't see anything odd, we'll get this merged shortly. Thanks!
Author
Member

Don't think I ever saw a response to

Whoops—I've now responded in that discussion.

> Don't think I ever saw a response to Whoops—I've now responded in that discussion.
Author
Member

If I am understanding Rust correctly, the move annotation specifies that the closure should take ownership of any free variables that appear therein. So that would mean that the function that computes the class of the input takes ownership of the regulator. Why is that desired? Is it because the input element may not actually be realized until after the RegulatorInput function returns, and the ownership transfer extends the lifetime of the regulator until the closure has a chance to run and grab the info it needs to set the class (or perhaps actually, it has to persist that regulator indefinitely and then it can react to all future changes to the regulator)?

Yes, I think so. One way to see this is to compile with one of the move keywords removed. The compiler will then complain that the closure may outlive the RegulatorInput function, but it borrows some variables which are owned by RegulatorInput.

But it seems my suggestions in the previous paragraph can't possibly be correct, because regulator also occurs free in the closure passed as on:change, but the ownership of one entity can't exist in two places, if I am understanding correctly. So I guess please just explain to me what's going on here.

I think this works because Signal implements the Copy trait, so the signals value, valid, and regulator.set_point are copied into the closures instead of being moved. Each closure ends up with its own copy of the signal, and each copy provides access to the same data. I think the following example shows the same phenomenon, because you get similar error messages if you remove any of the move keywords.

struct Inner {
    value: i32
}

struct Outer {
    inner: Inner
}

/* ⚠️ i'm not confident about the explanations in these comments ⚠️ ️*/
fn adders(mut outer: Outer) -> (Box<dyn Fn() -> i32>, Box<dyn Fn() -> i32>) {
    // if `outer.inner.value` had "move semantics", this would move it into
    // `add_one`, preventing us from using it later. however, since `i32`
    // implements the `Copy` trait, `outer.inner.value` has "copy semantics"
    // instead. its value is copied into `add_one`, allowing us to use it later
    let add_one = Box::new(move || outer.inner.value + 1);
    
    // this change to `outer.inner.value` doesn't affect the value that was
    // copied into `add_one`
    outer.inner.value *= 10;
    
    // the new value of `outer.inner.value` is copied into `add_two`
    let add_two = Box::new(move || outer.inner.value + 2);
    
    (add_one, add_two)
}

fn main() {
    let outer = Outer {
        inner: Inner {
            value: 70
        }
    };
    let (a, b) = adders(outer);
    print!("Original value plus one:   {}\nMultiplied value plus two: {}", a(), b());
}

If you want to apply these comments to the situation in our code, keep in mind that the word "value" is a little overloaded. In these comments, it would refer to the signal that a variable is holding, not the value that the signal is holding.

> If I am understanding Rust correctly, the move annotation specifies that the closure should take ownership of any free variables that appear therein. So that would mean that the function that computes the class of the input takes ownership of the regulator. Why is that desired? Is it because the input element may not actually be realized until after the RegulatorInput function returns, and the ownership transfer extends the lifetime of the regulator until the closure has a chance to run and grab the info it needs to set the class (or perhaps actually, it has to persist that regulator indefinitely and then it can react to all future changes to the regulator)? Yes, I think so. One way to see this is to compile with one of the `move` keywords removed. The compiler will then complain that the closure may outlive the `RegulatorInput` function, but it borrows some variables which are owned by `RegulatorInput`. > But it seems my suggestions in the previous paragraph can't possibly be correct, because regulator also occurs free in the closure passed as on:change, but the ownership of one entity can't exist in two places, if I am understanding correctly. So I guess please just explain to me what's going on here. I think this works because [`Signal`](https://docs.rs/sycamore/latest/sycamore/reactive/struct.Signal.html) implements the [`Copy`]() trait, so the signals `value`, `valid`, and `regulator.set_point` are copied into the closures instead of being moved. Each closure ends up with its own copy of the signal, and each copy provides access to the same data. I think the following example shows the same phenomenon, because you get similar error messages if you remove any of the `move` keywords. ```rust struct Inner { value: i32 } struct Outer { inner: Inner } /* ⚠️ i'm not confident about the explanations in these comments ⚠️ ️*/ fn adders(mut outer: Outer) -> (Box<dyn Fn() -> i32>, Box<dyn Fn() -> i32>) { // if `outer.inner.value` had "move semantics", this would move it into // `add_one`, preventing us from using it later. however, since `i32` // implements the `Copy` trait, `outer.inner.value` has "copy semantics" // instead. its value is copied into `add_one`, allowing us to use it later let add_one = Box::new(move || outer.inner.value + 1); // this change to `outer.inner.value` doesn't affect the value that was // copied into `add_one` outer.inner.value *= 10; // the new value of `outer.inner.value` is copied into `add_two` let add_two = Box::new(move || outer.inner.value + 2); (add_one, add_two) } fn main() { let outer = Outer { inner: Inner { value: 70 } }; let (a, b) = adders(outer); print!("Original value plus one: {}\nMultiplied value plus two: {}", a(), b()); } ``` If you want to apply these comments to the situation in our code, keep in mind that the word "value" is a little overloaded. In these comments, it would refer to the signal that a variable is holding, not the value that the signal is holding.
glen merged commit da28bc99d2 into main 2025-03-10 23:43:25 +00:00
Sign in to join this conversation.
No description provided.