Co można zrobić, aby poprawić czytelność kodu zorientowanego na matematykę w C #, Java i podobnych? [Zamknięte]

16

Jako programista C i programista C #, jedną z rzeczy, których nie lubię w C #, jest to, jak pełne są funkcje matematyczne. Za każdym razem, gdy będziesz musiał na przykład użyć funkcji Sin, cosinus lub power, musisz przygotować klasę statyczną Math. Prowadzi to do bardzo długiego kodu, gdy samo równanie jest dość proste. Problem staje się jeszcze poważniejszy, jeśli musisz typować typy danych. W rezultacie, moim zdaniem, cierpi czytelność. Na przykład:

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

W przeciwieństwie do zwykłego

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

Dotyczy to również innych języków, takich jak Java.

Nie jestem pewien, czy to pytanie rzeczywiście ma rozwiązanie, ale chciałbym wiedzieć, czy istnieją jakieś sztuczki używane przez programistów C # lub Java w celu poprawy czytelności kodu Math. Zdaję sobie jednak sprawę, że C # / Java / etc. nie są językami matematycznymi, takimi jak MATLAB lub podobnymi, więc ma to sens. Ale czasami trzeba jeszcze napisać kod matematyczny i byłoby wspaniale, gdyby uczynić go bardziej czytelnym.

9a3eedi
źródło
Nie znam żadnego konkretnego, ale prawdopodobnie mógłbyś znaleźć bibliotekę algebry, która pozwoliłaby ci zdefiniować funkcje matematyczne za pomocą łańcuchów, chociaż byłaby to pewna kara za wydajność.
raptortech97
7
Martwisz się o odrobinę dodatkowej gadatliwości, a jednak z radością ukrywasz znak „+” wśród „*” u jednych operatorów - wszystko bez nawiasów klamrowych - Podejrzewam, że masz złe priorytety.
mattnz
1
To był tylko przykład, ale dobry punkt
9a3eedi
6
W języku C # 6.0, będzie można napisać: using System.Math; … double x = -Cos(X) * Sin(Z) + Sin(X) * Sin(Y) * Cos(Z);.
svick

Odpowiedzi:

14

Można zdefiniować funkcje lokalne wywołujące globalne funkcje statyczne. Mamy nadzieję, że kompilator wstawi opakowania, a następnie kompilator JIT wygeneruje ścisły kod zestawu dla rzeczywistych operacji. Na przykład:

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

Możesz także tworzyć funkcje, które łączą typowe operacje matematyczne w pojedyncze operacje. Pozwoliłoby to zminimalizować liczbę przypadków, w których funkcje takie jak sini cospojawiają się w kodzie, przez co niezauważalne jest wywoływanie wywoływania globalnych funkcji statycznych. Na przykład:

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

Pracujesz na poziomie punktów i obrotów, a ukryte funkcje wyzwalające są zakopane.

Randall Cook
źródło
... dlaczego o tym nie pomyślałem :)
9a3eedi
Oznacziłem to jako prawidłową odpowiedź, ponieważ jest to rozwiązanie międzyplatformowe, które jest dość proste. Inne rozwiązania są również poprawne. Naprawdę nie mogę uwierzyć, że o tym nie pomyślałem :) to po prostu zbyt oczywiste
9a3eedi,
31

W Javie dostępnych jest wiele narzędzi, które sprawiają, że niektóre rzeczy są mniej szczegółowe, po prostu musisz o nich wiedzieć. W tym przypadku przydatny jest staticimport ( strona z samouczkiem , wikipedia ).

W tym przypadku,

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

działa całkiem nieźle ( ideone ). Trochę ciężko jest wykonać statyczny import całej klasy Math, ale jeśli robisz dużo matematyki, to może być wymagane.

Import statyczny pozwala zaimportować pole statyczne lub metodę do przestrzeni nazw tej klasy i wywołać ją bez wymagania nazwy pakietu. Często można to znaleźć w przypadkach testowych Junit, w których import static org.junit.Assert.*;można znaleźć wszystkie dostępne twierdzenia .


źródło
Doskonała odpowiedź. Nie byłem świadomy tej funkcji. W jakiej wersji Java jest to możliwe?
9a3eedi
@ 9a3eedi Po raz pierwszy został udostępniony w Javie 1.5.
Niezła technika. Lubię to. +1.
Randall Cook
1
@RandallCook W Javie 1.4 dni ludzie robili takie rzeczy, public interface Constants { final static public double PI = 3.14; }a potem public class Foo implements Constantswe wszystkich klasach, aby uzyskać dostęp do stałych w interfejsie. To zrobiło wielki bałagan. W wersji 1.5 dodano import statyczny, aby umożliwić pobieranie określonych stałych i funkcji statycznych bez konieczności implementowania interfejsu.
3
Możesz selektywnie importować niektóre funkcjeimport static java.lang.Math.cos;
maniak ratchet
5

W wersji C # 6.0 można korzystać z funkcji importu statycznego.

Twój kod może być:

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

Patrz: statyczne za pomocą instrukcji (wersja językowa AC # 6.0)

Inną funkcją „cukru syntaktycznego” w C # 6.0 jest wprowadzenie używania static. Dzięki tej funkcji możliwe jest wyeliminowanie wyraźnego odwołania do typu podczas wywoływania metody statycznej. Co więcej, użycie static pozwala wprowadzić tylko metody rozszerzenia dla określonej klasy, a nie wszystkie metody rozszerzenia w przestrzeni nazw.

EDYCJA: Od wydania Visual Studio 2015, CTP wydanego w styczniu 2015, import statyczny wymaga jawnego słowa kluczowego static. lubić:

using static System.Console;
Habib
źródło
4

Oprócz innych dobrych odpowiedzi tutaj, mogę również zalecić DSL dla sytuacji o znacznej złożoności matematycznej (nie przeciętne przypadki użycia, ale być może niektóre projekty finansowe lub akademickie).

Za pomocą narzędzia do generowania DSL, takiego jak Xtext , możesz zdefiniować własną uproszczoną gramatykę matematyczną, która z kolei mogłaby wygenerować klasę zawierającą formułę w języku Java (lub innym języku).

Wyrażenie DSL:

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

Wygenerowana moc wyjściowa:

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

W tak prostym przykładzie korzyści wynikające z tworzenia wtyczki gramatycznej i Eclipse nie byłyby opłacalne, ale w przypadku bardziej skomplikowanych projektów mogłoby to przynieść ogromne korzyści, zwłaszcza jeśli DSL umożliwiłoby ludziom biznesu lub naukowcom utrzymanie formalnych dokumentów w wygodnym język i zapewniamy, że ich praca została dokładnie przetłumaczona na język realizacji projektu.

Dan1701
źródło
8
Tak, ogólnie iz definicji DSL może być przydatny podczas pracy w określonej domenie. Jeśli jednak ten DSL nie istnieje lub nie odpowiada potrzebom, musisz go zachować , co może być problematyczne. Ponadto, w przypadku konkretnego pytania („Jak korzystać z metod / funkcji sin, cos,… bez pisania klasy Math za każdym razem”), DSL może być rozwiązaniem zbyt dużym.
mgoeminne
4

W języku C # można użyć metod rozszerzenia.

Poniżej przydaje się całkiem ładnie, gdy przyzwyczaisz się do notacji „postfiks”:

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

Niestety pierwszeństwo operatorów sprawia, że ​​sytuacja jest nieco brzydsza, gdy mamy do czynienia z liczbami ujemnymi. Jeśli Math.Cos(-X)zamiast tego chcesz obliczyć -Math.Cos(X), musisz podać liczbę w nawiasach:

var x = (-X).Cos() ...
rjnilsson
źródło
1
Nawiasem mówiąc, byłby to dobry przypadek użycia właściwości rozszerzenia, a nawet uzasadniony przypadek użycia nadużyć właściwości jako metod!
Jörg W Mittag
Tak myślałem. x.Sin()wymagałbym pewnych dostosowań, ale nadużywam metod przedłużania i to byłaby osobiście moja pierwsza skłonność.
WernerCD
2

C #: Wariacją odpowiedzi Randall Cooka , która podoba mi się, ponieważ zachowuje matematyczny „wygląd” kodu bardziej niż metody rozszerzenia, jest użycie opakowania, ale użycie odwołań funkcji do wywołań, a nie ich zawijanie. Osobiście uważam, że dzięki temu kod wygląda na czystszy, ale zasadniczo robi to samo.

Podrzuciłem mały program testowy LINQPad, w tym zawinięte funkcje Randalla, moje odwołania do funkcji i bezpośrednie wywołania.

Wywołania wywoływane przez funkcję zasadniczo zajmują tyle samo czasu, co wywołania bezpośrednie. Zawinięte funkcje są konsekwentnie wolniejsze - choć niezbyt duże.

Oto kod:

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

Wyniki:

Function reference:
00:00:06.5952113

Wrapped function:
00:00:07.2570828

Direct call:
00:00:06.6396096
Whelkaholism
źródło
1

Użyj Scali! Możesz zdefiniować operatory symboliczne i nie potrzebujesz parens dla swoich metod. To sprawia, że matematyczny sposób łatwiejszy do zinterpretowania.

Na przykład takie same obliczenia w Scali i Javie mogą wyglądać następująco:

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

Daje to dość szybko.

Rex Kerr
źródło
2
Scala nie jest dostępna w CLR, tylko JVM. Dlatego nie jest tak naprawdę realną alternatywą dla C #.
ben rudgers
@benrudgers - C # nie działa na JVM, więc nie jest tak naprawdę realną alternatywą dla Javy, o którą również zadawano to pytanie. Pytanie nie określa, że ​​musi to być CLR!
Rex Kerr
Może jestem luddytą, ale dwa dodatkowe znaki dla „kropki” zamiast „*”, z tą korzyścią, że kod jest wyraźniejszy, wydają się niewielką ceną do zapłacenia. Dobra odpowiedź.
user949300,