Jak działa wykonanie różnicowe?


83

Widziałem kilka wzmianek o tym na Stack Overflow, ale wpatrywanie się w Wikipedię (odpowiednia strona została odtąd usunięta) i demo dynamicznego okna dialogowego MFC nie zrobiło nic, aby mnie oświecić. Czy ktoś może to wyjaśnić? Nauka zasadniczo innej koncepcji brzmi nieźle.


Opierając się na odpowiedziach: myślę, że lepiej to czuję. Wydaje mi się, że za pierwszym razem nie przyjrzałem się wystarczająco uważnie kodowi źródłowemu. W tej chwili mam mieszane uczucia co do wykonywania różnicowego. Z jednej strony może znacznie ułatwić niektóre zadania. Z drugiej strony, uruchomienie go i uruchomienie (to znaczy ustawienie go w wybranym języku) nie jest łatwe (jestem pewien, że tak by było, gdybym to lepiej zrozumiał) ... chociaż wydaje mi się, że jest to zestaw narzędzi wystarczy wykonać tylko raz, a następnie w razie potrzeby rozszerzyć. Myślę, że aby naprawdę to zrozumieć, prawdopodobnie będę musiał spróbować zaimplementować go w innym języku.


3
Dzięki za zainteresowanie Brian. Dla mnie interesujące jest to, że coś prostego wydaje się rozczarowujące. Dla mnie najładniejsze rzeczy są proste. Dbać.
Mike Dunlavey

1
Myślę, że brakuje mi czegoś ważnego. Teraz myślę: „to jest proste”. Gdybym naprawdę to zrozumiał, pomyślałbym: „To jest proste. I naprawdę niesamowite i przydatne”.
Brian

6
... Wciąż widzę ludzi prezentujących MVC tak, jakby to było najwspanialsze, i myślę, że wolałbym przejść na emeryturę, niż musieć to robić ponownie.
Mike Dunlavey

1
... aby "cofnąć", serializujesz / deserializujesz dane i wyodrębniasz plik, który jest XOR obu, który jest w większości zerowy, tak łatwo skompresowany. Użyj tego, aby odzyskać poprzednie dane. Teraz uogólnij na dowolną strukturę danych.
Mike Dunlavey

1
Nie chcesz zwiększać swojego obciążenia pracą, @MikeDunlavey, ale jeśli przegapiłeś to, Source Forge stracił łaskę z powodu wątpliwych praktyk biznesowych. Github.com to miejsce, w którym wiszą obecnie fajne dzieci. Mają naprawdę fajnego klienta Windows dla W7 na desktop.github.com
Prof. Falken

Odpowiedzi:


95

Rany, Brian, żałuję, że nie spotkałem się z twoim pytaniem wcześniej. Ponieważ jest to właściwie mój „wynalazek” (na dobre lub na złe), być może będę w stanie pomóc.

Wstawione: Najkrótszym możliwym wyjaśnieniem, jakie mogę zrobić, jest to, że jeśli normalne wykonanie jest jak wyrzucenie piłki w powietrze i złapanie jej, to wykonanie różnicowe jest jak żonglowanie.

Wyjaśnienie @ windfinder różni się od mojego i to jest OK. Ta technika nie jest łatwa do ogarnięcia głowy i zajęło mi jakieś 20 lat (z przerwami), aby znaleźć wyjaśnienia, które działają. Pozwólcie, że powtórzę tutaj:

  • Co to jest?

Wszyscy rozumiemy prostą ideę, że komputer przechodzi przez program, bierze rozgałęzienia warunkowe na podstawie danych wejściowych i robi różne rzeczy. (Załóżmy, że mamy do czynienia tylko z prostym, strukturalnym kodem goto-bez i bez powrotu). Ten kod zawiera sekwencje instrukcji, podstawowe warunki warunkowe, proste pętle i wywołania podprogramów. (Zapomnij o funkcjach zwracających wartości na razie.)

Teraz wyobraź sobie, że dwa komputery wykonują ten sam kod krok po kroku i potrafią porównać notatki. Komputer 1 działa z danymi wejściowymi A, a komputer 2 - z danymi wejściowymi B. Działają one krok po kroku obok siebie. Jeśli dojdą do warunkowej instrukcji, takiej jak IF (test) .... ENDIF, i jeśli mają różnicę zdań co do tego, czy test jest prawdziwy, to ten, który mówi, że test jest fałszywy, przeskakuje do ENDIF i czeka na jego siostra, aby nadrobić zaległości. (Dlatego kod ma taką samą strukturę, że wiemy, że siostra w końcu dotrze do ENDIF).

Ponieważ oba komputery mogą ze sobą rozmawiać, mogą porównywać notatki i szczegółowo wyjaśniać, w jaki sposób te dwa zestawy danych wejściowych i historie wykonania różnią się.

Oczywiście w wykonaniu różnicowym (DE) odbywa się to za pomocą jednego komputera, symulując dwa.

TERAZ, załóżmy, że masz tylko jeden zestaw danych wejściowych, ale chcesz zobaczyć, jak zmienił się od czasu 1 do czasu 2. Załóżmy, że program, który wykonujesz, jest serializatorem / deserializatorem. Podczas wykonywania zarówno serializujesz (zapisujesz) bieżące dane, jak i deserializujesz (wczytujesz) poprzednie dane (które zostały zapisane ostatnim razem, gdy to zrobiłeś). Teraz możesz łatwo zobaczyć, jakie są różnice między tym, jakie dane były ostatnio, a tym, jakie są tym razem.

Plik, do którego piszesz, i stary plik, z którego czytasz, razem tworzą kolejkę lub FIFO (pierwszy na wejściu, pierwszy na wyjściu), ale nie jest to bardzo głęboka koncepcja.

  • Do czego to jest dobre?

Przyszło mi do głowy, gdy pracowałem nad projektem graficznym, w którym użytkownik mógł konstruować małe procedury procesora wyświetlacza zwane „symbolami”, które można było łączyć w większe procedury, aby malować takie rzeczy, jak schematy rur, zbiorników, zaworów, itp. Chcieliśmy, aby diagramy były „dynamiczne” w tym sensie, że mogły się stopniowo aktualizować bez konieczności przerysowywania całego diagramu. (Sprzęt był powolny według dzisiejszych standardów.) Zdałem sobie sprawę, że (na przykład) procedura rysowania słupka wykresu słupkowego może zapamiętać jego starą wysokość i po prostu stopniowo się aktualizować.

To brzmi jak OOP, prawda? Jednak zamiast „uczynić” „obiekt”, mogłem skorzystać z przewidywalności sekwencji wykonania procedury diagramu. Mógłbym zapisać wysokość paska w sekwencyjnym strumieniu bajtów. Następnie, aby zaktualizować obraz, mógłbym po prostu uruchomić procedurę w trybie, w którym sekwencyjnie odczytuje swoje stare parametry, podczas gdy zapisuje nowe parametry, aby być gotowym do następnego przebiegu aktualizacji.

Wydaje się to głupio oczywiste i wydaje się zepsuć, gdy tylko procedura zawiera warunek, ponieważ wtedy nowy i stary strumień stracą synchronizację. Ale potem dotarło do mnie, że gdyby również zserializowali wartość logiczną testu warunkowego, mogliby wrócić do synchronizacji . Przekonanie się, a potem udowodnienie, że to zawsze zadziała, pod warunkiem przestrzegania prostej zasady („zasada trybu kasowania”) zajęło trochę czasu .

W rezultacie użytkownik może zaprojektować te „dynamiczne symbole” i złożyć je w większe diagramy, bez martwienia się o to, jak będą dynamicznie aktualizować, bez względu na to, jak skomplikowany lub strukturalnie zmienny byłby wyświetlacz.

W tamtych czasach musiałem się martwić o interferencję między obiektami wizualnymi, aby wymazanie jednego nie zaszkodziło innym. Jednak teraz używam tej techniki z kontrolkami systemu Windows i pozwalam systemowi Windows zająć się problemami z renderowaniem.

Więc co to osiąga? Oznacza to, że mogę zbudować okno dialogowe, pisząc procedurę malowania elementów sterujących, i nie muszę się martwić o faktyczne zapamiętywanie obiektów kontrolnych lub zajmowanie się ich przyrostową aktualizacją lub sprawianiem, że pojawiają się / znikają / poruszają się zgodnie z warunkami. Rezultatem jest znacznie mniejszy i prostszy kod źródłowy okna dialogowego, o mniej więcej rząd wielkości, a rzeczy takie jak układ dynamiczny lub zmiana liczby kontrolek lub posiadanie tablic lub siatek kontrolek są trywialne. Ponadto formant, taki jak pole edycji, może być w trywialny sposób powiązany z danymi aplikacji, które edytuje, i zawsze będzie można udowodnić poprawność, a ja nigdy nie będę musiał zajmować się jego zdarzeniami. Wprowadzenie pola edycyjnego dla zmiennej ciągu aplikacji jest edycją jednowierszową.

  • Dlaczego trudno to zrozumieć?

Najtrudniej było mi wyjaśnić, że wymaga to innego myślenia o oprogramowaniu. Programiści są tak mocno przywiązani do poglądu oprogramowania na obiekt-działanie, że chcą wiedzieć, jakie są obiekty, jakie są klasy, jak „budują” wyświetlacz i jak radzą sobie ze zdarzeniami. bomba, żeby ich z niej wysadzić. To, co staram się przekazać, to to, co naprawdę się liczy to, co musisz powiedzieć?Wyobraź sobie, że tworzysz język specyficzny dla domeny (DSL), w którym wszystko, co musisz zrobić, to powiedzieć „Chcę edytować zmienną A tutaj, zmienną B tam, a zmienną C tam”, a on w magiczny sposób zajmie się tym za Ciebie . Na przykład w Win32 istnieje ten „język zasobów” do definiowania okien dialogowych. To doskonale DSL, z wyjątkiem tego, że nie idzie wystarczająco daleko. Nie „żyje” w głównym języku proceduralnym, nie obsługuje zdarzeń za Ciebie, ani nie zawiera pętli / warunków / podprogramów. Ale to dobrze, a Dynamic Dialogs próbuje dokończyć robotę.

Zatem inny sposób myślenia jest następujący: aby napisać program, najpierw należy znaleźć (lub wymyślić) odpowiedni DSL i zakodować w nim jak najwięcej swojego programu. Niech to sobie z wszystkich obiektów i działań, które istnieją tylko dla dobra implementacja jest.

Jeśli chcesz naprawdę zrozumieć wykonywanie różnicowe i używać go, jest kilka trudnych problemów, które mogą Cię potknąć. Kiedyś zakodowałem to w makrach Lispa , gdzie te trudne bity mogą być obsłużone za Ciebie, ale w "normalnych" językach wymaga to pewnej dyscypliny programisty, aby uniknąć pułapek.

Przepraszam, że jestem tak rozwlekły. Gdybym nie miał sensu, byłbym wdzięczny, gdybyś zwrócił na to uwagę, a ja mogę spróbować to naprawić.

Dodany:

W Java Swing istnieje przykładowy program o nazwie TextInputDemo. Jest to statyczne okno dialogowe, zajmujące 270 linii (nie licząc listy 50 stanów). W dynamicznych dialogach (w MFC) jest to około 60 linii:

#define NSTATE (sizeof(states)/sizeof(states[0]))
CString sStreet;
CString sCity;
int iState;
CString sZip;
CString sWholeAddress;

void SetAddress(){
    CString sTemp = states[iState];
    int len = sTemp.GetLength();
    sWholeAddress.Format("%s\r\n%s %s %s", sStreet, sCity, sTemp.Mid(len-3, 2), sZip);
}

void ClearAddress(){
    sWholeAddress = sStreet = sCity = sZip = "";
}

void CDDDemoDlg::deContentsTextInputDemo(){
    int gy0 = P(gy);
    P(www = Width()*2/3);
    deStartHorizontal();
    deStatic(100, 20, "Street Address:");
    deEdit(www - 100, 20, &sStreet);
    deEndHorizontal(20);
    deStartHorizontal();
    deStatic(100, 20, "City:");
    deEdit(www - 100, 20, &sCity);
    deEndHorizontal(20);
    deStartHorizontal();
    deStatic(100, 20, "State:");
    deStatic(www - 100 - 20 - 20, 20, states[iState]);
    if (deButton(20, 20, "<")){
        iState = (iState+NSTATE - 1) % NSTATE;
        DD_THROW;
    }
    if (deButton(20, 20, ">")){
        iState = (iState+NSTATE + 1) % NSTATE;
        DD_THROW;
    }
    deEndHorizontal(20);
    deStartHorizontal();
    deStatic(100, 20, "Zip:");
    deEdit(www - 100, 20, &sZip);
    deEndHorizontal(20);
    deStartHorizontal();
    P(gx += 100);
    if (deButton((www-100)/2, 20, "Set Address")){
        SetAddress();
        DD_THROW;
    }
    if (deButton((www-100)/2, 20, "Clear Address")){
        ClearAddress();
        DD_THROW;
    }
    deEndHorizontal(20);
    P((gx = www, gy = gy0));
    deStatic(P(Width() - gx), 20*5, (sWholeAddress != "" ? sWholeAddress : "No address set."));
}

Dodany:

Oto przykładowy kod do edycji tablicy pacjentów szpitalnych w około 40 wierszach kodu. Linie 1-6 definiują „bazę danych”. Wiersze 10–23 określają ogólną zawartość interfejsu użytkownika. Wiersze 30–48 definiują elementy sterujące do edycji rekordu pojedynczego pacjenta. Zwróć uwagę, że forma programu prawie nie zwraca uwagi na zdarzenia w czasie, tak jakby wystarczyło tylko raz utworzyć wyświetlacz. Następnie, jeśli tematy są dodawane lub usuwane lub mają miejsce inne zmiany strukturalne, jest po prostu ponownie wykonywana, tak jakby była odtwarzana od zera, z wyjątkiem tego, że DE powoduje zamiast tego aktualizację przyrostową. Zaletą jest to, że programista nie musi poświęcać żadnej uwagi ani pisać żadnego kodu, aby wykonać przyrostowe aktualizacje interfejsu użytkownika i gwarantujemy ich poprawność. Mogłoby się wydawać, że to ponowne wykonanie byłoby problemem z wydajnością, ale tak nie jest,

1  class Patient {public:
2    String name;
3    double age;
4    bool smoker; // smoker only relevant if age >= 50
5  };
6  vector< Patient* > patients;

10 void deContents(){ int i;
11   // First, have a label
12   deLabel(200, 20, “Patient name, age, smoker:”);
13   // For each patient, have a row of controls
14   FOR(i=0, i<patients.Count(), i++)
15     deEditOnePatient( P( patients[i] ) );
16   END
17   // Have a button to add a patient
18   if (deButton(50, 20, “Add”)){
19     // When the button is clicked add the patient
20     patients.Add(new Patient);
21     DD_THROW;
22   }
23 }

30 void deEditOnePatient(Patient* p){
31   // Determine field widths
32   int w = (Width()-50)/3;
33   // Controls are laid out horizontally
34   deStartHorizontal();
35     // Have a button to remove this patient
36     if (deButton(50, 20, “Remove”)){
37       patients.Remove(p);
37       DD_THROW;
39     }
40     // Edit fields for name and age
41     deEdit(w, 20, P(&p->name));
42     deEdit(w, 20, P(&p->age));
43     // If age >= 50 have a checkbox for smoker boolean
44     IF(p->age >= 50)
45       deCheckBox(w, 20, “Smoker?”, P(&p->smoker));
46     END
47   deEndHorizontal(20);
48 }

Dodano: Brian zadał dobre pytanie i pomyślałem, że odpowiedź należy do głównego tekstu tutaj:

@Mike: Nie jestem pewien, co właściwie robi instrukcja „if (deButton (50, 20,„ Add ”)) {”. Co robi funkcja deButton? Czy twoje pętle FOR / END używają jakiegoś makra czy czegoś takiego? - Brian.

@Brian: Tak, instrukcje FOR / END i IF to makra. Projekt SourceForge ma pełną implementację. deButton zachowuje kontrolę przycisków. Gdy ma miejsce jakakolwiek akcja wprowadzana przez użytkownika, kod jest uruchamiany w trybie „zdarzenia sterującego”, w którym deButton wykrywa, że ​​został naciśnięty i oznacza, że ​​został naciśnięty, zwracając wartość TRUE. Zatem „if (deButton (...)) {... kod akcji ...} jest sposobem na dołączenie kodu akcji do przycisku, bez konieczności tworzenia zamknięcia lub pisania procedury obsługi zdarzenia. DD_THROW jest sposób kończenia przebiegu, gdy akcja jest wykonywana, ponieważ akcja mogła zmodyfikować dane aplikacji, więc kontynuowanie przebiegu „zdarzenia kontrolnego” przez procedurę jest nieprawidłowe. Jeśli porównasz to z pisaniem programów obsługi zdarzeń, oszczędza to ich zapisywania, i pozwala mieć dowolną liczbę kontrolek.

Dodano: Przepraszam, powinienem wyjaśnić, co rozumiem przez słowo „utrzymuje”. Kiedy procedura jest wykonywana po raz pierwszy (w trybie SHOW), deButton tworzy kontrolkę przycisku i zapamiętuje jej identyfikator w FIFO. Przy kolejnych przebiegach (w trybie UPDATE) deButton pobiera identyfikator z FIFO, modyfikuje go w razie potrzeby i umieszcza z powrotem w FIFO. W trybie ERASE czyta go z FIFO, niszczy i nie odkłada z powrotem, przez co „zbiera śmieci”. Dlatego wywołanie deButton zarządza całym okresem życia formantu, utrzymując go w zgodzie z danymi aplikacji, dlatego mówię, że „utrzymuje” go.

Czwarty tryb to ZDARZENIE (lub KONTROLA). Gdy użytkownik wpisze znak lub kliknie przycisk, to zdarzenie jest przechwytywane i rejestrowane, a następnie procedura deContents jest wykonywana w trybie EVENT. deButton pobiera identyfikator kontrolki przycisku z FIFO i pyta, czy to jest kliknięta kontrolka. Jeśli tak, zwraca TRUE, więc kod akcji może zostać wykonany. Jeśli nie, zwraca po prostu FALSE. Z drugiej strony deEdit(..., &myStringVar)wykrywa, czy zdarzenie było przeznaczone dla niego, a jeśli tak, przekazuje je do kontrolki edycji, a następnie kopiuje zawartość kontrolki edycji do myStringVar. Między tym a normalnym przetwarzaniem UPDATE myStringVar zawsze równa się zawartości kontrolki edycji. W ten sposób odbywa się „wiązanie”. Ten sam pomysł dotyczy pasków przewijania, pól list, pól kombi, wszelkiego rodzaju elementów sterujących, które umożliwiają edycję danych aplikacji.

Oto link do mojej edycji Wikipedii: http://en.wikipedia.org/wiki/User:MikeDunlavey/Difex_Article


4
Przepraszam, że nękam cię odpowiedziami, ale jeśli dobrze to rozumiem, w zasadzie przesuwasz swoje obliczenia coraz bliżej procesora i dalej od sprzętu wyjściowego. To niesamowity wgląd, ponieważ inwestujemy dużo zasobów w pomysł, że programując w obiektach i zmiennych, można je dość łatwo przetłumaczyć na najlepszy kod maszynowy, aby uzyskać ten sam wynik, co z pewnością nie ma miejsca! Chociaż możemy optymalizować kod podczas kompilacji, nie jest możliwa optymalizacja działań zależnych od czasu. Odrzuć zależność od czasu i pozwól prymitywom działać !
sova

2
@Joey: Skoro już o tym wspomniałeś, idea struktury kontrolnej, która działa poza FIFO i równoległe współprogramy działające poza kolejką zadań, ma tam wiele wspólnego.
Mike Dunlavey

2
Zastanawiam się, jak bliskie jest wykonanie różnicowe podejściu używanemu przez bibliotekę reag.js.
Brian

2
@Brian: Po przejrzeniu informacji, respond.js używa funkcji porównywania do wysyłania przyrostowych aktualizacji do przeglądarki. Nie mogę powiedzieć, czy funkcja diff jest tak samo zdolna jak wykonanie różnicowe. Podobnie jak może obsługiwać dowolne zmiany i twierdzi, że upraszcza wiązanie. Nie wiem, czy zrobiło to w takim samym stopniu. W każdym razie myślę, że jest na dobrej drodze. Kilka filmów tutaj.
Mike Dunlavey

2
@MikeDunlavey, piszę moje narzędzia za pomocą kombinacji OpenGL / IMGUI i programowania reaktywnego w warstwach Model, Model-View i View. Już nigdy nie wrócę do starego stylu. Dzięki za linki do Twoich filmów.
Cthutu,

13

Wykonywanie różnicowe to strategia zmiany przepływu kodu na podstawie zdarzeń zewnętrznych. Odbywa się to zwykle poprzez manipulowanie jakąś strukturą danych w celu zapisania zmian. Jest to najczęściej używane w graficznych interfejsach użytkownika, ale jest również używane do takich rzeczy, jak serializacja, gdzie scalasz zmiany do istniejącego „stanu”.

Podstawowy przepływ jest następujący:

Start loop:
for each element in the datastructure: 
    if element has changed from oldDatastructure:
        copy element from datastructure to oldDatastructure
        execute corresponding subroutine (display the new button in your GUI, for example)
End loop:
Allow the states of the datastructure to change (such as having the user do some input in the GUI)

Zalet tego jest kilka. Po pierwsze, jest to oddzielenie wykonywania twoich zmian od faktycznej manipulacji danymi pomocniczymi. Co jest dobre dla wielu procesorów. Po drugie, zapewnia niską przepustowość metody komunikowania zmian w programie.


12

Pomyśl, jak działa monitor:

Jest aktualizowany z częstotliwością 60 Hz - 60 razy na sekundę. Migotanie migotanie migotanie 60 razy, ale twoje oczy są powolne i naprawdę nie potrafią powiedzieć. Monitor pokazuje wszystko, co jest w buforze wyjściowym; po prostu przeciąga te dane co 1/60 sekundy bez względu na to, co robisz.

Dlaczego miałbyś chcieć, aby Twój program aktualizował cały bufor 60 razy na sekundę, jeśli obraz nie powinien zmieniać się tak często? Co jeśli zmienisz tylko jeden piksel obrazu, czy powinieneś przepisać cały bufor?


To jest abstrakcja podstawowej idei: chcesz zmienić bufor wyjściowy w oparciu o informacje, które chcesz wyświetlić na ekranie. Chcesz zaoszczędzić jak najwięcej czasu procesora i czasu zapisu w buforze, więc nie edytujesz części bufora, które nie muszą być zmieniane przy następnym ściągnięciu ekranu.

Monitor jest oddzielony od komputera i logiki (programów). Odczytuje z bufora wyjściowego z jakąkolwiek szybkością uaktualniania ekranu. Chcemy, aby nasz komputer przestał niepotrzebnie synchronizować i przerysowywać. Możemy rozwiązać ten problem, zmieniając sposób pracy z buforem, co można zrobić na wiele sposobów. Jego technika implementuje kolejkę FIFO, która jest opóźniona - przechowuje to, co właśnie wysłaliśmy do bufora. Opóźniona kolejka FIFO nie przechowuje danych pikseli, zawiera "prymitywy kształtu" (które mogą być pikselami w twojej aplikacji, ale mogą to być również linie, prostokąty, rzeczy łatwe do narysowania, ponieważ są tylko kształtami, bez zbędnych danych dozwolony).

Więc chcesz narysować / wymazać rzeczy z ekranu? Nie ma problemu. Na podstawie zawartości kolejki FIFO wiem, jak w danej chwili wygląda monitor. Porównuję moje pożądane dane wyjściowe (aby usunąć lub narysować nowe prymitywy) z kolejką FIFO i zmieniam tylko wartości, które wymagają zmiany / aktualizacji. To jest krok, który nadaje mu nazwę Ocena różnicowa.

Doceniam to na dwa różne sposoby :

Pierwszy: Mike Dunlavey używa rozszerzenia instrukcji warunkowej. Kolejka FIFO zawiera wiele informacji („poprzedni stan” lub bieżące rzeczy na monitorze lub urządzeniu odpytywającym na podstawie czasu). Wszystko, co musisz do tego dodać, to stan, który ma się pojawić na ekranie jako następny.

Bit warunkowy jest dodawany do każdego gniazda, które może pomieścić prymityw w kolejce FIFO.

0 means erase
1 means draw

Mamy jednak poprzedni stan:

Was 0, now 0: don't do anything;
Was 0, now 1: add it to the buffer (draw it);
Was 1, now 1: don't do anything;
Was 1, now 0: erase it from the buffer (erase it from the screen);

Jest to eleganckie, ponieważ kiedy aktualizujesz coś, tak naprawdę musisz tylko wiedzieć, jakie prymitywy chcesz narysować na ekranie - to porównanie dowie się, czy powinno usunąć prymityw, czy dodać / zachować do / w buforze.

Po drugie: to tylko jeden przykład i myślę, że to, do czego naprawdę zmierza Mike, powinno mieć fundamentalne znaczenie w projektowaniu dla wszystkich projektów: Zmniejsz (obliczeniową) złożoność projektu, pisząc najbardziej intensywne obliczeniowo operacje jako pożywkę komputerową lub tak blisko, jak tylko możesz. Szanuj naturalne wyczucie czasu urządzeń.

Metoda przerysowania polegająca na narysowaniu całego ekranu jest niezwykle kosztowna, a istnieją inne aplikacje, w których ten wgląd jest niezwykle cenny.

Nigdy nie „przesuwamy” obiektów po ekranie. „Przenoszenie” to kosztowna operacja, jeśli zamierzamy naśladować fizyczną czynność „poruszania się”, gdy projektujemy kod dla czegoś takiego jak monitor komputera. Zamiast tego obiekty po prostu migają wraz z monitorem. Za każdym razem, gdy obiekt się porusza, jest to nowy zestaw prymitywów, a stary zestaw prymitywów znika.

Za każdym razem, gdy monitor pobiera dane z bufora, mamy wpisy, które wyglądają jak

Draw bit    primitive_description
0           Rect(0,0,5,5);
1           Circ(0,0,2);
1           Line(0,1,2,5);

Obiekt nigdy nie wchodzi w interakcję z ekranem (ani urządzeniem odpytywającym wrażliwym na czas). Możemy sobie z tym poradzić bardziej inteligentnie niż obiekt, który zachłannie prosi o aktualizację całego ekranu, aby pokazać zmianę specyficzną tylko dla siebie.

Powiedzmy, że mamy listę wszystkich możliwych graficznych prymitywów, które nasz program jest w stanie wygenerować i że przywiązujemy każdy prymityw do zestawu instrukcji warunkowych

if (iWantGreenCircle && iWantBigCircle && iWantOutlineOnMyCircle) ...

Oczywiście jest to abstrakcja i tak naprawdę zestaw warunków, które reprezentują włączoną / wyłączoną konkretną istotę prymitywną, mógłby być duży (być może setki flag, z których wszystkie muszą mieć wartość true).

Jeśli uruchomimy program, możemy rysować na ekranie z zasadniczo taką samą szybkością, z jaką możemy ocenić wszystkie te warunki. (Najgorszy przypadek: ile czasu zajmuje ocena największego zestawu instrukcji warunkowych).

Teraz, dla dowolnego stanu w programie, możemy po prostu ocenić wszystkie warunki warunkowe i błyskawicznie wyprowadzić na ekran ! (Znamy nasze prymitywy kształtu i zależne od nich instrukcje if).

Przypominałoby to kupowanie intensywnej graficznie gry. Tylko zamiast instalować ją na dysku twardym i przepuszczać przez procesor, kupujesz zupełnie nową płytę, która zawiera całość gry i pobiera jako dane wejściowe: mysz, klawiaturę i jako wyjście: monitor. Niesamowicie skondensowana ocena warunkowa (ponieważ najbardziej podstawową formą warunku są bramki logiczne na płytkach drukowanych). Byłoby to naturalnie bardzo responsywne, ale nie oferuje prawie żadnego wsparcia w naprawianiu błędów, ponieważ cały projekt płytki zmienia się po wprowadzeniu drobnej zmiany projektowej (ponieważ "projekt" jest tak bardzo odległy od natury płytki drukowanej ). Kosztem elastyczności i przejrzystości w sposobie, w jaki przedstawiamy dane wewnętrznie, uzyskaliśmy znaczną „responsywność”, ponieważ nie zajmujemy się już „myśleniem” w komputerze; to wszystko jest sprawiedliwe dla płytki drukowanej na podstawie wejść.

Lekcja, jak rozumiem, polega na podzieleniu pracy w taki sposób, aby dać każdej części systemu (niekoniecznie tylko komputerowi i monitorowi) coś, co może zrobić dobrze. „Myślenie komputerowe” można przeprowadzić w kategoriach pojęć takich jak przedmioty ... Mózg komputera chętnie spróbuje to wszystko przemyśleć, ale możesz bardzo uprościć zadanie, jeśli będziesz w stanie pozwolić komputerowi myśleć warunki data_update i conditional_evals. Nasze ludzkie abstrakcje pojęć w kod są idealistyczne, aw przypadku programów wewnętrznych metody rysowania są nieco zbyt idealistyczne. Kiedy wszystko, czego chcesz, to wynik (tablica pikseli z prawidłowymi wartościami kolorów) i masz maszynę, która to potrafi łatwością wypluwaj tak dużą tablicę co 1/60 sekundy, spróbuj wyeliminować jak najwięcej kwiecistego myślenia z mózgu komputera, abyś mógł skupić się na tym, czego naprawdę chcesz: synchronizować aktualizacje graficzne z (szybkimi) danymi wejściowymi i naturalne zachowanie monitora.

Jak to odnosi się do innych aplikacji? Chciałbym usłyszeć inne przykłady, ale jestem pewien, że jest ich wiele. Myślę, że wszystko, co zapewnia "okno" w czasie rzeczywistym na stan twoich informacji (stan zmienny lub coś w rodzaju bazy danych ... monitor jest tylko oknem do twojego bufora wyświetlania) może skorzystać z tych spostrzeżeń.


2
++ Doceniam twoje podejście do tego. Dla mnie początkowo była to próba wyświetlania opisów programowych na wolnych urządzeniach (na przykład zdalne terminale tekstowe o prędkości 9600 bodów), gdzie w zasadzie wykonywałby automatyczne porównywanie i przesyłanie minimalnych aktualizacji. Potem nalegano, aby po prostu zakodować to brutalną siłą. Odpowiedź: ponieważ jeśli powierzchnia kodu wygląda tak, jakby była zwykłą farbą , jest krótszy, prawie bezbłędny, a zatem jest wykonywany w ułamku czasu programowania. (To właśnie myślę o korzyściach z DSL.)
Mike Dunlavey

... Uwolniony wysiłek rozwojowy można następnie zainwestować w bardziej wyrafinowane i dynamiczne wyświetlacze, które użytkownicy uznają za responsywne i przyjemne. Więc dostajesz więcej interfejsu użytkownika za pieniądze programisty.
Mike Dunlavey

... Przykład: ta aplikacja sprzed około 10 lat: pharsight.com/products/prod_pts_using_dme.php
Mike Dunlavey

1
To sprawiło, że zrozumiałem ... kiedy mówiłeś o grach komputerowych. Właściwie wiele gier jest napisanych tak, jak interfejs użytkownika Mike'a. Lista aktualizacji, która jest przeglądana z każdą ramką.
Prof. Falken,

Pozornie powiązany przykład z tym, co powiedziałeś, dotyczy wykrywania, czy klawisz / przycisk jest przytrzymywany, czy właśnie został zwolniony. Łatwo jest wiedzieć, czy przycisk jest wciśnięty, czy nie. To prawda / fałsz z Twojego interfejsu API niskiego poziomu. Aby wiedzieć, czy klawisz jest przytrzymywany, musisz wiedzieć, w jakim stanie był poprzednio. Jeśli ma wartość od 0 do 1, to po prostu został naciśnięty. jeśli jest to 1 -> 1, to jest przytrzymywane, jeśli wynosi 1 -> 0, to właśnie zwolniłeś.
Joshua Hedges

3

Uważam tę koncepcję za bardzo podobną do maszyn stanowych klasycznej elektroniki cyfrowej. Szczególnie te, które pamiętają swoje poprzednie wyjście.

Maszyna, której następne wyjście zależy od aktualnego wejścia i poprzedniego wyjścia zgodnie z (TUTAJ TWÓJ KOD). To bieżące wejście to nic innego jak poprzednie wyjście + (UŻYTKOWNIK, INTERAKCJA TUTAJ).

Wypełnij powierzchnię takimi maszynami, a będzie ona interaktywna dla użytkownika i jednocześnie reprezentować warstwę zmiennych danych. Ale na tym etapie nadal będzie głupi, odzwierciedlając tylko interakcję użytkownika z podstawowymi danymi.

Następnie połącz maszyny na swojej powierzchni, pozwól im dzielić się notatkami zgodnie z (TWÓJ KOD TUTAJ), a teraz sprawimy, że będzie inteligentny. Stanie się interaktywnym systemem komputerowym.

Musisz więc po prostu podać swoją logikę w dwóch miejscach w powyższym modelu; o resztę zadba sam projekt maszyny. To jest dobre w tym.


1
Wydaje mi się, że miałem na myśli model sprzętu, kiedy to przyszło mi do głowy.
Mike Dunlavey,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.