314 lines
9 KiB
Markdown
314 lines
9 KiB
Markdown
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.
|