Cosa si può fare per migliorare la leggibilità del codice orientato alla matematica in C #, Java e simili? [chiuso]

16

Essendo sia un programmatore C che un programmatore C #, una delle cose che non mi piace di C # è il modo in cui le funzioni matematiche sono prolisse. Ogni volta che dovresti usare una funzione Sin, coseno o potenza, per esempio, dovresti anteporre la classe statica matematica. Questo porta a un codice molto lungo quando l'equazione stessa è piuttosto semplice. Il problema diventa ancora peggiore se è necessario digitare tipi di dati. Di conseguenza, a mio parere, la leggibilità soffre. Ad esempio:

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

Al contrario di semplicemente

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

Questo è anche il caso in altre lingue come Java.

Non sono sicuro che questa domanda abbia effettivamente una soluzione, ma vorrei sapere se ci sono trucchi C # o programmatori Java usati per migliorare la leggibilità del codice Math. Mi rendo conto però che C # / Java / etc. non sono linguaggi orientati alla matematica come MATLAB o simili, quindi ha senso. Ma occasionalmente si dovrebbe ancora scrivere codice matematico e sarà bello se uno potrebbe renderlo più leggibile.

    
posta 9a3eedi 24.10.2014 - 03:32
fonte

7 risposte

14

È possibile definire le funzioni locali che chiamano le funzioni statiche globali. Speriamo che il compilatore inline i wrapper, e quindi il compilatore JIT produrrà un codice di assemblaggio stretto per le operazioni effettive. Ad esempio:

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);
    }
}

Potresti anche creare funzioni che raggruppano operazioni matematiche comuni in singole operazioni. Ciò ridurrebbe al minimo il numero di istanze in cui funzioni come sin e cos compaiono nel codice, rendendo meno evidente il clunkiness di invocare le funzioni statiche globali. Ad esempio:

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

Stai lavorando a livello di punti e rotazioni e le funzioni trigonometriche sottostanti sono sepolte.

    
risposta data 24.10.2014 - 03:35
fonte
31

In Java, ci sono molti strumenti disponibili per rendere alcune cose meno dettagliate, devi solo esserne consapevole. Uno che è utile in questo caso è quello della static import ( pagina tutorial , wikipedia ).

In questo caso,

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);
    }
}

funziona abbastanza bene ( ideone ). È un po 'pesante per fare un'importazione statica di tutti della classe Math, ma se stai facendo un sacco di matematica, allora potrebbe essere richiesto.

L'importazione statica consente di importare un campo o un metodo statico nello spazio dei nomi di questa classe e di richiamarlo senza richiedere il nome del pacchetto. Lo trovi spesso nei casi di test di Junit in cui import static org.junit.Assert.*; viene trovato per ottenere tutti i asserzioni disponibili .

    
risposta data 24.10.2014 - 03:44
fonte
5

Con C # 6.0 puoi utilizzare la funzionalità di Static Import.

Il tuo codice potrebbe essere:

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 
        }
    }
}

Vedi: Statico Uso delle dichiarazioni (Anteprima della lingua C # 6.0)

Another C# 6.0 “syntactic sugar” feature is the introduction of using static. With this feature, it’s possible to eliminate an explicit reference to the type when invoking a static method. Furthermore, using static lets you introduce only the extension methods on a specific class, rather than all extension methods within a namespace.

MODIFICA: dal momento che Visual Studio 2015, CTP è stato rilasciato nel gennaio 2015, l'importazione statica richiede una parola chiave esplicita static . come:

using static System.Console;
    
risposta data 24.10.2014 - 16:42
fonte
4

Oltre alle altre buone risposte qui, potrei anche raccomandare un DSL per le situazioni con notevole complessità matematica (casi di utilizzo non medi, ma forse alcuni progetti finanziari o accademici).

Con uno strumento di generazione DSL come Xtext , puoi definire la tua grammatica matematica semplificata, che potrebbe trasforma generare una classe con contenente la rappresentazione Java (o qualsiasi altra lingua) delle tue formule.

Espressione DSL:

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

Output generato:

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 un esempio così semplice, i vantaggi derivanti dalla creazione della grammatica e del plugin Eclipse non varrebbero la pena, ma per progetti più complicati potrebbe portare grandi benefici, specialmente se la DSL consentisse agli imprenditori o ai ricercatori accademici di mantenere documenti formali in un linguaggio constrongvole e si assicuri che il loro lavoro sia stato tradotto in modo accurato nel linguaggio di implementazione del progetto.

    
risposta data 24.10.2014 - 05:04
fonte
4

In C # potresti utilizzare i metodi di estensione.

Il testo sottostante si legge molto bene una volta che ci si abitua alla notazione "postfix":

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();

Purtroppo la precedenza degli operatori rende le cose un po 'più brutte quando si tratta di numeri negativi qui. Se vuoi calcolare Math.Cos(-X) anziché -Math.Cos(X) dovrai racchiudere il numero tra parentesi:

var x = (-X).Cos() ...
    
risposta data 24.10.2014 - 08:54
fonte
2

C #: una variazione sulla risposta di Randall Cook , che mi piace perché mantiene il "look" matematico di il codice più dei metodi di estensione, consiste nell'utilizzare un wrapper ma utilizzare i riferimenti di funzione per le chiamate piuttosto che racchiuderli. Personalmente ritengo che il codice appaia più pulito, ma fondamentalmente sta facendo la stessa cosa.

Ho messo a punto un piccolo programma di test LINQPad che include le funzioni di Randall, i riferimenti alle mie funzioni e le chiamate dirette.

Le chiamate a cui la funzione fa riferimento prendono in pratica lo stesso tempo delle chiamate dirette. Le funzioni avvolte sono costantemente più lente, anche se non di molto.

Ecco il codice:

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);
    }
}

Risultati:

Function reference:
00:00:06.5952113

Wrapped function:
00:00:07.2570828

Direct call:
00:00:06.6396096
    
risposta data 24.10.2014 - 14:10
fonte
1

Usa Scala! Puoi definire operatori simbolici e non hai bisogno di parens per i tuoi metodi. Questo rende la via matematica più facile da interpretare.

Ad esempio, lo stesso calcolo in Scala e Java potrebbe essere qualcosa del tipo:

// 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));
}

Questo si aggiunge abbastanza rapidamente.

    
risposta data 24.10.2014 - 14:05
fonte

Leggi altre domande sui tag