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