Hlavní navigace

Squeak: návrat do budoucnosti (11)

20. 4. 2004
Doba čtení: 8 minut

Sdílet

Dnes se vrátíme opět k výkladu jazyka Smalltalk. Pro zopakování si vytvoříme malý prográmek. Bude se jednat o jednoduchý simulátor Petriho sítí. Pokud vás zajímá, jak vypadá smalltalkovský program a v čem se tento jazyk liší od vašeho oblíbeného, pak jste na správné adrese.

Předně bych měl upozornit, že náš simulátor je z prostorových důvodu maximálně zjednodušen. Neimplementuje např. kapacity a nekontroluje úplně vše, co by měl. Jeho smyslem je posloužit pouze jako podpora pro výklad jazyka.

Nejdříve si vytvoříme novou systémovou kategorii jménem PetriNet, do které budeme vytvářet naše třídy. Aby nedocházelo ke kolizím s názvy již existujících tříd, budeme pro ně používat prefix PN.

Pro vytvoření objektu ve Smalltalku se implicitně používá zprávanew zaslaná třídě. Protože budeme chtít, aby se každý objekt při vytvoření sám inicializovat, konstruktor new si přetížíme tak, aby novému objektu poslal zprávu initialize. Abychom to nemuseli dělat zvlášť pro každou třídu, vytvoříme si jednu základní, od níž budou naše ostatní dědit.

Object subclass: #PNBaseClass
        instanceVariableNames: ''
        classVariableNames: ''
        poolDictionaries: ''
        category: 'PetriNet' 

Do definice její metatřídy tedy vložíme přetížený konstruktor new.

new
        ^ super basicNew initialize. 

Nejdříve jsme zavolali základní konstruktor basicNew, vytvořený objekt jsme inicializovali a vrátili jej jako výsledek. Nedoporučuje se použít místo zprávy basicNew prosté zavolání konstruktoru předka, tedy super new initialize, protože od budoucí verze Squeaku (3.7) volá standardní implementace metody new zprávu initialize sama. Inicializace by tedy zbytečně proběhla dvakrát.

Metodu initialize necháme prázdnou.

initialize
        "implicitne vraci self" 

Petriho síť se skládá ze tří základních elementů: míst, přechodů a propojení. Pro každý tento element si vytvoříme vlastní třídu. Začneme u přechodů, protože na jejich bedrech tkví nejvíce práce.

Každý přechod má množinu vstupních a výstupních propojení. Protože přechody se v Petriho síti provádí synchronně všechny najednou, musíme si každé provedení přechodu napřed odsimulovat a výsledek uložit. Využijeme pro to instanční proměnnou enabled (proveditelnost přechodu).

PNBaseClass subclass: #PNTransition
        instanceVariableNames: 'input output enabled '
        classVariableNames: ''
        poolDictionaries: ''
        category: 'PetriNet' 

Nejdříve upravíme inicializaci přechodu, tzn. že přetížíme zprávu initialize tak, aby vytvořila vstupní a výstupní množinu propojení. Pro množinu máme k dispozici již existující třídu Set. Neměli bychom zapomenout vždy zavolat inicializaci předka.

initialize

        super initialize.

        input := Set new.
        output := Set new. 

Nyní naimplementujeme zprávy pro přidávání spojů na vstup a výstup. Jen připomínám, že skutečný typ parametru aLink je nám naprosto ukradený, a třídu, jejíž instance budou jako tento parametr předávány, ještě nemáme ani nadeklarovanou a můžeme jen tušit, jak asi bude vypadat.

addInput: aLink
        input add: aLink.

addOutput: aLink
        output add: aLink. 

Přechodu doplníme zprávy pro testování. Metoda isTransition nám řekne, zda se jedná o přechod.

isTransiton
        ^ true. 

Nejjednodušší třídou bude třída pro propojení. Ta bude obsahovat jen informace, odkud a kam spoj vede.

PNBaseClass subclass: #PNLink
        instanceVariableNames: 'from to '
        classVariableNames: ''
        poolDictionaries: ''
        category: 'PetriNet' 

Pro třídu PNLink nemusíme provádět žádnou speciální inicializaci. Vytvoříme přistupující metody k instančním proměnným from a to.

from
        ^ from.

from: anElement
        from := anElement.

to
        ^ to.

to: anElement
        to := anElement. 

Tuto operaci nám usnadní příkaz create inst var accessors v Browseru. Nyní jsme již s třídou PNLink takřka hotovi. Stačí už jen vytvořit v její metatřídě vhodný konstruktor, aby se s ní dalo intuitivně pracovat.

from: start to: end

        | link |

        (start = end) ifTrue: [ self notify: 'Nezle propojit stejny prvek'. ^ nil. ].

        link := self new.
        link from: start.
        link to: end.
        start isTransition ifTrue: [ start addOutput: link. ].
        end isTransition ifTrue: [ end addInput: link. ].
        ^ link. 

Pro místa Petriho sítě si vytvoříme třídu PNPlace, která bude udržovat počet značek.

PNBaseClass subclass: #PNPlace
        instanceVariableNames: 'marks '
        classVariableNames: ''
        poolDictionaries: ''
        category: 'PetriNet' 

Nadefinujeme si přistupující metody a metody pro přidávání a odebírání značek. Počet značek při inicializaci vynulujeme.

marks
        ^ marks.

marks: count
        marks := count.

addMark
        marks := marks + 1.

removeMark
        marks > 0 ifTrue: [ marks := marks - 1 ]

initialize
        super initialize.
        self marks: 0. 

Opět uvedeme definici testovacích zpráv.

isTransition
        ^false. 

Vytvoříme si metody, které prověří, zda je místo připraveno k provedení přechodu.

ready
        ^ (marks > 0).

readyAccept
        ^ true. 

Tím jsme s třídou PNPlace rovněž skončili. Nyní ještě doplníme logiku pro provádění přechodů do třídy PNTransition. Napřed vytvoříme metodu pro zjištění, zda je možné přechod provést.

canPerformTransition

        input do: [:link | link from ready ifFalse: [ ^false. ] ].
        output do: [:link | link to readyAccept ifFalse: [ ^false. ] ].
        ^ true. 

Pomocí metody do: projdeme všechna místa Petriho sítě s přechodem propojená v množinách vstupů a výstupů a prověříme jejich připravenost. Následně si připravíme metodu, která přechod skutečně provede v závislosti na hodnotě instanční proměnné enabled.

performTransition

        enabled ifTrue: [
        input do: [:link | link from removeMark].
        output do: [:link | link to addMark]
] 

Ještě přidáme metody, které instanční proměnnou enabled budou spravovat. Metoda reset ji zruší, metoda simulateTransition ji pro daný přechod nastaví podle toho, zda může být proveden.

reset
        enabled := nil.

simulateTransiton
        enabled := self canPerformTransition 

Nyní nadešel čas vytvořit si třídu pro vlastní Petriho síť. Pojmenujeme si ji PetriNet a bude obsahovat místa, přechody a propojení.

PNBaseClass subclass: #PetriNet
        instanceVariableNames: 'places transitions links '
        classVariableNames: ''
        poolDictionaries: ''
        category: 'PetriNet' 

Její instance budou inicializovány tak, že přechody a propojení budou uloženy ve slovnících a propojení v množině.

initialize
        super initialize.
        places := Dictionary new.
        transitions := Dictionary new.
        links := Set new. 

Slovník (Dictionary) je tabulka asociací klíč/hodnota.

Jádro simulace tvoří jednoduchá metoda step. Nejdříve resetujeme síť, tzn. že všem přechodům zrušíme obsah proměnnéenabled. Poté každý přechod nasimulujeme a na závěr přechody skutečně provedeme. Díky tomu je přechod v celé síti proveden synchronně.

step
        transitions do: [:transition | transition reset ].
        transitions do: [:transition | transition simulateTransition. ].
        transitions do: [:transition | transition performTransition. ]. 

Abychom naši Petriho síť mohli používat, přidáme si do ní metody pro vkládání elementů. Budeme je identifikovat pomocí symbolů. Totéž uděláme i pro kolekce (např. pole) elementů Petriho sítě.

addPlace: aSymbol
        places at: aSymbol put: PNPlace new.

addPlaces: aCollection
        aCollection do: [:place | self addPlace: place ].

addTransition: aSymbol
        transitions at: aSymbol put: PNTransition new.

addTransitions: aCollection
        aCollection do: [:transition | self addTransition: transition ]. 

Přidání propojení bude o něco komplikovanější. Propojení může být vytvořeno z místa do přechodu a obráceně. Již vytvořená místa a přechody, které propojujeme, si nejdříve musíme najít podle symbolů.

addLinkFrom: symFrom to: symTo

    | from to link |

    (transitions includesKey: symFrom)
        ifTrue: [ from := transitions at: symFrom ].
    (transitions includesKey: symTo)
        ifTrue: [ to := transitions at: symTo ].
    (places includesKey: symFrom)
        ifTrue: [ from := places at: symFrom ].
    (places includesKey: symTo)
        ifTrue: [ to := places at: symTo ].

    ((from = nil) or: [ to = nil])
        ifTrue: [ self notify: 'Nelze vytvorit propojeni'. ^ self. ].

    link := PNLink from: from to: to.

    links add: link. 

Metoda pro přidávání kolekce propojení bude také o špetku složitější, protože budeme přijímat kolekci kolekcí, jak si ukážeme v příkladu použití.

addLinks: aCollection
    aCollection do: [:link | self addLinkFrom: (link first) to: (link second ). ]. 

Ještě necháme uživateli možnost nastavit značky. Význam této metody by měl být také okamžitě patrný z příkladu.

addMarks: aCollection
    aCollection do: [:symbol | (places at: symbol) addMark. ]. 

A jsme prakticky u konce. Naši Petriho síť můžeme využít programově a nebo si s ní hrát ve Workspace za pomocí povelu print it a Inspectoru. Příklad použití výsledného simulátoru může vypadat např. takto:

| net |

net := PetriNet new.

net addPlaces: #(p1 p2 p3 p4 p5).
net addTransitions: #(c1 c2 c3).
net addLinks: #((p1 c1) (p2 c1) (p3 c1) (c1 p4) (c1 p5) (p4 c2) (c2 p1) (p5 c3) (c3 p3)).

net addMarks: #(p1 p2 p3).

net step. 

Další kroky sítě se provádí opětovným voláním metody step. Její použití je velmi intuitivní. Ke štěstí už nám chybí jen to, aby se dokázala ergonomičtěji prezentovat uživateli, a ten nemusel k jejímu zkoumání používat např. Inspector. Opravdu vřele se doporučuje přetěžovat v našich třídách metodu printOn:, která přijímá jako parametr proud a je využívána všude tam, kde je třeba získat textovou reprezentaci objektu. Např. pro třídu PNPlace by mohla vypadat takto:

printOn: aStream
    aStream nextPutAll: ('place(', self marks asString, ')'). 

Přetížení této metody pro ostatní třídy naleznete ve výsledném zdrojovém souboru.

Na tomto příkladu jsme si doufám jednoznačně ukázali hlavní rysy Smalltalku.

CS24_early

  • nikde jsme nemuseli zacházet do přílišných implementačních detailů
  • pracovali jsme s věcmi, o nichž jsme dopředu nevěděli skoro nic. Pouze to, že je v budoucnu budeme nějak implementovat
  • jen občas se nám podařilo vytvořit metodu delší než dva řádky
  • kolekce, o nichž bude následující díl, celý program výrazně zobecňují, zčitelňují a zjednodušují
  • místo symbolů bychom bez jakýchkoliv problémů mohli použít cokoliv jiného. Jakékoliv objekty čísly počínaje a morphy zdaleka nekonče. Použití literálů je ovšem nejelegantnější
  • skutečně velmi čitelný kód

Když se člověk učí Smalltalk, velmi rychle nabude dojmu, že v něm jde všechno skoro samo. V okamžiku, kdy je postaven před úkol něco skutečně naprogramovat, zjistí, že se v principu moc neliší od toho, na co byl až doposud zvyklý. Smalltalk za vás přemýšlet nebude. Na druhou stranu po vás ale nevyžaduje, abyste se věnovali něčemu více, než je funkčnost vašeho programu. Je snadné si v něm zachovat velkou míru abstrakce a nezabředávat do zoufalé snahy přeformulovat svoje myšlenky na zápis do programovacího jazyka.

Toto jsou vlastnosti, na které si člověk velmi rychle zvykne a okamžitě je začne brát jako samozřejmost. Má to jeden nepříjemný důsledek. Není cesty zpět. Můžete mi věřit, že práce s běžnými jazyky se stává takřka nesnesitelným utrpením. Je to jako brodit se po pás v bahně, když víte, že kousek vedle vede pevná lávka. Smalltalk přímo vybízí k tomu, dělat věci jednoduše a čistě. To je možná jeden z nejlepších důvodů, proč se Smalltalku alespoň na chvíli věnovat. Smalltalk si neprávem vybudoval pověst nepraktického jazyka, který je dobrý pouze k tomu, aby z něj ostatní měli co vykrádat. Přitom jeho kvality by nejvíce docenili zapřísáhlí pragmatici, kteří potřebují co nejrychleji psát kvalitní, udržovatelné a robustní aplikace.

Byl pro vás článek přínosný?