Was kann man tun, um die Lesbarkeit von mathematisch orientiertem Code in C #, Java und ähnlichem zu verbessern? [geschlossen]

16

Als C-Programmierer und C # -Programmierer mag ich an C # nicht, wie ausführlich mathematische Funktionen sind. Jedes Mal, wenn Sie beispielsweise eine Sinus-, Cosinus- oder Potenzfunktion verwenden müssten, müssten Sie der statischen Math-Klasse voranstellen. Dies führt zu sehr langem Code, wenn die Gleichung selbst ziemlich einfach ist. Das Problem wird noch schlimmer, wenn Sie Datentypen typisieren müssen. Infolgedessen leidet meiner Meinung nach die Lesbarkeit. Beispielsweise:

double x =  -Math.Cos(X) * Math.Sin(Z) + Math.Sin(X) * Math.Sin(Y) * Math.Cos(Z);

Im Gegensatz zu einfach

double x = -cos(X) * sin(Z) + sin(X) * sin(Y) * cos(Z);

Dies ist auch in anderen Sprachen wie Java der Fall.

Ich bin nicht sicher, ob diese Frage tatsächlich eine Lösung hat, aber ich würde gerne wissen, ob es Tricks gibt, mit denen C # oder Java-Programmierer die Lesbarkeit von Math-Code verbessern können. Mir ist jedoch klar, dass C # / Java / etc. Da es sich nicht um mathematisch orientierte Sprachen wie MATLAB oder ähnliches handelt, ist dies sinnvoll. Aber gelegentlich müsste man immer noch mathematischen Code schreiben, und es wäre großartig, wenn man ihn lesbarer machen könnte.

9a3eedi
quelle
Ich kenne keine spezifischen, aber Sie könnten wahrscheinlich eine Algebra-Bibliothek finden, mit der Sie mathematische Funktionen mit Zeichenfolgen definieren können, obwohl es einige Leistungseinbußen geben würde.
Raptortech97
7
Sie sorgen sich um etwas mehr Ausführlichkeit, verstecken aber gerne ein '+' unter '*' mit unären Operatoren - alles ohne geschweifte Klammern - ich vermute, Sie haben falsche Prioritäten.
Mattnz
1
Es war nur ein Beispiel, aber ein guter Punkt
9a3eedi
6
In C # 6.0, werden Sie schreiben können: using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
Svick

Antworten:

14

Sie können lokale Funktionen definieren, die die globalen statischen Funktionen aufrufen. Hoffentlich integriert der Compiler die Wrapper und der JIT-Compiler erstellt dann einen engen Assembler-Code für die tatsächlichen Vorgänge. Beispielsweise:

class MathHeavy
{
    private double sin(double x) { return Math.sin(x); }
    private double cos(double x) { return Math.cos(x); }

    public double foo(double x, double y)
    {
        return sin(x) * cos(y) - cos(x) * sin(y);
    }
}

Sie können auch Funktionen erstellen, die allgemeine mathematische Operationen in einzelne Operationen bündeln. Dies würde die Anzahl der Instanzen verringern , in denen Funktionen wie sinund cosin Ihrem Code erscheinen, wodurch die clunkiness der globalen statischen Funktionen weniger auffällig aufrufen. Beispielsweise:

public Point2D rotate2D(double angle, Point2D p)
{
    double x = p.x * Math.cos(angle) - p.y * Math.sin(angle);
    double y = p.x * Math.sin(angle) + p.y * Math.cos(angle);

    return new Point2D(x, y)
}

Sie arbeiten auf der Ebene von Punkten und Rotationen, und die zugrunde liegenden Triggerfunktionen sind vergraben.

Randall Cook
quelle
... warum habe ich nicht daran gedacht :)
9a3eedi
Ich habe dies als die richtige Antwort markiert, da es sich um eine plattformübergreifende Lösung handelt, die einfach genug ist. Auch die anderen Lösungen stimmen. Ich kann wirklich nicht glauben, dass ich nicht daran gedacht habe :) es ist einfach zu offensichtlich
9a3eedi
31

In Java stehen viele Tools zur Verfügung, um bestimmte Dinge weniger ausführlich zu gestalten. Sie müssen sich lediglich ihrer bewusst sein. In diesem Fall ist der staticImport nützlich ( Tutorial-Seite , Wikipedia ).

In diesem Fall,

import static java.lang.Math.*;

class Demo {
    public static void main (String[] args) {
        double X = 42.0;
        double Y = 4.0;
        double Z = PI;

        double x =  -cos(X) * sin(Z) + sin(X) * sin(Y) * cos(Z);
        System.out.println(x);
    }
}

läuft ganz gut ( ideone ). Es ist etwas umständlich, einen statischen Import der gesamten Math-Klasse durchzuführen, aber wenn Sie viel Mathe ausführen, kann dies erforderlich sein.

Mit dem statischen Import können Sie ein statisches Feld oder eine statische Methode in den Namespace dieser Klasse importieren und aufrufen, ohne den Paketnamen zu benötigen. Sie finden dies häufig in Junit-Testfällen, in denen import static org.junit.Assert.*;alle Asserts verfügbar sind.


quelle
Hervorragende Antwort. Mir war diese Funktion nicht bekannt. Unter welcher Java-Version ist das möglich?
9a3eedi
@ 9a3eedi Es wurde erstmals in Java 1.5 zur Verfügung gestellt.
Schöne Technik. Ich mag das. +1.
Randall Cook
1
@RandallCook In Java 1.4 taten die Benutzer in allen Klassen Dinge wie public interface Constants { final static public double PI = 3.14; }und dann public class Foo implements Constants, um Zugriff auf die Konstanten in der Benutzeroberfläche zu erhalten. Dies machte ein großes Durcheinander. Daher wurde mit 1.5 der statische Import hinzugefügt, um das Abrufen bestimmter Konstanten und statischer Funktionen zu ermöglichen, ohne eine Schnittstelle implementieren zu müssen.
3
Sie können bestimmte Funktionen selektiv importierenimport static java.lang.Math.cos;
Ratschenfreak
5

Mit C # 6.0 können Sie die Funktion des statischen Imports verwenden.

Ihr Code könnte sein:

using static System.Math;
using static System.Console;
namespace SomeTestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            double X = 123;
            double Y = 5;
            double Z = 10;
            double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);
            WriteLine(x); //Without System, since it is imported 
        }
    }
}

Siehe: Statische Using-Anweisungen (AC # 6.0-Sprachvorschau)

Eine weitere C # 6.0-Funktion für "syntaktischen Zucker" ist die Einführung der statischen Aufladung. Mit dieser Funktion ist es möglich, einen expliziten Verweis auf den Typ beim Aufrufen einer statischen Methode zu entfernen. Darüber hinaus können Sie mit static nur die Erweiterungsmethoden einer bestimmten Klasse anstelle aller Erweiterungsmethoden in einem Namespace einführen.

BEARBEITEN: Seit der Veröffentlichung von Visual Studio 2015, CTP im Januar 2015, ist für den statischen Import ein explizites Schlüsselwort erforderlich static. mögen:

using static System.Console;
Habib
quelle
4

Zusätzlich zu den anderen guten Antworten hier kann ich auch eine DSL für Situationen mit erheblicher mathematischer Komplexität empfehlen (keine durchschnittlichen Anwendungsfälle, aber möglicherweise einige finanzielle oder akademische Projekte).

Mit einem DSL-Generierungstool wie Xtext können Sie Ihre eigene vereinfachte mathematische Grammatik definieren, die wiederum eine Klasse mit der Java-Darstellung (oder einer anderen Sprache) Ihrer Formeln generiert.

DSL-Ausdruck:

domain GameMath {
    formula CalcLinearDistance(double): sqrt((x2 - x1)^2 + (y2 - y1)^2)
}

Erzeugte Ausgabe:

public class GameMath {
    public static double CalcLinearDistance(int x1, int x2, int y1, int y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    }
}

In einem solch einfachen Beispiel lohnen sich die Vorteile der Erstellung der Grammatik und des Eclipse-Plugins nicht. Bei komplizierteren Projekten kann dies jedoch große Vorteile bringen, insbesondere wenn die DSL es Geschäftsleuten oder akademischen Forschern ermöglicht, formelle Dokumente in einem komfortablen Format zu verwalten und seien Sie versichert, dass ihre Arbeit korrekt in die Implementierungssprache des Projekts übersetzt wurde.

Dan1701
quelle
8
Ja, im Allgemeinen und per Definition kann eine DSL nützlich sein, wenn Sie in einer bestimmten Domäne arbeiten. Wenn dieses DSL jedoch nicht vorhanden ist oder nicht den Anforderungen entspricht, müssen Sie es warten , was problematisch sein kann. Auch für die spezielle Frage ("Wie kann ich die sin, cos, ... Methoden / Funktionen verwenden, ohne jedes Mal die Math-Klasse zu schreiben") ist eine DSL möglicherweise eine übergroße Lösung.
Mgoeminne
4

In C # können Sie Erweiterungsmethoden verwenden.

Das Folgende liest sich ziemlich gut, wenn man sich an die "Postfix" -Notation gewöhnt hat:

public static class DoubleMathExtensions
{
    public static double Cos(this double n)
    {
        return Math.Cos(n);
    }

    public static double Sin(this double n)
    {
        return Math.Sin(n);
    }

    ...
}

var x =  -X.Cos() * Z.Sin() + X.Sin() * Y.Sin() * Z.Cos();

Leider macht die Rangfolge der Operatoren den Umgang mit negativen Zahlen hier etwas hässlicher. Wenn Sie Math.Cos(-X)stattdessen rechnen möchten, müssen -Math.Cos(X)Sie die Zahl in Klammern setzen:

var x = (-X).Cos() ...
rjnilsson
quelle
1
Übrigens wäre dies ein guter Anwendungsfall für Erweiterungseigenschaften und sogar ein legitimer Anwendungsfall für den Missbrauch von Eigenschaften als Methoden!
Jörg W Mittag
Das habe ich mir gedacht. x.Sin()Ich würde einige Anpassungen vornehmen, aber ich missbrauche Erweiterungsmethoden und dies wäre persönlich meine erste Neigung.
WernerCD
2

C #: Eine Variation von Randall Cooks Antwort , die mir gefällt, weil sie das mathematische "Aussehen" des Codes mehr beibehält als Erweiterungsmethoden, ist die Verwendung eines Wrappers, aber die Verwendung von Funktionsreferenzen für die Aufrufe, anstatt sie zu umbrechen. Ich persönlich denke, dass der Code dadurch sauberer aussieht, aber im Grunde ist es dasselbe.

Ich habe ein kleines LINQPad-Testprogramm gestartet, das Randalls umschlossene Funktionen, meine Funktionsreferenzen und die direkten Aufrufe enthält.

Die funktionsbezogenen Anrufe dauern grundsätzlich genauso lange wie die direkten Anrufe. Die umschlossenen Funktionen sind durchweg langsamer - wenn auch nicht um ein Vielfaches.

Hier ist der Code:

void Main()
{
    MyMathyClass mmc = new MyMathyClass();

    System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

    for(int i = 0; i < 50000000; i++)
        mmc.DoStuff(1, 2, 3);

    "Function reference:".Dump();
    sw.Elapsed.Dump();      
    sw.Restart();

    for(int i = 0; i < 50000000; i++)
        mmc.DoStuffWrapped(1, 2, 3);

    "Wrapped function:".Dump();
    sw.Elapsed.Dump();      
    sw.Restart();

    "Direct call:".Dump();
    for(int i = 0; i < 50000000; i++)
        mmc.DoStuffControl(1, 2, 3);

    sw.Elapsed.Dump();
}

public class MyMathyClass
{
    // References
    public Func<double, double> sin;
    public Func<double, double> cos;
    public Func<double, double> tan;
    // ...

    public MyMathyClass()
    {
        sin = System.Math.Sin;
        cos = System.Math.Cos;
        tan = System.Math.Tan;
        // ...
    }

    // Wrapped functions
    public double wsin(double x) { return Math.Sin(x); }
    public double wcos(double x) { return Math.Cos(x); }
    public double wtan(double x) { return Math.Tan(x); }

    // Calculation functions
    public double DoStuff(double x, double y, double z)
    {
        return sin(x) + cos(y) + tan(z);
    }

    public double DoStuffWrapped(double x, double y, double z)
    {
        return wsin(x) + wcos(y) + wtan(z);
    }

    public double DoStuffControl(double x, double y, double z)
    {
        return Math.Sin(x) + Math.Cos(y) + Math.Tan(z);
    }
}

Ergebnisse:

Function reference:
00:00:06.5952113

Wrapped function:
00:00:07.2570828

Direct call:
00:00:06.6396096
Whelkaholism
quelle
1

Benutze Scala! Sie können symbolische Operatoren definieren und benötigen keine Parens für Ihre Methoden. Dies macht Mathe Weise leichter zu interpretieren.

Die gleiche Berechnung in Scala und Java könnte beispielsweise so aussehen:

// Scala
def angle(u: Vec, v: Vec) = (u*v) / sqrt((u*u)*(v*v))

// Java
public double angle(u: Vec, v: Vec) {
  return u.dot(v) / sqrt(u.dot(u)*v.dot(v));
}

Das summiert sich ziemlich schnell.

Rex Kerr
quelle
2
Scala ist in der CLR nicht verfügbar, nur in der JVM. Somit ist es keine wirklich praktikable Alternative zu C #.
Ben Rudgers
@benrudgers - C # läuft nicht auf der JVM, ist also keine echte Alternative zu Java, worüber sich auch die Frage stellte. Die Frage gibt nicht an, dass es CLR sein muss!
Rex Kerr
Vielleicht bin ich eine Luddite, aber zwei zusätzliche Zeichen für "Punkt" anstelle von "*", mit dem Vorteil, dass der Code klarer ist, scheint ein geringer Preis zu sein. Trotzdem eine gute Antwort.
user949300