O que se pode fazer para melhorar a legibilidade do código orientado à matemática em C #, Java e similares? [fechadas]

16

Como programador em C e programador em C #, uma das coisas que eu não gosto no C # é como são as funções matemáticas detalhadas. Toda vez que você precisaria usar uma função Sin, cosseno ou potência, por exemplo, teria que preceder a classe estática Math. Isso leva a um código muito longo quando a própria equação é bastante simples. O problema fica ainda pior se você precisar digitar tipos de dados. Como resultado, na minha opinião, a legibilidade sofre. Por exemplo:

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

Ao contrário de simplesmente

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

Este também é o caso em outros idiomas como Java.

Não tenho certeza se essa pergunta realmente tem uma solução, mas gostaria de saber se existem truques que os programadores de C # ou Java usam para melhorar a legibilidade do código Math. Percebo, no entanto, que C # / Java / etc. não são linguagens orientadas à matemática como MATLAB ou similar, por isso faz sentido. Mas, ocasionalmente, ainda é necessário escrever código matemático e será ótimo se você puder torná-lo mais legível.

9a3eedi
fonte
Eu não conheço nenhum especificamente, mas você provavelmente poderia encontrar uma biblioteca de álgebra que permitiria definir funções matemáticas com strings, embora houvesse alguma penalidade no desempenho.
raptortech97
7
Você se preocupa com um pouco de verbosidade extra, mas felizmente esconde um '+' entre '*' com operadores unários - todos sem chaves - suspeito que você tenha prioridades erradas.
mattnz
1
Foi apenas um exemplo, mas bom ponto
9a3eedi
6
No C # 6.0, você será capaz de escrever: using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
svick

Respostas:

14

Você pode definir funções locais que chamam as funções estáticas globais. Esperançosamente, o compilador alinhará os wrappers e, em seguida, o compilador JIT produzirá um código de montagem rígido para as operações reais. Por exemplo:

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

Você também pode criar funções que agrupam operações matemáticas comuns em operações únicas. Isso minimizaria o número de instâncias em que funções gostam sine cosaparecem no seu código, tornando menos perceptível a dificuldade de chamar as funções estáticas globais. Por exemplo:

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

Você está trabalhando no nível de pontos e rotações, e as funções trigonométricas subjacentes são ocultadas.

Randall Cook
fonte
... por que eu não pensei nisso :)
9a3eedi
Marquei isso como a resposta correta, porque é uma solução multiplataforma bastante simples. As outras soluções também estão corretas. Eu realmente não posso acreditar que eu não pensei nisso embora :) é simplesmente muito óbvio
9a3eedi
31

No Java, existem muitas ferramentas disponíveis para tornar certas coisas menos detalhadas, você só precisa estar ciente delas. Um que é útil nesse caso é o da staticimportação ( página do tutorial , wikipedia ).

Nesse 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 muito bem ( ideone ). É um pouco pesado fazer uma importação estática de toda a classe Math, mas se você estiver fazendo muita matemática, pode ser necessário.

A importação estática permite importar um campo ou método estático para o espaço de nomes dessa classe e invocá-lo sem exigir o nome do pacote. Você costuma encontrar isso nos casos de teste do Junit, onde import static org.junit.Assert.*;é possível obter todas as declarações disponíveis.


fonte
Excelente resposta. Eu não estava ciente desse recurso. Em qual versão do Java isso é possível?
9a3eedi
@ 9a3eedi Foi disponibilizado pela primeira vez no Java 1.5.
Boa técnica. Eu gosto disso. +1.
Randall Cook
1
@RandallCook Em Java 1,4 dias, as pessoas faziam coisas como essas public interface Constants { final static public double PI = 3.14; }e, public class Foo implements Constantsem todas as classes, para obter acesso às constantes na interface. Isso fez uma grande bagunça. Portanto, com o 1.5, a importação estática foi adicionada para permitir a inserção de constantes e funções estáticas específicas sem a necessidade de implementar uma interface.
3
Você pode importar seletivamente certas funçõesimport static java.lang.Math.cos;
aberração catraca
5

Com o C # 6.0, você pode usar o recurso de importação estática.

Seu código pode 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: Static Using Statements (Visualização de idioma do AC # 6.0)

Outro recurso "açúcar sintático" do C # 6.0 é a introdução do uso de estática. Com esse recurso, é possível eliminar uma referência explícita ao tipo ao invocar um método estático. Além disso, o uso estático permite introduzir apenas os métodos de extensão em uma classe específica, em vez de todos os métodos de extensão em um espaço para nome.

EDIT: desde o Visual Studio 2015, o CTP lançado em janeiro de 2015, a importação estática requer uma palavra-chave explícita static. gostar:

using static System.Console;
Habib
fonte
4

Além das outras boas respostas aqui, também posso recomendar uma DSL para situações com complexidade matemática substancial (não casos de uso médios, mas talvez alguns projetos financeiros ou acadêmicos).

Com uma ferramenta de geração de DSL como o Xtext , você pode definir sua própria gramática matemática simplificada, que por sua vez pode gerar uma classe contendo a representação Java (ou qualquer outra linguagem) de suas fórmulas.

Expressão DSL:

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

Saída gerada:

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

Em um exemplo tão simples, os benefícios da criação da gramática e do plug-in do Eclipse não valeriam a pena, mas para projetos mais complicados, poderia render grandes benefícios, especialmente se o DSL permitisse que pessoas de negócios ou pesquisadores acadêmicos mantivessem documentos formais de maneira confortável. idioma e tenha certeza de que o trabalho deles foi traduzido com precisão para o idioma de implementação do projeto.

Dan1701
fonte
8
Sim, em geral, e por definição, uma DSL pode ser útil quando você trabalha em um domínio específico. No entanto, se esse DSL não existir ou se não atender às necessidades, você deverá mantê- lo, o que pode ser problemático. Além disso, para a pergunta específica ("Como posso usar o sin, cos, ... métodos / funções sem escrever a classe Math todas as vezes"), uma DSL é talvez uma solução superdimensionada.
mgoeminne
4

Em C #, você poderia usar métodos de extensão.

A seguir, você lê bem quando se acostuma à notação "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();

Infelizmente, a precedência do operador torna as coisas um pouco mais feias quando se lida com números negativos aqui. Se você deseja calcular em Math.Cos(-X)vez de -Math.Cos(X), precisará colocar o número entre parênteses:

var x = (-X).Cos() ...
rjnilsson
fonte
1
Aliás, isso seria um bom caso de uso para propriedades de extensão e até mesmo um caso de uso legítimo por abusar de propriedades como métodos!
Jörg W Mittag
Foi o que pensei. x.Sin()precisaria de algum ajuste, mas eu abuso os métodos de extensão e essa seria, pessoalmente, minha primeira inclinação.
WernerCD
2

C #: Uma variação da resposta de Randall Cook , que eu gosto porque mantém a "aparência" matemática do código mais do que os métodos de extensão, é usar um wrapper, mas usar referências de função para as chamadas em vez de envolvê-las. Pessoalmente, acho que faz o código parecer mais limpo, mas basicamente está fazendo a mesma coisa.

Eu encerrei um pequeno programa de teste do LINQPad, incluindo as funções agrupadas de Randall, minhas referências de função e as chamadas diretas.

As chamadas referenciadas à função geralmente levam o mesmo tempo que as chamadas diretas. As funções agrupadas são consistentemente mais lentas - embora não em grande quantidade.

Aqui está o 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
Whelkaholism
fonte
1

Use Scala! Você pode definir operadores simbólicos e não precisa de parênteses para seus métodos. Isso facilita muito a interpretação da matemática .

Por exemplo, o mesmo cálculo no Scala e Java pode 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));
}

Isso aumenta muito rapidamente.

Rex Kerr
fonte
2
Scala não está disponível no CLR, apenas na JVM. Portanto, não é realmente uma alternativa viável ao C #.
ben rudgers
@benrudgers - O C # não roda na JVM, portanto, não é realmente uma alternativa viável ao Java, sobre a qual a pergunta também foi feita. A pergunta não especifica que deve ser CLR!
Rex Kerr
Talvez eu seja ludita, mas dois caracteres extras para "ponto" em vez de "*", com o benefício de que o código é mais claro, parece um preço pequeno a pagar. Ainda assim, uma boa resposta.
user949300