Czy klasa Matrix powinna być immutable?

0

Taka zagadka inspirowana rozmowami z paroma osobami.
Czy obiekt klasy reprezentującą algebraiczną macierz powinien być niezmienny? Moim zdaniem, fajnie by było, bo jest to zabezpieczenie przed efektami ubocznymi, ale z drugiej strony zapis wielu algorytmów stałby się nieczytelny.
Co o tym sądzicie?

0

Skłaniałbym się ku dwóm klasom: Matrix i MutableMatrix. Używając tej drugiej tam, gdzie znacznie poprawiłoby to czytelność lub wydajność.

0

Zwykle jak już się używa specjalnej klasy do macierzy, to wydajność jest wtedy ważna. Dodatkowo nie spotkałem się z niezmienną macierzą w używanych przeze mnie bibliotekach, co też jest argumentem przeciwko robieniu jej immutable. W tym wypadku trzeba by się zastanowić jakie są przypadki użycia tej klasy, w bibliotece bardziej ogólnej zostawiłbym jako mutable. Nieczytelny zapis algorytmów będzie większym źródłem błędów niż zmienność macierzy.

0

Zdecydowanie macierz powinna być Mutable.

  • Często potrzebne są macierze duże, a wprowadzanie poprawek do takiej macierzy byłoby kosztowne.
  • niezmienna macierz utrudniłaby implementacje wielu algorytmów
  • zrobienie macierzy niezmiennej, z macierzy zmiennej to nie problem
  • zawsze można zaimplementować copy-on-write by zabezpieczyć się przed efektami ubocznymi (chociaż nie wiem jak to powinno wyglądać w Java).
0

zawsze można zaimplementować copy-on-write by zabezpieczyć się przed efektami ubocznymi (chociaż nie wiem jak to powinno wyglądać w Java).

CopyOnWrite to wrapper na niezmienną stukturę danych, który z zewnątrz zachowuje się jak zmienna struktura danych. Zaletą COW jest to, że operacje niemodyfikujące nie muszą być synchronizowane, jeśli raz zapiszemy sobie w metodzie referencję do niezmiennej struktury i korzystamy z niej aż do zwrócenia wartości.

zrobienie macierzy niezmiennej, z macierzy zmiennej to nie problem

macierz zmienną używa się tak:
macierz.pomnóż(macierz2);
macierz niezmienną uzywa się tak:
macierz = macierz.pomnóż(macierz2);
jak zmieniasz z niezmiennej na zmienną to musisz pozamieniać wszystkie użycia

Nieczytelny zapis algorytmów będzie większym źródłem błędów niż zmienność macierzy.

nie zawsze to będzie prawdą.

mamy funkcję:
f(m) = m.plus(2).mnóż(m.plus(5))

  1. macierz niemutowalna:
    metoda klonuj() robi klona, plus(x) zwraca macierz z każdą komórką powiększoną o x, mnóż(macierz2) zwraca iloczyn macierzy
    kod:
def f(m: Macierz) = m.plus(2).mnóż(m.plus(5))
  1. macierz mutowalna:
    klonuj() robi odizolowanego klona, plus(x) zwiększa każdą komórkę o x, mnóż(macierz2) zamienia macierz na iloczyn macierzy
    wtedy kod jest taki:
def f(m: Macierz) {
  val m2 = m.klonuj()
  m.plus(2)
  m2.plus(5)
  m.mnóż(m2)
}

Można by oczywiście zrobić mutowalną macierz z metodami zarówno mutującymi ją jak i metodami zwracającymi nowe macierze, wtedy mielibyśmy niektóre zalety z obu podejść.

0
Wibowit napisał(a):

macierz zmienną używa się tak:
macierz.pomnóż(macierz2);
macierz niezmienną uzywa się tak:
macierz = macierz.pomnóż(macierz2);
jak zmieniasz z niezmiennej na zmienną to musisz pozamieniać wszystkie użycia

Implementacja niezmiennego mnożenia macierzy będzie raczej dość nieczytelna, do tego wymagająca utworzenia kopii macierzy, ja bym tego pod żadnym pozorem nie robił.
Intuicyjnie spodziewamy się, że działania arytmetyczne będą zwracały wyniki, więc zaimplementowane funkcje powinny zwracać nowe obiekty, a nie modyfikować stare. No i jak sam pokazałeś swoim kodem, to podejście jest krótsze i czytelniejsze.

Może ja niejasno się wyraziłem, jako mutowalność określam możliwość zmiany obiektu, który został już utworzony. W przypadku macierzy oznacza to możliwość podmiany jej elementów, na poziomie kodu można to rozumieć jako funkcję set(r, c, newValue). W przypadku niemutowalnej macierzy ta funkcja będzie zwracała nowy obiekt, co moim zdaniem spowoduje stratę wydajności, gdy np. każda zmiana jednego elementu powoduje powstanie nowej kopii całej macierzy. Do tego zamiast:

macierz.set(x, y, funkcjaObliczającaWartośćNowegoElementu(...));

Trzeba by wielokrotnie pisać:

macierz = macierz.set(x, y, funkcjaObliczającaWartośćNowegoElementu(...));

Co jest chyba lekkim WTFem...

0

Zamiast:

macierz.set(...);
macierz.set(...);
macierz.set(...);

miałbyś:

macierz = macierz
  .set(...)
  .set(...)
  .set(...);

A konstruowanie mnożenia? Możesz zrobić konstruktor przyjmujący funkcję o parametrach (x, y) i zwracającą obliczoną wartość komórki.

0

macierz zmienną używa się tak:

macierz.pomnóż(macierz2);

macierz niezmienną uzywa się tak:

macierz = macierz.pomnóż(macierz2);

To akurat nieprawda, nic nie szkodzi na przeszkodzie żeby był na przykład operator *, a nawet *=: (tak, zależy od języka, ale chodzi o zasadę)

matrix *= macierz2;

I macierz może tutaj być immutable...

Dowód:

class Scalar
{
    readonly int i;

    public Scalar(int i)
    {
        this.i = i;
    }

    public static Scalar operator *(Scalar fst, Scalar snd)
    {
        return new Scalar(fst.i * snd.i);
    }

    public int I { get { return i; } }
}

class Program
{
    static void Main(string[] args)
    {
        Scalar a = new Scalar(2);
        Scalar b = new Scalar(6);

        b *= a; // uwaga.

        Console.WriteLine(a.I);
        Console.WriteLine(b.I);
    }
}

Największy problem to właśnie indeksowanie.

Chociaż, z drugiej strony, raczej rzadko korzysta się z kompletnie randomowej pozycji w macierzy (tak jak w liście np) - można by spróbować rozpoznać wzorce które się tutaj pojawiają (forEach albo map na przykład) i napisać do nich funkcje.

0

a i jeszcze:
bez bardzo sprytnie optymalizującego kompilatora macierz niemutowalna może czasem być nieodpowiednia wydajnościowo. no ale tego chyba nie trzeba powtarzać.

0
Wibowit napisał(a):

Zamiast:

macierz.set(...);
macierz.set(...);
macierz.set(...);

miałbyś:

macierz = macierz
  .set(...)
  .set(...)
  .set(...);

Moim zdaniem, to lepiej wygląda dla wielu operacji "pod rząd", ale dla jednej gorzej.

A konstruowanie mnożenia? Możesz zrobić konstruktor przyjmujący funkcję o parametrach (x, y) i zwracającą obliczoną wartość komórki.

I jakby to wyglądało dla mnożenia, bo nie kumam?

0

konstruktor przymowałby funkcję f(x, y) => v

macierz(x, y, f) = {
  a = new array(x, y) // tablica dwuwymiarowa
  for (i <- (1 to x); j <- (1 to y))
    a(i, j) = f(i, j)
}
mnóż(m1, m2) = 
 macierz(m1.x, m2.y, (x, y) => 
  var c = 0
  for (i <- (1 to m1.y)) 
    c += m1(x, i) * m2(i, y)
  return c
  )

edit:
zamiast mnóż(m1, m2) oczywiście trzeba zrobić this.mnóż(m2)

mniej więcej tak

0 użytkowników online, w tym zalogowanych: 0, gości: 0