Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3696f8b
Initial commit of files on MCP branch
henrikt-ma Nov 22, 2019
b7e8ce4
Add pseudo code definition of Ternary
henrikt-ma Nov 22, 2019
38b433e
Define Ternary relations by means of total order, making Ternary usef…
henrikt-ma Nov 22, 2019
7933c90
Mention the Boolean option type alternative in the rationale
henrikt-ma Nov 24, 2019
4f05aa8
Elaborate on the interpretation of 'none' as either 'unknown' or 'und…
henrikt-ma Dec 5, 2019
3715b9b
Fix incorrect markdown for issue link
henrikt-ma Dec 17, 2019
d2b42b0
Tidy up issue link
henrikt-ma Dec 17, 2019
fa8fa14
Updates after gaining experience with the prototype
henrikt-ma Feb 9, 2020
a5164b5
Reuse 'none' instead of introducing 'neither' when giving example of …
henrikt-ma Feb 9, 2020
e99fa56
Fix typo
henrikt-ma Jun 12, 2020
f27fb06
Fix language
henrikt-ma Jun 12, 2020
5d62e4f
Fix typo
henrikt-ma Jun 10, 2021
50b985e
Fix error in example of Ternary ==
henrikt-ma Jun 10, 2021
379c0bc
Logical connectives are defined _in accordance with_ Kleene
henrikt-ma Jun 21, 2021
0aa42c1
Clarify that Ternary is not Kleene
henrikt-ma Jun 21, 2021
cf846b7
Clarify uniqueness in the external language mapping of values
henrikt-ma Oct 12, 2021
a59d27e
Merge remote-tracking branch 'central/master' into MCP/0034
henrikt-ma Oct 12, 2021
a44062a
Mention collection of examples provided in separate repository
henrikt-ma Oct 25, 2021
397ee9c
Merge remote-tracking branch 'central/MCP/0034' into MCP/0034+ternary…
henrikt-ma Oct 25, 2021
b62a65f
Merge pull request #2958 from modelica/MCP/0034+ternary-is-not-kleene
christoff-buerger Oct 29, 2021
dd4a298
Change external language mapping of Ternary to int
henrikt-ma Mar 14, 2023
1d896a3
Change 'unknown' from keyword to top-level constant
henrikt-ma Mar 24, 2023
647a2c8
Add another line to the revision history
henrikt-ma Mar 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions RationaleMCP/0034/ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Modelica Change Proposal MCP-0034<br/>Ternary
Henrik Tidefelt

**(In Development)**

## Summary
It was concluded in [#2211](https://github.com/modelica/ModelicaSpecification/issues/2211) that an MCP for _Ternary_ (3-valued logic) in Modelica should be created, and here it is.

The proposal is based on the following principles:
- A new built-in type, `Ternary`.
- A new top-level constant `unknown` for the third truth value.
- Explicit as well as implicit conversion from `Boolean`.
- No implicit conversion to `Boolean`.
- No new built-in functions.

See [design](ternary.md) for details.

## Revisions
| Date | Description |
| --- | --- |
| 2019-11-22 | Henrik Tidefelt. Filling this document with initial content. |
| 2021-10-25 | Henrik Tidefelt. Introducing collection of examples. |
| 2023-03-24 | Henrik Tidefelt. Changing `unknown` to be top-level constant. |

## Contributor License Agreement
All authors of this MCP or their organizations have signed the "Modelica Contributor License Agreement".

## Rationale
See [separate document](rationale.md).

## Backwards Compatibility
As built-in types are treated similar to keywords, the introduced `Ternary` introduces a backwards incompatibility with code making use of that name for identifiers.
By introducing `unknown` as a top-level constant rather than a keyword, this will not break code making use of this name for identifiers.
By deprecating uses of `unknown` that wouldn't work if `unknown` was a keyword, it will be possible to later align the syntaxes of `true`, `false`, and `unknown`.

## Tool Implementation
A prototype has been implemented in a development version of Wolfram SystemModeler. In the prototype, the type is named `__Wolfram_Ternary`, and the third truth value is named `__Wolfram_unknown`. The prototype currently has `__Wolfram_unknown` as a keyword, but will be updated to have a top-level constant instead.

The prototype is complemented with a collection of example models, provided in a separate repository: https://github.com/henrikt-ma/TernaryTest

### Experience with Prototype
Although introducing a new built-in type is a change that ammounts to a large number of smaller changes, finding the places in a code base that need attention is easy due to the similarity between `Ternary` and `Boolean`. In a similar way, the implicit conversion from `Boolean` to `Ternary` is a feature that can be implemented by glancing at the handling of implicit conversion from `Integer` to `Real`.

There are still a few examples in the collection that are not supported by the prototype. Fixing the the last examples is work in progress and none of them seem tricky.

Regarding the application of this MCP to a new attribute called `visible` in the `Dialog` annotation, the proposed choice of defining `not`, `and` and `or` in accordance with Kleene logic certainly gets the job done. The default value of `unknown` which means _apply tool-specific rules for visibility_, a user can easily write a ternary logical expression to override the default in either way, and the expression may also evaluate to `unknown` in cases where the user doesn't want to fall back on the tool logic. Thanks to the implicit cast from `Boolean`, most uses of `visible` would probably be in the simple forms `visible = true` or `visible = false`. However, upon closer acquaintance with the Kleene logic – which still appears as the most natural basis for defining the logical connectives for `Ternary` – it has been questioned whether this is the best way to model the absence of information, see [rationale](rationale.md#The-option-type-alternative).

## Required Patents
To the best of our knowledge, there are no patents that would conflict with the incorporation of this MCP.

## References
- Wikipedia, _Three-valued logic_, https://en.wikipedia.org/wiki/Three-valued_logic
- Wikipedia, _Four-valued logic_, https://en.wikipedia.org/wiki/Four-valued_logic
116 changes: 116 additions & 0 deletions RationaleMCP/0034/rationale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Background
For those who don't want to read through #2211 to get the background, the application of ternary logic there is to have a `Dialog` annotation member called `visible`. The problem with using `Boolean` is that as soon as an expression is given, it is no longer possible to say _default behavior_ (which is neither `true` nor `false`, but depends on tool-specific logic). With a third truth value, _unknown_, it becomes possible to give an expression for selecting between:
- `false` — force hiding
- `unknown` — follow tool-specific logic (same as not specifying `visible` at all)
- `true` — force showing

# Rationale
The principles on which this MCP is base were listed on the [entry page](ReadMe.md), and are repeated here for convenience:
- A new built-in type, `Ternary`.
- A new top-level constant `unknown` for the third truth value.
- Explicit as well as implicit conversion from `Boolean`.
- No implicit conversion to `Boolean`.
- No new built-in functions.

The rationale for each of these is given below.

## New built-in type
Although one could imagine ternary logic only being used in annoations to just have a pseudo code definition, and not be present in the language, it would seem like a half-baked solution. Besides, the use of `Ternary` for expressing explicit conversion from `Boolean` solves the problem of constructing the two known truth values nicely without introducing two more literal constants.

One could also have imagined just introducing `Ternary` as a new built-in enumeration, but expressing ternary logic in analogy with Boolean logic would then require defining the meaning of Boolean operators such as `and` to have meaning for this particular enumeration. That is probably not how we want enumerations to be used.

## New literal constant
The introduction of the top-level constant `unknown` to represent the third third truth value avoids the backwards incompatibility that would come with making `unknown` a keyword similar to `true` and `false`. Uses that makes it distinguishable from being a keyword are deprecated from start, to pave the way for future alignment with `true` and `false`.

Alternatives that were considered less attractive include:
- Making `unknown` a keyword similar to `true` and `false`.
- Defining `Ternary()` to construct `unknown`.
- Defining a `Ternary`-valued operation that can produce `unknown` as a function of only known values, for example `consensus(true, false)`.

The alternative approaches both suffer from the problem of not having a symmetric way of refering to _unknown_ in the same way as we refer to `false` and `true`. This lack of symmetry is both a problem for source code as well as in specification text and other places where writing about Modelica.

As shadowing the top-level `unknown` is allowed but deprecated, there can be situations where one would like to access the top-level constant while it is shadowed. It was considered to allow something like `Ternary()` to avoid the need to access the top level constant, but the current design instead relies on using the fully qualified `.unknown`. Use of the fully qualified form is deprecated from start, meaning that it should only be used when a model also contains the deprecated exostence of another identifier named `unknown`.

## Conversion from Boolean

### Explicit conversion
With the explicit conversion from `Boolean`, the other two `Ternary` values besides `unknown` can be expressed as `Ternary(false)` and `Ternary(true)`. This is preferred over a solution with three new literal constants (such as {`False`, `Unknown`, `True`}), where it would be a constant source of confusion that there would be two different literals for _false_ and two different literals for _true_.

### Implicit conversion
The implicit conversion from `Boolean` to `Ternary` would be very similar to the implicit conversion from `Integer` to `Real` — a well established and everywhere used feature of Modelica. To start with, it would mean that one would never need to write `Ternary(false)` or `Ternary(true)`; just writing `false` in a context where a `Ternary` is expected would be equivalent to writing out `Ternary(false)`, just like writing `1` is equivalent to writing `1.0` in a context where a `Real` is expected. Then, it would also make mixing `Ternary` and `Boolean` logic seamless, so the user doesn't have to explicitly choose between either `Boolean` or `Ternary` logic for all the subexpressions that don't make use of the third truth value. For example:
```
parameter Ternary t;
Real x;
Ternary y = t or (time > 1 and x < 5); /* No need to explicitly convert conjunction to Ternary. */
```

## Conversion to Boolean

### Ternary operations resulting in Boolean
Instead of introducing conversion constructs, there are some operations on `Ternary` that can be used to express exactly what should map to `false` and what should map to `true`. For example, `unknown == true` is defined as `false`, not `unknown`.

### No implicit conversion
To not have implicit conversion from `Ternary` to `Boolean` (such as defining the meaning of `Boolean(t)` where `t` is a `Ternary`) means that the user will always have to make an explicit choice of how to interpret a `Ternary` where a `Boolean` is required, for example in `if` conditions. There will be no surprises of _unknown_ being treated as either `true` or `false` instead of being reported as an error.

## No built-in functions
This MCP does not include any new built-in functions, although there are several that would be natural to have. For an example of a useful function the `consensus(t1, t2, …, tn)` would mean the common ternary value if all arguments are equal, otherwise `unknown`.

The reason for not introducing such functions is to minimize backwards incompatibility. If the lack of such functions turns out to be too much of a limitation, they can be introduced with a future MCP.

## External function value mapping
In the external language interface, the `Ternary` values are mapped to {1, 2, 3}.
This makes it similar to enumerations, and by not mapping any `Ternary` value to 0 there is a better chance of detecting the mistake of directly using the `Ternary` as a truth value in the external code.

# The option type alternative
Before deciding to go with the design where `Ternary` is introduced, we must also mention the following important alternative approach. Reasons will be given why this isn't the design proposed by this MCP.

## Using `none` to represent _unknown_
A completely different way of introducing ternary logic would be to introduce a `Boolean` _option_ type. Instead of writing
```
Ternary t = unknown;
```
one would then write something like one of the alternatives
```
Boolean? t = Boolean?(); /* Explicit construction of the 'none' of a particular option type. */
```
or
```
Boolean? t = none; /* Using type inference to infer the particular option type. */
```
where the `?` plays a similar role as an array dimension; it constructs a new type based on the type to the left. In this case, an option type, meaning that the value of `t` is either a `Boolean` value, or a value representing the absence of a `Boolean` value.

While the type inference alternative with `none` looks more elegant on first glance, it doesn't really fit well with current Modelica, and there is also a problem with type inference and implicit conversion that speaks to the advantage of explicitly giving the type of a _none_-value. For the rest of this section, only the `Boolean?()` alternative for constructing the _none_-value is considered. Explicit construction of a non-_none_ value of `Boolean?` would take the form `Boolean?(e)` where `e` is an expression of type `Boolean`. There is thus an obvious way to define implicit conversion from any type `T` to `T?` simply by the mapping `t` → `T?(t)`, thus making the use of `Boolean?` almost as convenient as the use of `Ternary` (the difference being the keyword `unknown` vs the more cumbersome `Boolean?()`). (To see why implicit conversion doesn't work nicely with type inference, note that the implicit conversion from `none` to `Boolean??` is ambiguous; should it be `Boolean??()` or `Boolean??(Boolean?())`.)

Even though the use of option types could be restricted to `Boolean` to start with, it would open up for supporting more types in the future. Given the use case of allowing an expression of a built-in attribute to refer to the default value, it seems as if this could be useful for other types as well. For example, pretend that the `group` of `Dialog` didn't have `"Parameters"` as fixed start value, allowing tools to organize dialogs as they see fit. Then one could imagine giving an expression for `group` that only shall determine the group under certain circumstances, and otherwise leave it to the tool to decide. Then the use of a `String` option would be an elegant way of making this possible.

Another advantage of introducing option types instead of `Ternary` would be that it would probably be acceptable to say that no option type can be used for indexing into arrays. This would simplify parts of the implementation compared to `Ternary`.

However, these are some reasons for sticking with `Ternary` instead of `Boolean?`:
- If the usual ternary operators `not`, `and` and `or` in accordance with Kleene is what we want — which is the assumption of this MCP — the use of `Boolean?` would imply an unnatural interpretation of `none`. [This argument is elaborated below.](#Using-none-to-represent-undefined)
- Introducing the literal `none` to refer to the absence of a value for an option type leads to a significant change of the Modelica type system, as `none` can have any option type. Even if type inference would be implemented, there would be problems when it comes to implicit conversions (see above).
- Explicitly writting out the type of a _none_ as in `Boolean?()` would be inconvenient compared to just saying `unknown`. (However, it would then be natural to define implicit conversion to any option type.)
- Attributes of other type than `Boolean` typically have a default behavior that can be expressed with a default value (like the empty string in case of `String`), removing the need for an option type to express the absence of a value.
- The `Boolean?` type might end up being the only special case of an option type with meanings given to the built-in logical operators. (Possibilities to define meanings for other option types exist. For example, one could imagine the `Real?()` and `Integer?()` both being at the same time additive zeros and multiplicative ones. The question is whether there would ever be a need to introduce such definitions in the language.)
- Relational operators would need to be defined generically, most likely giving the order `Boolean()?` < `Boolean(false)?` < `Boolean(true)?`, which doesn't correspond to the understanding of `Boolean?()` as some kind of middle ground between false and true.
- Defining external language interface for option types is a pretty big change compared to just introducing one new scalar type (it would need to be defined generically, not just with `Boolean?` in mind).
- It is probably a bad idea to just introduce option types in Modelica without considering the more general concepts that would give option types as a special case. Such more general constructs inspired by MetaModelica have been discussed at many design meetings without getting much traction.

## Using `none` to represent _undefined_
While possible to interpret `Boolean?()` (hereafter referred to as `none` for brevity) as _unknown_ and define logical operation on `Boolean?` in the same way as for `Ternary` to get consistency with Kleene logic, the generalization of this interpretation to other option types such as `Real?` or `String?` isn't as useful. The natural interpretation of `none` would rather be _undefined_, which leads to more useful generalizations to other option types.

For example, one could define that concatenation with an _undefined_ `String?`, would be a no-op, and the same for addition or multiplication with an _undefined_ `Real?`. For `Boolean?` it would then be natural to define both disjunction and conjunction with _undefined_ to be no-ops, leading to things such as `none and true = some(true)`.

Combined with `Ternary` to get `Ternary?`, one could define four-valued logic, having its own applications beyond both `Ternary` and `Boolean?`. In detail, by identifying `true` both with `Ternary(true)` and `Boolean?(true)`, and similarly for `false`, the truth tables for `Ternary` could be reordered and extended so that they define consistent tables for all of `Boolean`, `Ternary`, `Boolean?` and `Ternary?`. For instance, consider conjunction and let `none` refer to `Boolean?()` for brevity:

| `x and y` | `y = none` | `y = false` | `y = true` | `y = unknown` |
| ------------- | ------------- | ----------- | ------------- | ------------- |
| `x = none` | `none` | `false` | `true` | `unknown` |
| `x = false` | `false` | `false` | `false` | `false` |
| `x = true` | `true` | `false` | `true` | `unknown` |
| `x = unknown` | `unknown` | `false` | `unknown` | `unknown` |

Please note that this logical table does not correspond to the often cited four-valued logic by Belnap, hinting at the potential problems of reaching agreement on how to define logic on `Boolean?` and `Ternary?` in Modelica.

For the numeric types `Real` and `Integer`, a better analog to `unknown` would be `NaN` (not-a-number), having very different arithmetic behavior compared to the no-ops of _undefined_. For example, this gives the expected behavior of adding _unknown_ to any number resulting in _unknown_.

To summarize, while option types have many interesting applications — including modeling of built-in attributes with non-trivial defaults — it would be a mistake to use `Boolean?` for the usual ternary connectives defined in accordance with Kleene. This also shows that `Ternary` is not going to become redundant if option types are added to Modelica in the future.
Loading