lt-core/motivation-angle.md
2024-06-11 15:17:58 +02:00

9 KiB
Raw Permalink Blame History

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.

    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.

    #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.

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.

    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).

    template < typename Angle, typename UnitInterval >
    struct HSV {
    Angle hue;
    UnitInterval saturation;
    UnitInterval value;
    };
    HSV<Turns<float>, float> hsv;

In Rust sieht es ähnlich aus (gekürzt aus dem angular-units crate):

    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 AngleRadians.. 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.