Czy istnieje konkretna strategia projektowania, która może być zastosowana do rozwiązania większości problemów z kurczakiem i jajami przy użyciu niezmiennych obiektów?

17

Pochodzę z OOP (Java), uczę się Scali na własną rękę. Chociaż łatwo dostrzegam zalety korzystania z niezmiennych obiektów indywidualnie, trudno mi jest dostrzec, jak można zaprojektować taką aplikację. Podam przykład:

Powiedzmy, że mam przedmioty reprezentujące „materiały” i ich właściwości (projektuję grę, więc naprawdę mam ten problem), takie jak woda i lód. Miałbym „menedżera”, który jest właścicielem wszystkich takich instancji materiałów. Jedną właściwością byłby punkt zamarzania i topnienia oraz to, do czego materiał zamarza lub topi się.

[EDYCJA] Wszystkie materialne wystąpienia są „singleton”, podobnie jak Java Enum.

Chcę, żeby „woda” powiedziała, że ​​zamarza do „lodu” w 0 ° C, a „lód”, by powiedzieć, że topi się do „wody” w 1 ° C. Ale jeśli woda i lód są niezmienne, nie mogą uzyskać odniesienia do siebie jako parametrów konstruktora, ponieważ jeden z nich musi zostać utworzony jako pierwszy i nie można uzyskać odniesienia do nieistniejącego jeszcze innego jako parametru konstruktora. Mógłbym rozwiązać ten problem, podając im zarówno odwołanie do kierownika, aby mogli zapytać go, aby znaleźć inną istotną instancję, której potrzebują za każdym razem, gdy są proszeni o ich właściwości zamrażania / topienia, ale wtedy mam ten sam problem między kierownikiem i materiały, które potrzebują odniesienia do siebie, ale można je podać tylko w konstruktorze dla jednego z nich, więc ani menedżer, ani materiał nie mogą być niezmienne.

Czy po prostu nie można obejść tego problemu, czy też muszę użyć „funkcjonalnych” technik programowania lub innego wzoru, aby go rozwiązać?

Sebastien Diot
źródło
2
dla mnie, tak jak mówisz, nie ma wody ani lodu. Jest tylko h2omateriał
komara
1
Wiem, że miałoby to bardziej „naukowy sens”, ale w grze łatwiej jest modelować go jako dwa różne materiały o „ustalonych” właściwościach, niż jeden materiał o „zmiennych” właściwościach w zależności od kontekstu.
Sebastien Diot,
Singleton to głupi pomysł.
DeadMG,
@DeadMG Cóż, OK. Nie są prawdziwymi singletonami Java. Mam na myśli, że nie ma sensu tworzyć więcej niż jednego wystąpienia każdego z nich, ponieważ są one niezmienne i byłyby sobie równe. W rzeczywistości nie będę mieć żadnych rzeczywistych instancji statycznych. Moje obiekty „root” to usługi OSGi.
Sebastien Diot,
Odpowiedź na to drugie pytanie wydaje się potwierdzać moje podejrzenie, że sprawy szybko się komplikują z niezmiennymi: programmers.stackexchange.com/questions/68058/...
Sebastien Diot

Odpowiedzi:

2

Rozwiązaniem jest trochę oszukać. Konkretnie:

  • Utwórz A, ale pozostaw jego odwołanie do B niezainicjowane (ponieważ B jeszcze nie istnieje).

  • Utwórz B i poprowadź go do A.

  • Zaktualizuj A, aby wskazywał na B. Nie aktualizuj ani A ani B po tym.

Można to zrobić jawnie (przykład w C ++):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

lub pośrednio (przykład w Haskell):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

Przykład Haskella wykorzystuje leniwą ocenę, aby uzyskać iluzję wzajemnie niezmiennych wartości. Wartości zaczynają się od:

a = 0 : <thunk>
b = 1 : a

ai boba są niezależnie ważnymi formami głowy-normalności . Każdy minus może być skonstruowany bez potrzeby uzyskiwania końcowej wartości drugiej zmiennej. Podczas oceny thunk wskaże te same bpunkty danych do.

Dlatego jeśli chcesz, aby dwie niezmienne wartości wskazywały na siebie, albo musisz zaktualizować pierwszą po zbudowaniu drugiej, albo użyć mechanizmu wyższego poziomu, aby zrobić to samo.


W twoim konkretnym przykładzie mogę wyrazić to w Haskell jako:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

Jednak omijam ten problem. Wyobrażam sobie, że w podejściu obiektowym, w którym setTemperaturemetoda jest dołączana do wyniku każdego Materialkonstruktora, musielibyście mieć konstruktory skierowane do siebie. Jeśli konstruktory są traktowane jako niezmienne wartości, możesz użyć podejścia opisanego powyżej.

Joey Adams
źródło
(zakładając, że zrozumiałem składnię Haskella) Myślę, że moje obecne rozwiązanie jest bardzo podobne, ale zastanawiałem się, czy to „właściwe”, czy może istnieje coś lepszego. Najpierw tworzę „uchwyt” (odniesienie) dla każdego (jeszcze nie utworzonego) obiektu, następnie tworzę wszystkie obiekty, podając im potrzebne uchwyty, a na końcu inicjuję uchwyt dla obiektów. Obiekty same w sobie są niezmienne, ale nie uchwyty.
Sebastien Diot,
6

W twoim przykładzie zastosujesz transformację do obiektu, więc użyłbym czegoś takiego jak ApplyTransform()metoda, która zwraca, BlockBasezamiast próbować zmienić bieżący obiekt.

Na przykład, aby zmienić IceBlock na WaterBlock przez zastosowanie ciepła, nazwałbym coś takiego

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

a IceBlock.ApplyTemperature()metoda wyglądałaby mniej więcej tak:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}
Rachel
źródło
To dobra odpowiedź, ale niestety tylko dlatego, że nie wspomniałem, że moje „materiały”, a nawet moje „bloki”, są singletonami, więc nowy WaterBlock () po prostu nie wchodzi w grę. Jest to główna zaleta niezmienności, można z nich korzystać w nieskończoność. Zamiast 500 000 bloków w pamięci RAM, mam 500 000 odniesień do 100 bloków. Znacznie taniej!
Sebastien Diot,
A co powiesz na powrót BlockList.WaterBlockzamiast tworzenia nowego bloku?
Rachel
Tak, właśnie to robię, ale jak zdobyć listę bloków? Oczywiście bloki muszą zostać utworzone przed listą bloków, a zatem, jeśli blok jest naprawdę niezmienny, nie może otrzymać listy bloków jako parametru. Skąd więc, do kogo pobiera listę? Moja ogólna uwaga polega na tym, że zwiększając stopień skomplikowania kodu, rozwiązujesz problem z kurczakiem i jajkami na jednym poziomie, aby uzyskać go ponownie na następnym. Zasadniczo nie widzę możliwości stworzenia całej aplikacji opartej na niezmienności. Wydaje się, że dotyczy to tylko „małych obiektów”, ale nie pojemników.
Sebastien Diot,
@Sebastien Myślę, że BlockListto tylko staticklasa odpowiedzialna za pojedyncze wystąpienia każdego bloku, więc nie musisz tworzyć wystąpienia BlockList(jestem przyzwyczajony do C #)
Rachel
@Sebastien: Jeśli używasz Singeltons, płacisz cenę.
DeadMG,
6

Innym sposobem na przerwanie cyklu jest rozdzielenie problemów związanych z materiałem i transmutacją w niektórych wymyślonych językach:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);
SingleNegationElimination
źródło
Huh, ciężko mi było ją przeczytać na początku, ale myślę, że jest to w zasadzie to samo podejście, które zalecałem.
Aidan Cully,
1

Jeśli zamierzasz używać funkcjonalnego języka i chcesz zdać sobie sprawę z korzyści niezmienności, powinieneś podejść do problemu z tą myślą. Próbujesz zdefiniować obiekt typu „lód” lub „woda”, który może obsługiwać szereg temperatur - aby zachować niezmienność, będziesz musiał utworzyć nowy obiekt za każdym razem, gdy zmienia się temperatura, co jest marnotrawstwem. Spróbuj więc uczynić koncepcje typu bloku i temperatury bardziej niezależnymi. Nie znam Scali (jest na mojej liście do nauki :-)), ale pożyczając od Joeya Adamsa w Haskell , sugeruję coś takiego:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

albo może:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(Uwaga: nie próbowałem tego uruchamiać, a mój Haskell jest trochę zardzewiały). Teraz logika przejścia jest oddzielona od rodzaju materiału, więc nie marnuje tyle pamięci i (moim zdaniem) jest dość nieco bardziej funkcjonalnie zorientowany.

Aidan Cully
źródło
Właściwie to nie próbuję używać „języka funkcjonalnego”, ponieważ po prostu go nie rozumiem! Jedyną rzeczą, którą normalnie zachowuję w każdym nietrywialnym przykładzie programowania funkcjonalnego jest: „Cholera, ja byłem bardziej sprytny!” To jest poza mną, jak to może mieć sens dla każdego. Z czasów moich studentów pamiętam, że Prolog (oparty na logice), Occam (domyślnie wszystko działa równolegle) i nawet asembler miał sens, ale Lisp był po prostu szalony. Ale zdaję sobie sprawę z przesunięcia kodu, który powoduje „zmianę stanu” poza „stan”.
Sebastien Diot,