Maßeinheiten und komplexe Zahlen

Dieser Artikel befasst mit dem F# Feature Units of measure und den Möglichkeiten das Feature auch für komplexe Zahlen zu verwenden. Die Strategien lassen sich aber genauso auch auf Vektoren übertragen.

Historie – F#

Im Rahmen dieses Artikels habe ich versucht rauszufinden, seit wann bzw. ab welcher Version F# die Unterstützung für Maßeinheiten anbietet. Am Ende habe ich diesen Blogeintrag von Andrew Kennedy gefunden. Seine Doktorarbeit stellte die theoretischen Grundlagen für die Implementierung F#. Aus dem Artikel von Ende August 2008 geht hervor, dass Units of measure bereits in der Community Technical Preview beinhaltet waren.

In der Zwischenzeit – seit F# 4.0 – kam die Unterstützung für Brüche in Einheiten dazu. Eine spannende Ergänzung, um generische Funktionen mit Einheitenunterstützung bereitzustellen. Aber das wird sich später noch zeigen.

Grundlagen

Es gibt genügend gute andere Seiten mit einführenden Erläuterungen, daher hier nur die minimale Fassung, damit der restliche Artikel leichter zu verstehen ist.

  • Einheiten sind Typen, die man nicht instanziieren kann.
  • Einheitentypen müssen das Attribut [<Measure>] haben.
  • Alle numerischen primitiven Datentypen sind mit Einheiten versehbar.
  • Einheiten, da sie Typen sind, werden als generische Typargumente angegeben.
  • Einheiten sind nur zur Compile-Zeit verfügbar und nur werden nur dann überprüft.
  • Einheiten erzeugen keinen Laufzeit Overhead
  • Zahlen mit und ohne Einheit sind verschiedene Typen und sind nicht kompatibel (ohne weiteres)

An der Stelle sei auch der Namespace Microsoft.FSharp.Data.UnitSystems.SI erwähnt. Dort sind bereits viele der SI-Einheiten definiert. Der Vorteil, wenn man diese Einheiten verwendet ist, dass es die gleichen Einheiten-Typen sind, wie bei jedem anderen und damit untereinander kompatibel.

Komplexe Zahlen

Eine komplexe Zahl setzt sich zusammen aus dem reellen Anteil und dem imaginären Anteil und wird in der Mathematik so geschrieben: 3.5 + 2.5i. Das i ist dabei die imaginäre Einheit.

Es gibt nun mindestens zwei naheliegende Varianten, um in einem Programm mit komplexen Zahlen umzugehen. Die erste ist die beiden Faktoren als zwei eigene float Variablen zu führen. Die imaginäre Einheit wäre dabei implizit zugehörig zur zweiten Variable. Die andere Variante ist einen benutzerdefinierten Typen anzulegen, der diese beiden float Variablen kapselt.

Beide Varianten haben ihre Vor- und Nachteile, die sich grob so zusammenfassen lassen. Der eigene Datentyp führt sicherlich dazu, dass man umfangreichere Programme korrekter entwickelt. Dafür haben eigene Datentypen einen Laufzeit-Overhead, zumindest in der CLR. Für die andere Variante gilt dementsprechend das Gegenteil.

Für die Problemstellung in meinem aktuellen Projekt ist die Korrektheit momentan im Vordergrund, vor allem zwischen verschiedenen Modulen. Daher wird dort ein Datentyp Complex verwendet.

Es wäre nun doch schön, wenn es möglich wäre auch eine Einheitenbetrachtung durchzuführen, die die verwendeten Funktionen mit komplexen Parametern miteinschließt. Das wollen wir nun im Folgenden betrachten. Damit die Beispiele für jeden nachprogrammierbar sind, verwende ich den Typ System.Numerics.Complex.

Erster Lösungsansatz

Neben dem [<Measure>] Attribut definiert F# auch das [<MeasureAnnotedTypeAbbreviation>]. Mit diesem Attribut erweitert F# selbst seine Datentypen um die Fähigkeit Einheiten anzunehmen.

Der Vorteil daran ist, dass dieser neue Typ den gleichen Namen haben kann, wie der bereits existierende Typ ohne Einheit. Wenn man das Attribut [<MeasureAnnotatedAbbreviation>] nicht verwendet, beschwert sich der Compiler, dass der Typ bereits definiert sei.

Wie können wir unseren „neuen“ Datentyp nun verwenden, um komplexe Werte mit Einheiten zu definieren.

Alle drei Möglichkeiten werden vom Compiler nicht akzeptiert. Beim ersten Versuch sind die Typen inkompatibel. Wie vorher beschrieben, handelt es sich um unterschiedliche Typen und es gibt keine automatische Umwandlung. Beim zweiten Versuch versteht der Parser den Code bereits nicht. Das ist keine gültige F# Syntax. Beim letzten Versuch ist zwar wieder die Syntax korrekt, aber der annotierte Datentyp kann nicht instanziiert werden, bzw. System.Numerics.Complex hat kein generisches Argument.

Stellt sich doch die Frage, warum es dann mit float u.a. funktioniert. Der Trick dahinter ist, dass es F#-Basis-Bibliothek eine Funktion gibt namens retype. Diese Funktion hat keine Implementierung, sondern dient nur dazu für den Compiler die Typen anzupassen. Leider ist diese Funktion aus genau diesem Grund außerhalb der Basis-Bibliothek nicht benutzbar. Stattdessen gibt es spezialisierte Funktionen für die verschiedenen numerischen Typen, z.B. für float.

Momentan ist es zwar noch möglich die Funktion samt „Implementierung“ im eigenen Code neu zu definieren, aber der Compiler markiert die Funktion direkt mit Warnungen. Auch wenn wir damit momentan weiter kommen würden, erscheint mir das kein guter Weg. Hier müsste die Unterstützung für benutzerdefinierte Datentypen besser werden.

Zweiter Lösungsansatz

Die nächstbeste Möglichkeit ist den existierenden Complex-Typen nochmal zu wrappen. Das bringt einige Nachteile mit sich, die der andere Ansatz nicht gehabt hätte, wie den zusätzlichen Runtime Overhead und das Complex nicht der gleiche Typ ist wie Complex<1>, bzw. dass man diese Beziehung auch nicht herstellen kann.

Das Wrappen selber ist ohne weiteres möglich:

Noch ein paar abschließende Worte zum Wrapper:

  • Die Properties Real und Imaginary verwenden die vorhin angesprochene Funktion FloatWithMeasure, um die Einheit wiederherzustellen.
  • Zero ist in unserem Wrapper ein static member und kein static field und damit verwendbar in z.B. Array.sum, was mit System.Numerics.Complex nicht funktioniert.
  • Exp habe ich eingeschränkt auf komplexe Zahlen ohne Einheit.
  • Sqrt verwendet das F#4.0 Feature mit den Brüchen in Einheiten.
  • System.Numerics.Complex hat noch viel mehr Funktionen, die ich erst mal nicht um Einheiten erweitert habe.

Mini-Fazit

Die Units of measure sind ein tolles Feature und ich habe keine Probleme bei der Verwendung gehabt solange ich bei float geblieben bin. Benutzerdefinierte Typen genießen allerdings momentan ein Dasein zweiter Klasse scheint es mir, sowohl hinsichtlich der Performance als auch der Unterstützung durch die Basis-Bibliothek. Ich bin aber zuversichtlich, dass sich hier noch etwas tun wird. Die Brüche in den Einheiten waren ja ursprünglich auch nicht möglich.

Danke fürs Lesen.

Anhang: