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


16

Come programmatore C e programmatore C #, una delle cose che non mi piacciono di C # è quanto sono dettagliate le funzioni matematiche. Ogni volta che dovresti usare una funzione Sin, coseno o potenza, ad esempio, devi anteporre la classe statica matematica. Questo porta a un codice molto lungo quando l'equazione stessa è piuttosto semplice. Il problema peggiora ulteriormente se è necessario digitare i tipi di dati. Di conseguenza, secondo me, la leggibilità ne risente. Per 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 di altri linguaggi come Java.

Non sono sicuro che questa domanda abbia effettivamente una soluzione, ma vorrei sapere se ci sono dei trucchi che i programmatori C # o Java usano per migliorare la leggibilità del codice matematico. Mi rendo conto tuttavia che C # / Java / ecc. non sono linguaggi orientati alla matematica come MATLAB o simili, quindi ha senso. Ma a volte si dovrebbe ancora scrivere codice matematico e sarebbe bello se si potesse renderlo più leggibile.


Non ne conosco nessuno in particolare, ma probabilmente potresti trovare una libreria di algebra che ti permetterebbe di definire le funzioni matematiche con le stringhe, anche se ci sarebbe una certa penalità prestazionale.
raptortech97,


7
Ti preoccupi di un po 'di verbosità in più ma nascondi felicemente un' + 'tra' * 'con operatori unari - tutti senza parentesi graffe - Sospetto che tu abbia priorità sbagliate.
mattnz,

1
Era solo un esempio, ma un buon punto
9a3eedi

6
In C # 6.0, si sarà in grado di scrivere: using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
svick,

Risposte:


14

È possibile definire funzioni locali che chiamano le funzioni statiche globali. Si spera che il compilatore incorporerà i wrapper e quindi il compilatore JIT produrrà un codice assembly stretto per le operazioni effettive. Per 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);
    }
}

È inoltre possibile creare funzioni che raggruppano le operazioni matematiche comuni in singole operazioni. Ciò ridurrebbe al minimo il numero di istanze in cui le funzioni piacciono sine coscompaiono nel codice, rendendo in tal modo meno evidente il clunkiness di invocare le funzioni statiche globali. Per 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 di trigger sottostanti vengono sepolte.


... perché non ci ho pensato :)
9a3eedi

Ho contrassegnato questa come la risposta corretta perché è una soluzione multipiattaforma abbastanza semplice. Anche le altre soluzioni sono corrette. Non riesco davvero a credere di non aver pensato a questo però :) è semplicemente troppo ovvio
9a3eedi

31

All'interno di Java, ci sono molti strumenti disponibili per rendere alcune cose meno dettagliate, devi solo conoscerle. Uno che è utile in questo caso è quello staticdell'importazione ( 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 tutto la classe di matematica, ma se stai facendo molta matematica, allora potrebbe essere richiesto.

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


Risposta eccellente. Non ero a conoscenza di questa funzione. In quale versione di Java è possibile farlo?
9a3eedi

@ 9a3eedi È stato reso disponibile per la prima volta in Java 1.5.

Bella tecnica. Mi piace. +1.
Randall Cook,

1
@RandallCook In Java 1.4 giorni, le persone farebbero cose come public interface Constants { final static public double PI = 3.14; }e poi public class Foo implements Constantsin tutte le classi per ottenere l'accesso alle costanti nell'interfaccia. Questo ha fatto un gran casino. Quindi, con 1.5, è stata aggiunta l'importazione statica per consentire l'inserimento di costanti e funzioni statiche specifiche senza dover implementare un'interfaccia.

3
È possibile importare in modo selettivo determinate funzioniimport static java.lang.Math.cos;
maniaco del cricchetto

5

Con C # 6.0 è possibile utilizzare la funzione di importazione statica.

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

Vedere: Dichiarazioni sull'uso statico (Anteprima lingua AC # 6.0)

Un'altra caratteristica "zucchero sintattico" di C # 6.0 è l'introduzione dell'uso statico. Con questa funzione, è possibile eliminare un riferimento esplicito al tipo quando si richiama un metodo statico. Inoltre, l'uso di static consente di introdurre solo i metodi di estensione su una classe specifica, anziché tutti i metodi di estensione all'interno di uno spazio dei nomi.

EDIT: da Visual Studio 2015, CTP rilasciato a gennaio 2015, l'importazione statica richiede una parola chiave esplicita static. piace:

using static System.Console;

4

Oltre alle altre buone risposte qui, potrei anche raccomandare un DSL per situazioni con sostanziale complessità matematica (non casi d'uso medi, ma forse alcuni progetti finanziari o accademici).

Con uno strumento di generazione DSL come Xtext , potresti definire la tua grammatica matematica semplificata, che a sua volta potrebbe generare una classe contenente la rappresentazione Java (o qualsiasi altro linguaggio) delle tue formule.

Espressione DSL:

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

Uscita generata:

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 della creazione della grammatica e del plug-in Eclipse non varrebbero la pena, ma per progetti più complicati, potrebbero apportare grandi benefici, soprattutto se il DSL consentisse a uomini d'affari o ricercatori accademici di conservare documenti formali in un ambiente confortevole lingua e si assicura che il loro lavoro sia stato tradotto accuratamente nella lingua di implementazione del progetto.


8
Sì, in generale e per definizione, un DSL può essere utile quando lavori in un dominio specifico. Tuttavia, se questo DSL non esiste, o se non soddisfa i bisogni, è necessario mantenerlo , il che potrebbe essere problematico. Inoltre, per la domanda specifica ("Come posso usare i metodi / funzioni sin, cos, ... senza scrivere la classe Math ogni volta"), un DSL è forse una soluzione sovradimensionata.
mgoeminne,

4

In C # potresti usare metodi di estensione.

Di seguito si legge abbastanza bene una volta che ci si abitua alla notazione "postfisso":

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

Sfortunatamente la precedenza dell'operatore rende le cose un po 'più brutte quando si tratta di numeri negativi qui. Se vuoi calcolare Math.Cos(-X)invece di -Math.Cos(X)te dovrai racchiudere il numero tra parentesi:

var x = (-X).Cos() ...

1
Per inciso, questo sarebbe un buon caso d'uso per le proprietà di estensione e persino un caso d'uso legittimo per abusare delle proprietà come metodi!
Jörg W Mittag,

Questo è quello che ho pensato. x.Sin()richiederebbe qualche aggiustamento, ma io abuso dei metodi di estensione e questa sarebbe, personalmente, la mia prima inclinazione.
WernerCD,

2

C #: una variazione sulla risposta di Randall Cook , che mi piace perché mantiene l'aspetto matematico del codice più dei metodi di estensione, è usare un wrapper ma usare i riferimenti di funzione per le chiamate piuttosto che racchiuderli. Personalmente penso che renda il codice più pulito, ma sostanzialmente sta facendo la stessa cosa.

Ho eliminato un piccolo programma di test LINQPad che includeva le funzioni di Randall, i miei riferimenti di funzione e le chiamate dirette.

La funzione a cui fanno riferimento le chiamate prende sostanzialmente lo stesso tempo delle chiamate dirette. Le funzioni avvolte sono costantemente più lente, anche se non di una quantità enorme.

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

1

Usa Scala! Puoi definire operatori simbolici e non hai bisogno di parentesi per i tuoi metodi. Questo rende la matematica modo 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.


2
Scala non è disponibile su CLR, ma solo su JVM. Quindi non è davvero una valida alternativa a C #.
ben rudgers,

@benrudgers - C # non funziona su JVM, quindi non è in realtà una valida alternativa a Java, di cui si è posta anche la domanda. La domanda non specifica che deve essere CLR!
Rex Kerr,

Forse sono un Luddite, ma due caratteri extra per "punto" anziché "*", con il vantaggio che il codice è più chiaro, sembrano un piccolo prezzo da pagare. Comunque, una buona risposta.
user949300,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.