ModelicaSpecification
ModelicaSpecification copied to clipboard
MCP-0034 Ternary
Opening a draft pull request for MCP-0034 in order to create a forum for discussing the MCP at the stage of having a prototype implementation, hoping that there will be a clear direction to follow when writing specification changes. Such preliminary discussion, combined with just a short delay for writing specification changes, will then provide MCP reviewers with fresh background that should help reaching the under evaluation stage in a timely manner.
Language group at 101th design meeting:
Poll: Scope of Ternary:
- Full-fledged built in type, similar to
Boolean: 6 - Defined as proposed, but then restricted in various ways to only be allowed inside annotations: 4
- Defined in a different way with meaning only inside annotations: 0
Conclusion: No change for now.
Poll: How to define Ternary:
- In the same way as
Boolean(usingTernaryTypeetc): 4 - Similar to
Clock, that is, not as a reserved name: 3 - Define as pre-defined
operator record: 2 - Abstain: 1
Conclusion: No change for now.
Poll: How to refer to unknown:
unknownas keyword: 5unknownas identifier: 3Ternary.unknownwithoutTernary.trueandTernary.false: 2Ternary.unknownwithTernary.trueandTernary.false: 1consensus(false, true): 0- Abstain: 1
Conclusion: No change for now.
Define how to map Ternary when exporting to FMI:
- Yes, but only for FMI 2 and later, as enumeration type with enumerators {false = -1, unknown = 0, true = 1}: 5
- Yes, and also for FMI 1, but without mapping to integer values for FMI 1: 2
- No, don't define it: 6
- Add non-normative comment about possibility to map to enumeration: 4
Conclusion: No change for now.
Poll: Make specification changes and start collecting reviews:
- Yes: 5
- No, revisit at another design meeting: 0
- No, give up: 0
- Abstain: 4
Conclusion: Make spec changes and collect reviews.
From phone-meeting: Ternary MCP-0034 -> Henrik?
- Gerd: Should have time to implement
@gkurzbach status?
@gkurzbach status? Will start this month.
After a year of inactivity, I am now about to start pushing specification changes to the MCP branch. The delay has been motivated by a long series of document-wide cleanups following the transition to the new text-based document source code format (LaTeX), many of which expected to have caused major merge conflicts with this MCP branch that I wanted to avoid. Now we have finally reached a state where I am hopeful that the changes for this MCP can be prepared without too much disturbance from other activities.
I am not sure where to best document the following, so I just do it here and we might later figure out where to place it with the MCP.
Side note: An issue with the MPC is that it does not refer to which exact logic is to be implemented (reference to the literature formally defining the logic and proofing basic properties). The MCP mentions Kleene and I assume this also means Kleene logic's meaning of implication and not Priest logic's. Giving a reference is good enough; the logic tables and interpretation are not required to be repeated in the MCP (avoid typos).
General opinion
I am not in favor of adding three-valued or any many-valued logic to Modelica.
I think that we can support such -- or should I better say the different flavours as they turn out to be needed -- in the form of a function library that can be used for the few cases it is needed by specialists that know what they do.
I do not think that non-specialists will be capable to use three-valued logic, particularly when models with such scale. In best case they never write such logic expressions but just happen to use some library "magically" handling the idiosyncratic nature of such; but then the question is "can a library not already do it?", which I believe is "yes!".
Rational (I consider worthwhile to amend the MCP)
There is not the three-valued logic, like Boolean algebra is for two-valued logic. There are many different; and when I say many, I mean more than most programmers ever will learn about, not to speak of physics modellers. Or as Wolfram MathWorld puts it: "There are 3072 such logics.". I don't know how they got to that number, but I liked to quote it for the bitter irony of the issue.
From my experience with these things, I draw the conclusion that every concrete domain/problem at hand tends to slightly modify three-valued reasoning to tailer it to the problem. There are at least two fundamental three-valued logic interpretations, (1) Kleene and Priest and (2) Łukasiewicz, with (1) having "slightly" different meanings between Kleene vs. Priest. One can not say that (1) or (2) is better; it depends on the application and non surpasses the other. In programming languages, often neither is used in its pure form -- if there exist three values for logic at all -- but instead some language-specific "thing", often around some nil construct. In logic programming, all kinds of systems exist and are successfully applied.
The reason is, that implication has no general useful interpretation in many-valued logics (including three-valued). But automatic reasoning -- and that more or less is the motivation to use logic expressions in the first place -- is based on implication.
The issue of implication → is best explained by examples (→ is defined as ¬A ∨ B).
Take the basic sequence of reasoning (A → B) → C implies A → C.
For Boolean logics this deduction holds, due to the equivalence of A ∨ B ≡ (A → B) → B because ¬(¬A ∨ B) ∨ B ≡ (A ∧ ¬B) ∨ B ≡ A ∨ B.
For three-valued logic this very basic deduction does not hold in general, and it depends which interpretation is used.
In Kleenes logic it does not hold, because we have for A=true and B=unknown that A ∨ B = true but (A → B) → B = unknown → unknown = unknown.
This is a surprise! And believe me, it will bite you in the but sooner or later when combining logic expressions.
Now we can attenuate that issue by using Łukasiewicz logic, where unknown → unknown = true. But here we get other issues because modus ponens (P ∧ (P → Q)) → Q is not a tautology and neither is the transitive rule for the conditional ((P → Q) ∧ (Q → R)) → (P → R). These are basic reasoning rules.
I am not going into details for other three-valued logic interpretations because the MCP doesn't mention nor propose these. I mentioned Łukasiewicz because at least this should be considered as alternative. But the point is: there is no three-valued logic interpretation that can claim to be universally accepted nor applicable; there is no standard interpretation and whatever we choose, it will be wrong for this or that use case. But even worse, on top of that its deductions will surprise you sooner or later and I am saying that with a big logics background from my computer science studies, being worried of unleashing such a thing on our user base.
Another open question not discussed in the MCP is what the interpretation of three-valued conditionals is.
Is the body of if C then evaluated when C is unknown or not? And in if C then ...A... else ...B... end if are the A and B bodies executed if C is unknown, both are not executed or only either?
I think the answer to that question very much depends on the application area/domain of the model. And that is what I am afraid of.
The whole situation is comparable to NaN values in floating point arithmetic, where we have the contradicting situation that in a program like
if a < b then
...A...
end if;
if a >= b then
...B...
end if;
neither, A nor B are executed if either a and/or b are NaN. NaN is the unknown of floating point operations and handling it is the big challenge. Our current Modelica answer is: "I don't care". But the whole purpose of a three-valued logic is to care.
I will try to illustrate the issue by an analogy of three-valued vs. Boolean two-valued logic. When I say "my mother has a car" and we know that "cars are good for driving", most people will conclude that "my mother can drive to pick me up". This is implicit two-valued logic, because we assume that "my mother has a driving license". In a three-valued logic, the conclusion can not be drawn because we don't know if "my mother has a driving license" (the fact is not stated anywhere, thus = unknown). She may have a car, and the car is not broken and good for driving and she has time to pick me up, but maybe she just owns it and can not drive it (the unknown). Humans are very bad with this stupid corner-case reasoning as typical with many-valued logics, because we always implicitly assume a lot of context packed/shipped with our statements. If I want to talk about that "my mother owns a car she is not driving" I will put it exactly like that, because I want to emphasize the corner case -- that is the pure purpose of talking about this situation -- whereas when I just say "my mother has a car" I want to communicate to you the point that she can pick me up and I don't have to use the public transport or whatever. The point is, that we tick boolean in reasoning.
In a likewise manner, I do not negate everything that is not the case, like "my mother has no driving license", except when I want to emphasize this fact. When it comes to automatic logic deduction however, three-valued logic immediately suffers from the fact that it is not sufficient to state the facts and some logic formulas, but your facts must also include the negation of the most basic human assumptions because otherwise they are concluded to be unknown, thus can result in the incapability to reason this or that (or funnily the opposite, start to conclude the most crazy things) -- well that is the purpose to introduce unknown in the first place. But if you really must give such simple rules like -- assume we are talking about time -- if A is previous to B then B is past A and if C is inbetween both, then B is neither before A nor C nor is A past C nor B ... nor is ... blablabla. If you leave any of these rules and facts then your system will happily start to conclude time travel scenarios or conclude nothing (depending on your three-valued interpretation of implication, see for example the two most basic viable options explained in my previous post), whereas if it is boolean everything that is not true is simply false and the reasoning is closed under this assumption (eventually allowing you to deduct a few things instead of just unknown, unknown unknonwn -- I know nothing and need more rules/facts). For example, most many-valued logics that want to reason about time have special rules or logic deductions for the time related things (because explicitly putting all the crazy, for humans obvious stuff, in rules to make the system useful will drive you crazy). To just use simple Kleene logic is not working except for the most simplistic use-cases (trivial conclusions, pille palle).
I can summarise my opinion on the topic as: Modelica is a physics simulation tool and not theorem proofer, and its users are not higher-valued logics experts.
@christoff-buerger, let me try to answer to your concerns. Before that, I'd like to start by giving some strong reasons to try to proceed with this MCP without tearing it up too much:
- We have spent many precious design meeting hours (probably even days) in big group discussions, leading to the conclusion that this MCP for a ternary type was needed.
- The current state of the MCP has also been discussed in several design meetings, with agreement on proceeding.
- We even have two test implementations.
Fortunately, I am hopeful that we can also get you onboard in the end. :)
When presented to someone with a stronger background in logic, like you, it is clearly misleading to say that this MCP wants to introduce "Ternary (3-valued logic)". A better description for someone like you would probably have been to call it something like "Ternary (a 3-valued truth type)", as the MCP does not intend to enter the muddy waters of all 3-valued logics.
To reassure you that the MCP is not about a 3-valued logic as you think of it, note that the MCP does not introduce an implies operator, and deliberately avoids introducing new functions corresponding to any particular 3-valued logic. I'd say that the main ReadMe.md of the MCP is misleading in this regard, and I would appreciate if you in the end could help finding the right way to present it.
OK, so what the MCP solves is the need for a three-valued type that can model {true, false, unknown}, and that can be used conveniently as an extension of the Boolean expressions we have today. In addition to the type itself, there are the truth tables, which I believe are worth spelling out in this audience instead of just pointing to a reference. As long as only not, and, and or are defined, this seems fairly uncontroversial to me, but there might be a problem in how they are presented. Would it be better to say that the truth tables are according to Kleene, but that there is no definition of a logical implication operator since Ternary and its operations is not meant to be a complete model of Kleene logic?
Another open question not discussed in the MCP is what the interpretation of three-valued conditionals is.
This is discussed and not an open question; it is one of the main points in the summary on the front page:
No implicit conversion to
Boolean.
That is, where the specification requires a Boolean expression, one cannot give a Ternary one instead.
I do not think that non-specialists will be capable to use three-valued logic, particularly when models with such scale. In best case they never write such logic expressions but just happen to use some library "magically" handling the idiosyncratic nature of such; but then the question is "can a library not already do it?", which I believe is "yes!".
The MCP was initiated because we needed this for the specification itself, and applications include uses that may even require in-tool evaluation during translation or model editing, so a library is not an option. Also, a library solution could never offer the same convenience as the first class language type proposed here, and could therefore completely fail to fulfill the needs that Ternary is meant for.
I can summarise my opinion on the topic as: Modelica is a physics simulation tool and not theorem proofer, and its users are not higher-valued logics experts.
Right. We should not use Ternary to build theorem proofers.
I think that could be enough to start with. Would it make sense to try to amend the MCP by being more careful about how it relates to 3-valued logic in general and Kleene logic in particular? To me, this seems to be mostly an issue with the front page and the rationale, and not with the main design document.
Thanks for the detailed answer Henrik!
I do not intend by any means to stop this MCP (as you say, I also agree that it is too late and to much time spend).
I am only worried about the long term implications to support 3-valued logic and are just not convinced this is really needed in Modelica. I agree, it looks easy and straight forward for simple issues at hand like visibility of dialogs. But I think it won't scale for more interesting applications than something like dialog visibility.
To reassure you that the MCP is not about a 3-valued logic as you think of it, note that the MCP does not introduce an implies operator
The Kleene truth tables of the MCP already define implication (since implication in Kleene is just ¬A ∨ B). The issues of this logic are there, regardless if we support a dedicated implication operator or not. The point of these issues I described above is not that Kleene is wrong or something. The issue is that complex expressions in that logic often yield very surprising results (for which reason other, more advanced logics are proposed). More complex 3-valued logic relations may be hard to develop and likely boil down to try and error for users or need for step-by-step debugging of logic expressions.
Right. We should not use Ternary to build theorem proofers.
But our users will see the 3-valued logic, and start trying it on more complex domains than just dialog visibility. I have this requirements engineering library of EDF in my mind that had been proposed some time ago, also as a motivation to add 3-valued logic (at the end it turned out it was kind of a 4-valued logic because of the need to model time ... voilà, there it goes). With the proposed 3-valued logic, this requirements engineering example will first become sort-of more easy to develop, but then turns out to fall short and then all the craziness comes in step by step as additional requirements and corner-cases.
the MCP does not intend to enter the muddy waters of all 3-valued logics
And I don't want it to be the opener for future proposals in that direction.
If we say: That's it, Kleene truth tables and nothing more -- even if hell freezes -- I am very much Ok with it. The Kleene 3-valued logic is the most simple straight forward many-valued logic. I consider it very limited in its application area however -- that is why it is not used for anything but most simple applications. If somebody comes in the future with "but that case of the three-valued logic also needs some consideration" we should pull the plug.
In my opinion, we can not on the one hand complain that Modelica is complicated, missing good tutorials and introduction material and wonder how to attract more users and then introduce every concept that looks promising for small issues but is known to be troublesome when scaled (modelling-wise scaled, regarding understanding of models and maintenance/development). These issues, complexity of the language, and adding all sorts of stuff are related.
@christoff-buerger I propose that the discussion is continued in #2958 to keep the discussion more concentrated and hopefully saving this main MCP issue from being completely drowned in loose comments on these matters.
Intuitively, I think we should satisfy common C (and many other programming language) practice that 0 is false.
I think that intention would be more of a source of errors than a convenience. In the same family of languages, everything but 0 also represents true, and this is an analogy that has to be broken. To avoid errors, I think we should not encourage reuse of the intuition that 0 means false.
Thus, I'd rather propose some mapping of the form:
While such an external language mapping could be made independently of the total order, I think it could be confusing that the external language mapping of values doesn't preserve the order.
What the current proposal gives us is something that I find more appealing than mapping Ternary(false) to 0, namely the analogy between and and min as well as or and max, combined with an external language mapping that preserves the ordering.
In the spirit of helping detecting errors in external code, this mapping would be even better:
| `Ternary(false)` | 1 |
| `unknown` | 2 |
| `Ternary(true)` | 3 |
However, I like the symmetry around unknown at 0, both for esthetic reasons and because it allows external code to be written based on signs (allowing compilers to see that all cases have been covered) as an alternative to checking for specific values.
@henrikt-ma I basically agree with you in all the points, but the different runtime mapping of the Boolean literals and the corrsponding Ternary literals is the main point which makes me struggleling during the implementation, because the things get complicated in some corner cases, more then I expected. Finally I got stuck because running out of time.
There is now a fairly extensive collection of examples: https://github.com/henrikt-ma/TernaryTest
Since the prototype uses the names __Wolfram_Ternary and __Wolfram_unknown, these are the names you will find in the examples. They can be renamed later if we want.
Right now, there are some examples that don't work in the System Modeler prototype, but as far as I can see there is nothing particularly tricky about these examples and it is ongoing work to fix them all. The reason I am announcing the collection even though it isn't yet fully supported by the prototype is that I'd like to start collecting feedback already now. For example, if there are aspects you find not covered by any example or if there's a problem with how the examples are organized, etc, please let me know so that I can improve the collection and also make sure all examples work as expected the prototype.
The widespread "shoudPass" typo (in the TernaryTest library) mentioned in yesterday's phone meeting has now been corrected.
Design meeting:
Questions:
- Does
unknownreally need to be keyword? - Can external C interface use enumeration?
- Inconsistent to allow explicit conversion from
Booleanwhen there is also implicit conversion? - Are we making sure that we are explaining what ternary logic is?
Does unknown really need to be keyword?
Alternatives:
unknownas keyword- Compact notation and always clear what
unknownmeans - Analogous to
falseandtruebeing keywords
- Compact notation and always clear what
Ternary.unknown- No new keyword
- Mentally similar to enumeration
- Similar to
StateSelect.never - Cannot easily also have
Ternary.trueandTernary.falsedue totrueandfalsebeing keywords - Could make
trueandfalseconstants in top level scope instead of keywords
unknownas built-in constant in top level scope- Won't break models using
unknownas identifier - Can deprecate use as identifier, for making it keyword in the future
- Can use
.unknownto avoid shadowing problems, but won't automatically work ifunknownbecomes keyword - Could make
trueandfalseconstants in top level scope instead of keywords
- Won't break models using
Ternary()- No problem with shadowing.
- No clear sign of this being the unknown value.
Two step solution with different end results:
- Introduce
unknownas top level constant and deprecate shadowing it (possibly also deprecate accessing it as.unknown). AllowTernary()for the odd case of suffering from shadowing during deprecation period. - One of the following directions:
- Make all of
false,unknown, andtruekeywords. - Make all of
false,unknown, andtruetop level constants.
Language group:
- Do 1 now, and postpone deciding 2.
Can external C interface use enumeration?
Alternatives:
- -1, 0, 1
- 1, 2, 3 (similar to Modelica enumerations)
typedef enum{MODELICA_FALSE, MODELICA_UNKNOWN, MODELICA_TRUE} Modelica_ternary;in ModelicaUtilities.h
Nobody wants the typedef with its clutter in the global namespace.
Majority in favor of 1, 2, 3.
Inconsistent to allow explicit conversion from Boolean when there is also implicit conversion?
- Reasons for having it:
- Makes code more clear when present, useful to express what was originally implicit in later intermediate representations such as "Flat Modelica".
- Resembles how enumeration is cast to
Integer(even though this can't be done implicitly). - Not hard to implement.
- Reasons for not having it:
- Consistency with
Integer→Realand enumeration→Integerconversions: explicit conversion if and only if there is no implicit conversion.
- Consistency with
Majority wants to have possibility to convert explicitly.
Are we making sure that we are explaining what ternary logic is?
Yes, we should make sure this makes it from the rationale documents to the specification changes.
Next steps
- Update test implementation
- Get language group support for continuing with making specification changes.
Of the Next steps in https://github.com/modelica/ModelicaSpecification/pull/2477#issuecomment-1467706014, the first has been completed. This brings us to the next item, which I'd like to put on the agenda for the upcoming phone meeting:
- Get language group support for continuing with making specification changes.
I have received local feedback on the proposal that I'm forwarding to the language group. It is the same old question about the external C mapping to int, and with this mapping also being expected to show up in other places such as result files and table data files, there's a fear of users mixing up Boolean and Ternary data.
By skipping 1 in the mapping, one could reliably detect when a Ternary table column is populated with Boolean data, even when all values are Ternary(true). From the point of view of the test implementation, it would mean no added complexity to use the mapping 2 (Ternary(false)), 3 (unknown), 4 (Ternary(true)) instead of 1, 2, 3. I'd appreciate if the language group would like to comment on the pros and cons of these two mappings, and conclude with a poll:
- Mapping A: 1 (
Ternary(false)), 2 (unknown), 3 (Ternary(true)) - Mapping B: 2 (
Ternary(false)), 3 (unknown), 4 (Ternary(true))
I have received local feedback on the proposal that I'm forwarding to the language group. It is the same old question about the external C mapping to
int, and with this mapping also being expected to show up in other places such as result files and table data files, there's a fear of users mixing upBooleanandTernarydata.By skipping 1 in the mapping, one could reliably detect when a
Ternarytable column is populated withBooleandata, even when all values areTernary(true). From the point of view of the test implementation, it would mean no added complexity to use the mapping 2 (Ternary(false)), 3 (unknown), 4 (Ternary(true)) instead of 1, 2, 3. I'd appreciate if the language group would like to comment on the pros and cons of these two mappings, and conclude with a poll:* Mapping A: 1 (`Ternary(false)`), 2 (`unknown`), 3 (`Ternary(true)`) * Mapping B: 2 (`Ternary(false)`), 3 (`unknown`), 4 (`Ternary(true)`)
I really do not understand why we do not use 0 for false. I see your point to avoid mixup with _Bool, but I honestly think there is no chance to avoid such -- anything non-zero is true in C; and any assumption about which values are used in practice by a C compiler and linked libraries is false.
But using a non-zero value for false just adds boilerplate code to C based implementations, with some funny intrinsic consequences. To represent false by 0 in C is so deeply build in the language design, it is related to pointer arithmetics, implementation of control-flow instructions on hardware side, compiler optimizations etc. I am afraid a non-zero false is just fighting the language and all the ecosystem around it.
All of this is of course, if we are concerned of C as implementation language at all -- it is not like the world is build around C. My understanding of your question is that we are concerned.
My take of the long discussion behind https://github.com/modelica/ModelicaSpecification/pull/2477#issuecomment-1467706014 was that we really didn't want a mapping that could easily be confused with the mapping of Boolean; the desirable property we got from {1, 2, 3} was that we at least didn't map any value the same as the Boolean false, so that mistakes could be detected by noticing that none of the Ternary values would have the effect of false if accidentally being treated as a Boolean in the external function type mapping.
With Ternary(false) being mapped to 0, I see a big risk of not detecting when unknown and Ternary(true) are mixed up in the external functions, especially if there are not many unknown values in the test data.
Also note that with the Ternary values mapping to {2, 3, 4}, implementations will almost certainly detect when the external function makes the mistake of returning a value with the encoding for Boolean, while the mapping {1, 2, 3} would mean that a function always returning true, incorrectly encoded as 1 (meaning false), could not be detected by the external language interface. Of course, chances of detecting problems in return values would be drastically reduced by the mapping {0, 1, 2}, where both false and true would map to valid values for a Ternary. With {2, 3, 4}, the only way a truth value could be incorrectly returned as a Boolean without being detected is when true has the numeric value 4 in the C code, and is returned without canonicalization to 1; in practice extremely unlikely to happen.
From reading this, I get the impression that detecting an accidental misuse of Boolean where Ternary was called for is the most important aspect to consider in choosing a mapping onto a numerical range. But what about avoiding an error (minimize the probability for an unintended return value) or minimizing error effects? Given that a mapping {unknown, false, true} onto {-1, 0, 1} is rather widespread and that the range {0, 1, 2} nicely aligns with ternary digits and that from the interpretation of undecidable the range {0, 0.5, 1} lends itself, we see that in all (!) of these common mappings false and true will be correctly interpreted, e.g., automatic conversion from Boolean to Ternary. The only value that is different when returned, e.g., -1 or 2 or 0.5, can safely be assumed to correspond to unknown making the probability of errors lower—not higher—imho? The range {2, 3, 4} appears highly unusual...
I can see the benefit of the mapping to 1, 2, 3; since that makes Ternary close to a built-in enumeration type (and we could possibly extend enumeration types later so that it is just a built-in enumeration type).
Obviously one can argue that Modelica should be closer to C for enumerations in general, which would argue for 0, 1, 2 - but that would be a breaking change.
I can see the benefit of the mapping to 1, 2, 3; since that makes Ternary close to a built-in enumeration type (and we could possibly extend enumeration types later so that it is just a built-in enumeration type).
Obviously one can argue that Modelica should be closer to C for enumerations in general, which would argue for 0, 1, 2 - but that would be a breaking change.
But adjusting 1-indexing to 0-indexing is easy to do, the question remains then, whether it will be {false, true, unknown} or {false, unknown, true}. Even if you decide to go for 1, 2, 3, the likeliness for a misrepresentation might be lower if unknown were to be outside the range and not inside and thus unknown === 3?
I think a very strong reason to avoid mapping Ternary(false) to 0 in C is that it like a trap: as long as the value is Ternary(false), you get away with the Boolean interpretation of an int in C, but as soon as you do so you will typically also interpret any non-zero value as true, and the error won't be detected until the day someone passes the value unknown to the function and the user actually realizes that it is being misinterpreted as Ternary(true).
For those who prefer a 1-based mapping similar to enumerations, this can be combined with a safe mapping to {2, 3, 4}, by internally using an enumeration corresponding to:
type Ternary_enum = enumeration(Ternary_error, Ternary_false, Ternary_unknown, Ternary_true);
When receiving values from an external C function, one can treat it as an enumeration, but instead of verifying that the value is in 1..4, one just modifies the test or adds an extra test that will reject Ternary_error.
Looking at the original motivation this seems to have become way too complicated compared to the problem it was intended to solve, i.e., handling a tool-specific default for visible as a third-value - such that a user can say something like visible=if x then true else <keep tool-specific default>.
To implement that we need:
- A built-in tree-valued type
- Some kind of overloading - so that visible can be Boolean or three-valued.
- Possibility to define handling of
and/or(will return to).
The first part is to check what we currently have and in MSL we have 4-valued (Modelica.Electrical.Digital.Interfaces.UX01) and 9-valued logic (Modelica.Electrical.Digital.Interfaces.Logic), and built-in in the language we have one type that is sort of 5-valued logic (StateSelect - similarly as visible it allows a tool to have a default for a binary choice and the user to override it to force the underlying binary choice to be true or false). I believe the reason we missed the 4-valued and 9-valued logic in the original discussion is that we unfortunately didn't consider language and standard library as one entity.
As you might guess they are all enumerations (a concept that was added together with StateSelect in 2.0).
So, why not accept that Ternary is another built-in enumeration (as already suggested). Since we cannot name it Ternary.false call it Ternary.ForceFalse or Ternary.never (following StateSelect). Nothing special for conversion in expressions - similarly as for StateSelect one will have to write: stateSelect=if enforceStates then (if useQuaternions then StateSelect.always else StateSelect.never) else StateSelect.avoid).
Note that for visible the common cases will be: if x then Ternary.always else Ternary.default and if x then Ternary.never else Ternary.default (or possibly some other name than Ternary); and to me not having a conversion between Boolean and Ternary actually makes this more readable. If the name is good enough we might even deprecate using a Boolean for visible - thinking that visible=Ternary.always is clearer than visible=true.
The benefits of this are:
- The language and library stays consistent for users, and doesn't add much to the mental model. Basically having 3-, 4-, 9-valued (and kind of 5-valued) logic handled in a similar manner reduces the threshold for new users. This also means that the external interface just works like any other enumeration (if anyone actually uses them in external C-functions). If we make any simplifications it should be across all enumerations.
- Consistent user-interface reducing the burden for users and developers: e.g., code completion, plotting etc, can work consistently between 3-, 4-, 9-valued logic.
- Shorter time to add to MSL. There are blocks for handling operations on 4- and 9-valued logic; with connectors as inputs (and outputs) - users expect the same for 3-valued. By having it as a built-in enumeration we can add it to MSL-repo as a top-level class together with blocks using 3-valued logic that will work right now, without waiting for a new ModelicaSpec (and having it used in MSL).
Note that this means that and/or will be blocks similarly as for 4- and 9-valued logic. (How they are implemented is of less importance - internally they can use min/max for 3-valued logic or tables as the 4- and 9-valued logic.)
Note that Boolean is kind of the exception here, one reason is that Boolean expressions can be used to control if an equation is present or not - a binary decision that doesn't have a real counter-part in the other logics. Another is that we for some reason started enumeration-counting at 1 not 0.
We can then later build on this - if needed; I guess there's a reason most computer languages don't have 3-valued logic built in. For the C-interface we might, similarly as for records, consider how to extend the mapping to specific C-enumerations.
Let's not forget that the initial reason for creating this MCP was that you blocked any progress on dialog visibility control due to the lack of a ternary type in the language. Back then, it meant that there was a lot of work that had to be done for such a simple thing as a ternary visibility flag, but the idea of a Ternary in the language seemed useful enough that I considered it worthwhile trying to get this sort of obstacle out of the way once and for all. In endless design meetings since the beginning of 2020, the language group has guided the design towards the current state of the MCP. I must say I find too late to all of a sudden request a total change of direction now, but instead of discussing whether it's too late or not, let me give some good reasons for sticking with the current design.
To break the current separation of language and standard library design by making them cyclically dependent would be a disaster in my opinion. It is bad enough that ModelicaServices isn't defined in the specification, but at least the rest of the specification doesn't depend upon ModelicaServices, so let's not put Ternary in ModelicaServices either.
Unlike Ternary, it seems right to me that the domain-specific enumerations for digital circuits in Modelica.Electrical.Digital are defined within the library. For example, I really don't want to see Digital.Interfaces.UX01 being used in the standard language annotations. The Ternary of this MCP, on the other hand, is not a domain-specific type. It serves a much more basic and generic purpose in the language, and as we know from the background of this MCP, it is the kind of thing we would like to make use of in standard language annotations.
A Ternary with implicit conversion from Boolean is very user-friendly compared to a built-in enumeration, as the user doesn't have to keep track of which true or false to use when expressing truth values.
The implicit conversion from Boolean also means that functions and annotations can be enhanced in a backwards compatible manner by changing function inputs or annotation flags from Boolean to Ternary. Existing code where Boolean expressions are used will still work, but a function input can be made optional with more intelligent behavior than just defaulting to a Boolean expression in the other inputs, and an annotation can explicitly default to unknown instead of having special rules applying when no value is given (which no expression that could also be true or false can represent).
Similar to the lack of blocks for StateSelect in the MSL, I don't see a strong need for Ternary blocks in the MSL, and it might even be desirable to not introduce such blocks until we have identified a block-oriented modeling domain where Ternary is a good model of the data in connectors.
(Apologies if duplicated.)
Let's not forget that the initial reason for creating this MCP was that you blocked any progress on dialog visibility control due to the lack of a ternary type in the language. Back then, it meant that there was a lot of work that had to be done for such a simple thing as a ternary visibility flag, but the idea of a Ternary in the language seemed useful enough that I considered it worthwhile trying to get this sort of obstacle out of the way once and for all. In endless design meetings since the beginning of 2020, the language group has guided the design towards the current state of the MCP. I must say I find too late to all of a sudden request a total change of direction now, but instead of discussing whether it's too late or not, let me give some good reasons for sticking with the current design.
To me it sometimes makes sense to step back and reconsider, and simplify the design based on previously considered variants. This MCP seems to have taken some basic ideas and run far ahead with them - without considering what already exists.
A Ternary with implicit conversion from Boolean is very user-friendly compared to a built-in enumeration, as the user doesn't have to keep track of which true or false to use when expressing truth values.
That is not generally true, but depends on the tool support (or lack thereof).
For a declared enumeration Ternary Dymola will currently:
- Have drop-down boxes in the parameter dialog (with optional description texts).
- Allow code completion so that you can just type
Ternary.and hit Ctrl-Space to get a list of elements.
In that case an implicit conversion is in fact less user-friendly, unless something is added for that as well.
Obviously, it would be possible to support both - but it will still add to the cost in tool and to the user's mental model; and I'm not sure if it will ever become even as user-friendly as an enumeration.
Adding new features, because the current ones (such as enumeration) aren't convenient enough, isn't a good way forward.
Similar to the lack of blocks for StateSelect in the MSL, I don't see a strong need for Ternary blocks in the MSL, and it might even be desirable to not introduce such blocks until we have identified a block-oriented modeling domain where Ternary is a good model of the data in connectors.
If we don't see a major need for Ternary blocks (or functions) in MSL (or other similar libraries), then I'm not sure if there is sufficient use of Ternary outside of annotations to make it more convenient.
I'm not saying that there's no use for Ternary in model, just that I don't see it as significant enough to motivate the added complexity.
Language group: Markus: Need operations, don't think a lot of people will use in C-code. Gerd: Gave up implementing (too much effort)
Markus: Need operations, don't think a lot of people will use in C-code.
It is not the expectation that there will be lots of Ternary inputs and outputs of external functions that is the main motivation for specifying the mapping to integers. A much more important motivation is that the external language mapping is the de facto convention for data interchange outside the scope of the specification. For example, how to store simulation results, or how to represent a table on file for use with the MSL tables. If the de facto convention leaves the tools without any guidance when it comes to Ternary, tools will inevitably have to come up with their own tool-specific mappings, and this would cause a large amount of unnecessary debate and headache the day we regret that we didn't agree on a mapping form the beginning.
That the external function value mapping is the de facto convention for other kinds of data interchange also means that if we agree on a mapping which makes it easy to detect mistakes when using external functions, it will also become easy to detect mistakes when dealing with things like simulation results and table data. In the end, these could be more important reasons to support easy error detection than the error detection in external functions. The less overlap with the mappings of Boolean and Ternary, the better.
Once we have agreed upon a mapping, there are good reasons to actually specify that this mapping should be used by the external language interface:
- We don't need to come up with another way of stating what the convention is.
- It avoids leaving the specification with a strange gap in the external language type mappings.
- The little implementation effort is very small.
I also heard from @qlambert-pro that there were some questions in the meeting about the completeness of our test implementation. The easiest way to assess this is to look at the test library https://github.com/henrikt-ma/TernaryTest, which is fully supported. For those who find it boring to just look at the test library code, I can give a live demo where we can both look at the examples in the test library, as well as other cases brought up by the audience.
Please react with :thumbsup: in case you are interested!
Markus: Need operations, don't think a lot of people will use in C-code.
It is not the expectation that there will be lots of
Ternaryinputs and outputs of external functions that is the main motivation for specifying the mapping to integers. A much more important motivation is that the external language mapping is the de facto convention for data interchange outside the scope of the specification. For example, how to store simulation results, or how to represent a table on file for use with the MSL tables. If the de facto convention leaves the tools without any guidance when it comes toTernary, tools will inevitably have to come up with their own tool-specific mappings, and this would cause a large amount of unnecessary debate and headache the day we regret that we didn't agree on a mapping form the beginning.
And we should take the logical next step, and see if we can avoid even having to ask those questions.
We currently have the following kinds of types in Modelica:
- Non-primitives:
- record
- array
- Primitive:
RealIntegerStringBoolean- enumerations (including StateSelect, 4- and 9-valued logic in MSL)
Adding a new kind of primitive type has proven to be a substantial effort (based on experience with adding enumerations), and thus doing it needlessly would be an unnecessary hurdle for all Modelica users and implementers especially as Ternary can naturally fit into one of these existing categories; as an enumeration. (Alternatively it could be a record for "option type" with two booleans, but that would be messier.)
Apart from not increasing the burden internally in tools (including code completion, drop-down menus for parameters, plotting) that also removes the need to consider the external C-interface, exporting to FMI, as all of that is already defined for the existing categories, and simplifies the user's mental model.
Supporting a reasonable Ternary that solves the original issue shouldn't take more code than this comment. Going back to the original issue I also realize that we don't have consider compatibility for the new Ternary visible (or active?) as it doesn't replace an existing Boolean visible (or active?).
That's why the lack of second implementation should have been a major warning flag.