¿Qué se puede hacer para mejorar la legibilidad del código orientado a las matemáticas en C #, Java y similares? [cerrado]

16

Como programador de C y programador de C #, una de las cosas que no me gusta de C # es cuán detalladas son las funciones matemáticas. Cada vez que tendría que usar una función Sin, coseno o potencia, por ejemplo, tendría que anteponer la clase estática Math. Esto lleva a un código muy largo cuando la ecuación en sí es bastante simple. El problema empeora aún más si necesita escribir tipos de datos. Como resultado, en mi opinión, la legibilidad sufre. Por ejemplo:

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

A diferencia de simplemente

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

Este es también el caso en otros idiomas como Java.

No estoy seguro de si esta pregunta realmente tiene una solución, pero me gustaría saber si hay algún truco que C # o los programadores de Java usan para mejorar la legibilidad del código matemático. Sin embargo, me doy cuenta de que C # / Java / etc. no son lenguajes orientados a las matemáticas como MATLAB o similares, por lo que tiene sentido. Pero de vez en cuando uno todavía necesitaría escribir código matemático y sería genial si pudiera hacerlo más legible.

9a3eedi
fuente
No conozco ninguno específicamente, pero probablemente puedas encontrar una biblioteca de álgebra que te permita definir funciones matemáticas con cadenas, aunque habría alguna penalización en el rendimiento.
raptortech97
77
Te preocupas por un poco de verbosidad extra pero ocultas felizmente un '+' entre '*' con operadores unarios, todo sin llaves, sospecho que tienes prioridades equivocadas.
mattnz
1
Fue solo un ejemplo, pero buen punto
9a3eedi
66
En C # 6.0, usted será capaz de escribir: using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
svick

Respuestas:

14

Puede definir funciones locales que llaman a las funciones estáticas globales. Con suerte, el compilador incluirá los envoltorios, y luego el compilador JIT producirá un código de ensamblaje ajustado para las operaciones reales. Por ejemplo:

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

También puede crear funciones que agrupen operaciones matemáticas comunes en operaciones individuales. Esto minimizaría la cantidad de instancias en las que las funciones aparecen siny cosaparecen en su código, lo que hace que la dificultad de invocar las funciones estáticas globales sea menos notable. Por ejemplo:

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

Está trabajando a nivel de puntos y rotaciones, y las funciones trigonométricas subyacentes están enterradas.

Randall Cook
fuente
... por qué no pensé en eso :)
9a3eedi
Marqué esto como la respuesta correcta porque es una solución multiplataforma que es lo suficientemente simple. Las otras soluciones también son correctas. Aunque realmente no puedo creer que no haya pensado en esto :) es demasiado obvio
9a3eedi
31

Dentro de Java, hay muchas herramientas disponibles para hacer que ciertas cosas sean menos detalladas, solo hay que tenerlas en cuenta. Uno que es útil en este caso es el de la staticimportación ( página de tutorial , wikipedia ).

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

corre bastante bien ( ideone ). Es un poco pesado hacer una importación estática de todos la clase de Matemáticas, pero si está haciendo muchas matemáticas, entonces podría ser necesario.

La importación estática le permite importar un campo o método estático en el espacio de nombres de esta clase e invocarlo sin requerir el nombre del paquete. A menudo encuentras esto en los casos de prueba de Junit donde import static org.junit.Assert.*;se encuentra para obtener todas las afirmaciones disponibles.


fuente
Excelente respuesta No estaba al tanto de esta característica. ¿Bajo qué versión de Java es esto posible?
9a3eedi
@ 9a3eedi Se puso a disposición por primera vez en Java 1.5.
Buena técnica Me gusta. +1.
Randall Cook
1
@RandallCook En Java 1.4 días, la gente haría cosas como public interface Constants { final static public double PI = 3.14; }y luego public class Foo implements Constantsen todas las clases para obtener acceso a las constantes en la interfaz. Esto hizo un gran desastre. Entonces, con 1.5, se agregó la importación estática para permitir la extracción de constantes específicas y funciones estáticas sin tener que implementar una interfaz.
3
Puede importar selectivamente ciertas funcionesimport static java.lang.Math.cos;
Ratchet Freak
5

Con C # 6.0 puede usar la función de importación estática.

Tu código podría ser:

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

Consulte: Declaraciones de uso estático (Vista previa del lenguaje AC # 6.0)

Otra característica del "azúcar sintáctico" C # 6.0 es la introducción del uso de static. Con esta característica, es posible eliminar una referencia explícita al tipo al invocar un método estático. Además, el uso de static le permite introducir solo los métodos de extensión en una clase específica, en lugar de todos los métodos de extensión dentro de un espacio de nombres.

EDITAR: desde Visual Studio 2015, CTP lanzado en enero de 2015, la importación estática requiere una palabra clave explícita static. me gusta:

using static System.Console;
Habib
fuente
4

Además de las otras buenas respuestas aquí, también podría recomendar un DSL para situaciones con una complejidad matemática sustancial (no casos de uso promedio, pero tal vez algunos proyectos financieros o académicos).

Con una herramienta de generación de DSL como Xtext , puede definir su propia gramática matemática simplificada, que a su vez podría generar una clase que contenga la representación Java (o cualquier otro lenguaje) de sus fórmulas.

Expresión DSL:

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

Salida generada:

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

En un ejemplo tan simple, los beneficios de crear la gramática y el complemento de Eclipse no valdrían la pena, pero para proyectos más complicados, podría generar grandes beneficios, especialmente si el DSL permitiera a los empresarios o investigadores académicos mantener documentos formales en un ambiente cómodo. lenguaje, y tenga la seguridad de que su trabajo fue traducido con precisión al lenguaje de implementación del proyecto.

Dan1701
fuente
8
Sí, en general, y por definición, un DSL puede ser útil cuando trabajas en un dominio específico. Sin embargo, si este DSL no existe, o si no se ajusta a las necesidades, debe mantenerlo , lo que puede ser problemático. Además, para la pregunta específica ("¿Cómo puedo usar los métodos / funciones sin, cos, ... sin escribir la clase de Matemáticas cada vez"), un DSL es quizás una solución de gran tamaño.
mgoeminne
4

En C # podría usar métodos de extensión.

Lo siguiente se lee bastante bien una vez que te acostumbras a la notación "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();

Desafortunadamente, la precedencia del operador hace que las cosas sean un poco más feas cuando se trata de números negativos aquí. Si desea calcular en Math.Cos(-X)lugar de -Math.Cos(X), deberá encerrar el número entre paréntesis:

var x = (-X).Cos() ...
rjnilsson
fuente
1
Por cierto, esto sería un buen caso de uso para las propiedades de extensión, ¡e incluso un caso de uso legítimo para abusar de las propiedades como métodos!
Jörg W Mittag
Esto es lo que pensaba. x.Sin()tomaría algún ajuste, pero abuso de los métodos de extensión y esta sería, personalmente, mi primera inclinación
WernerCD
2

C #: Una variación de la respuesta de Randall Cook , que me gusta porque mantiene el "aspecto" matemático del código más que los métodos de extensión, es usar un contenedor pero usar referencias de función para las llamadas en lugar de ajustarlas. Personalmente, creo que hace que el código se vea más limpio, pero básicamente está haciendo lo mismo.

Abrí un pequeño programa de prueba LINQPad que incluía las funciones ajustadas de Randall, mis referencias de funciones y las llamadas directas.

Las llamadas a las que se hace referencia a la función básicamente toman el mismo tiempo que las llamadas directas. Las funciones ajustadas son consistentemente más lentas, aunque no en gran medida.

Aquí está el código:

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

Resultados:

Function reference:
00:00:06.5952113

Wrapped function:
00:00:07.2570828

Direct call:
00:00:06.6396096
El alcoholismo
fuente
1

Use Scala! Puede definir operadores simbólicos y no necesita parens para sus métodos. Esto hace que las matemáticas manera más fácil de interpretar.

Por ejemplo, el mismo cálculo en Scala y Java podría ser algo como:

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

Esto se suma bastante rápido.

Rex Kerr
fuente
2
Scala no está disponible en CLR, solo en JVM. Por lo tanto, no es realmente una alternativa viable a C #.
Ben Rudgers
@benrudgers: C # no se ejecuta en la JVM, por lo que no es realmente una alternativa viable a Java, sobre la cual también se hizo la pregunta. ¡La pregunta no especifica que tiene que ser CLR!
Rex Kerr
Tal vez soy un ludita, pero dos caracteres adicionales para "punto" en lugar de "*", con la ventaja de que el código es más claro, parece un pequeño precio a pagar. Aún así, una buena respuesta.
user949300