lt-core/motivation-angle.md

315 lines
9 KiB
Markdown
Raw Normal View History

2024-06-11 15:17:58 +02:00
Mit der Motivation, auf generische Art Farbräume und deren
Repräsentationen zu definieren, nehmen wir als erstes Beispiel das HSV
Modell. Der Hue-wert wird über einen Winkel dargestellt. Die
einfachste Variante hier wäre, diesen Winkel als eine Variable vom Typ
'float' zu implementieren.
```c++
float angle = 28.0; // this is an angle represented in degrees
```
Da Winkel aber gerne in unterschiedlichsten Einheiten ausgedrückt
werden wie Grad, Umdrehungen, Bogenmaß, etc.. muss der Programmierer
achtgeben, hier nicht diese Einheiten zu vermischen und gegebenenfalls
je nach Kontext mit Bibliotheksfunktionen / Nutzereingaben /
Dateiformaten eine korrekte Umrechnung definieren.
```c++
#include <cmath>
// remember to perform conversion from degrees to radians!!!
std::sin( 6.283 * angle / 360.0 );
```
Um dieses Problem zu beheben, könnte man in Sprachen mit 'flachem'
Typsystem wie C/C++ einen *Wrapper-Typ* erstellen. Dies ist ein gängiges
Pattern um diesen und andere ähnliche Sachverhalte zu modellieren, was
allerdings oft viel Boilerplatecode bedeutet, welcher daher in vielen
solcher Libraries durch Macros generiert wird.
```c++
struct Degrees {
float value;
/* need to overload all arithmetic operators for the new
* Degree type.. much boilerplate
*/
friend Degrees operator+ ( Degrees a, Degrees b ) {
return Degrees{ a.value + b.value };
}
/* define type conversions */
operator Turns() {
return Turns{ value / 360.0 };
}
operator Radians() {
Turns turns = *this;
Radians rad = turns;
return rad;
}
};
struct Turns {
float value;
/* overload all arithmetic operators, much boilerplate
...
*/
/* type conversions */
operator Radians() {
return Radians{ 6.283 * value };
}
operator Degrees() {
return std::sin()
}
};
struct Radians {
float value;
operator Turns() {
return Turns{ value / 6.283 };
}
/* overload all operators, much boilerplate
...
*/
}
/* overload `sin` function
* to accept wrapped angle types
*/
float sin( Radians angle_rad ) {
return std::sin( angle_rad.value );
}
float sin( Turns angle_turns ) {
Radians angle_rad = angle_turns;
return sin( angle_rad );
}
float sin( Degrees angle_deg ) {
Radians angle_rad = angle_deg;
return sin( angle_rad );
}
```
Soweit kann nun ausgeschlossen werden, dass Einheiten verwechselt
werden. Diese Winkel, welche zunächst reele Zahlen sind, können aber
auch wieder auf unterschiedliche weise repräsentiert werden: Als
Fließkommazahl oder als lineare Abbildung in einen Ganzzahlraum
(z.B. normierte reele Zahlen dargestellt als 8-bit Integer mit dem
Mapping 0.0 => 0 und 0.99 => 255), jeweils mit verschiedenen
Auflösungen. Dafür würde man nun einen Typ-parameter einführen, welcher
die zugrundeliegende Zahlenrepräsentation bestimmt. Durch concepts bzw
traits können die benötigten arithmetischen Operationen auf dem
Zahlentyp statisch abgesichert werden.
```c++
template< typename T >
struct Degrees {
T value;
//...
};
template< typename T >
struct Turns {
T value;
//...
};
template< typename T >
struct Radians {
T value;
//...
};
```
Dieser weitere Parameter multipliziert allerdings die möglichen
Transformationen zwischen den vielen verschiedenen möglichen
Angle-Typen. Außerdem pflanzt sich dieser Typparameter weiter in
unsere HSV Abstraktion fort, obwohl er dort garnicht von Bedeutung
ist, da hier eigentlich die Semantik des HSV Modells nur mit dem
abstrakten Interface des 'Angle' traits definiert werden kann.
(Der Typparameter ließe sich theoretisch durch virtual dispatch
vermeiden, tradeoff gegen runtime overhead).
```c++
template < typename Angle, typename UnitInterval >
struct HSV {
Angle hue;
UnitInterval saturation;
UnitInterval value;
};
```
```c++
HSV<Turns<float>, float> hsv;
```
In Rust sieht es ähnlich aus (gekürzt aus dem `angular-units` crate):
```rust
pub struct Degrees<T>(pub T);
pub struct Radians<T>(pub T);
pub struct Turns<T>(pub T);
pub struct ArcMinutes<T>(pub T);
pub struct ArcSeconds<T>(pub T);
pub trait Angle: Clone + FromAngle<Self> + PartialEq + PartialOrd + num::Zero
{
/// Internal type storing the angle value.
type Scalar: Float;
fn new(value: Self::Scalar) -> Self;
/// The length of a full rotation.
fn period() -> Self::Scalar;
fn scalar(&self) -> Self::Scalar;
fn set_scalar(&mut self, value: Self::Scalar);
fn normalize(self) -> Self;
fn sin(self) -> Self::Scalar;
}
/// Construct `Self` from an angle.
///
/// Analogous to the traits in the standard library,
/// FromAngle and IntoAngle provide a way to convert between angle
/// types and to mix various angle types in a single operation.
pub trait FromAngle<T>
where T: Angle
{
/// Construct `Self` by converting a `T`.
fn from_angle(from: T) -> Self;
}
/* and implement all required boilerplate via macros
*/
macro_rules! impl_angle {
($Struct: ident, $period: expr) => {
// ...
impl<U, T> Add<U> for $Struct<T>
where T: Float + Add<T, Output=T>,
U: IntoAngle<$Struct<T>, OutputScalar=T>
{
type Output=$Struct<T>;
fn add(self, rhs: U) -> $Struct<T> {
$Struct(self.0 + rhs.into_angle().0)
}
}
// ...
// repeat for all operations
}
}
impl_angle!(Degrees, 360.0);
impl_angle!(Radians, 2.0*consts::PI);
impl_angle!(Turns, 1.0);
```
Ladder-Types kehren diesen inneren Repräsentationstyp, welcher in der
C++ und Rust Variante durch generische Typparameter wieder nach außen
getragen werden muss, auf einer der metastrukturellen Ebene nach außen.
Da die 'Represented-As' Relation schon nativ durch das Typsystem erfasst
werden kann, müssen weder weitere Typvariablen eingeführt werden noch
müssen komplexe macros zur generierung von Boilerplate eingesetzt
werden, während die typisierung immernoch statisch bleibt.
Mit Ladder-Types ist es möglich, Funktionen auf dem Abstrakten Typ
'Angle' zu definieren, während man auf einer konkreten
Repräsentationsform, gegeben durch den Ladder-Type arbeitet (in diesem
Fall Float64). Gleichzeitig werden Funktionen zwischen gleichwertigen
Repräsentationen eines abstrakten Typs als Isomorphismus gekennzeichnet
und dürfen daher vom Compiler als implizite Konvertierung eingesetzt
werden, um Ausdrücke in denen mehrere Repräsentationen des gleichen
abstrakten Typs gemischt werden, ohne "typcast-noise" zu ermöglichen.
Eine Funktion wie 'sin', welche für alle Winkeltypen gelten soll, aber
nur für eine konkrete Repräsentation wie Angle~Radians~.. definiert ist,
kann durch das einsetzen implizieter Konvertierungen durch alle anderen
Repräsentationen auch genutzt werden. Ein Polymorphismus über
verschiedene Repräsentationsformen ist nun ohne generische Typparameter
und Macros möglich.
```
#[isomorphism]
let angle-degree-to-turns =
λ a : Angle
~ Degrees
~ _[0,360)
~ machine.Float64
-> Angle
~ Turns
~ _[0,1)
~ machine.Float64
↦ {
float64div a 360.0
};
#[isomorphism]
let angle-turns-to-radians =
λ a : Angle
~ Turns
~ _[0,1)
~ machine.Float64
-> Angle
~ Radians
~ _[0,pi2)
~ machine.Float64
↦ {
float64mul a 6.283
};
let sin =
λ a : Angle
~ Radians
~ _[0,2π)
~ machine.Float64
-> _[-1,1]
~ machine.Float64
↦ {
// original sin implementation in radians as float
};
// now it is possible to call `sin` which is
// implemented on radians with a value of degrees,
// because conversions can be inserted implicitly.
let some_angle : Angle ~ Degrees = 120;
let y : ~ machine.Float64 = sin some_angle;
// the previous line will be expanded to:
let y : ~ machine.Float64 = sin (angle-turns-to-radians (angle-degree-to-turns some_angle));
```
Auf die gleiche Weise könnten jetzt auch Konvertierungen zwischen Int
und Float repräsentationen definiert werden:
```
#[isomorphism]
let normalized-float-from-uint =
λ value : _[0,1) ~ _256 ~ machine.UInt8
-> _[0,1) ~ machine.Float64
↦ {
float64div (value as machine.Float64) 256.0
};
let half_turn : Angle ~ Turns ~ _[0,1) ~ _256 ~ machine.UInt8 = 128;
let y = sin half_turn;
// last line will be expanded to:
let y = sin (angle-turns-to-radians (normalized-float-from-uint half_turn));
```
Weiterführend könnten die Konvertierungsfunktionen auch mit
Komplexitätsannotationen versehen werden, welche bei der Suche eines
optimalen Konvertierungspfades als Gewichte herangezogen wereden können.