Permalänk
Datavetare

Progblemet #4

Detta problem kommer förhoppningsvis vara lite enklare att begripa sig på jämfört med #3. Detta progblemet handlar om att visa hur olika språk hanterar polymorfism. Polymorfism är något de flesta kanske associerar med objektorientering, arv och liknande, men det är egentligen mycket mer generellt än så även om detta exempel inte visar detta.

Skriv ett program som på något sätt definierar ett interface/protokoll för ett djur. Vi har ett väldigt enkelt djur som bara har en metod make-sound, make_sound eller makeSound (beroende på vad som är standard i resp. språk). Räcker nog med två olika djur för att man ska se principen, hund och katt.

Katten/hunden har ett namn som ska sättas när man skapar djuret och namnet ska finnas med när djuret gör sitt ljud. Hundar skäller och katter jamar, så make-sound ska skriva ut ungefär detta för en hund

namn : voff

och detta för en katt

namn : mjao

Om det är relevant / möjligt i språket, gör även en demonstration i hur man verifierar att det värde man just nu håller i implementerar interfacet / protokollet. I vissa statiskt typade språk kanske det alltid leder till ett kompikeringsfel att anropa en metod på en typ som inte har metoden (t.ex. Haskell), då kan man bara skriva en kommentar att så är fallet.

För att demonstrera att detta inte kräver ett "objektorienterat" språk kommer här två möjliga lösningar i Clojure som inte är OO på det sätt som t.ex. Java, C# är.

Med hjälp av "defprotocol" och "deftype"

;; Detta definierar ett Java-interface "under huven" med namnet ;; "Animal" och en metod med namnet "make-sound" (defprotocol Animal (make-sound [this])) ;; Låt typen "Dog" implementerar interfacet "Animal" (deftype Dog[name] Animal (make-sound [this] (println name ": voff"))) ;; Låt typen "Cat" implementerar interfacet "Animal" (deftype Cat[name] Animal (make-sound [this] (println name ": mjao"))) ;; Skapa lite hundar och katter och säg åt dem att låta! (doseq [animal [(Dog. "Killer") (Cat. "Tom") (Dog. "Lufsen") (Dog. "Lady")]] (make-sound animal)) ;; Clojure är ett starkt och dynamiskt typat språk, man kan därför ;; stoppa in saker av vilken typ som helst i listor. Vill man veta om ;; ett visst objekt implementerar ett visst protokoll så kan man göra ;; så här. (doseq [maybe-animal ["not animal" (Cat. "Bill")]] (if (satisfies? Animal maybe-animal) (make-sound maybe-animal)))

Dold text

Och här är ett annat sätt att lösa samma problem i Clojure med s.k. "multimethods" och explicita arvshierarkier. Själv skulle jag nog fördra att lösa problemet på detta sätt i Clojure.

(defmulti make-sound :type) (defmethod make-sound ::dog [this] (println (:name this) ": voff")) (defmethod make-sound ::cat [this] (println (:name this) ": mjao")) ;; Här är "klasserna" representerad rakt upp och ner med hjälp av hash-maps ;; Namnet är sparat under nyckeln ":name" och typen under nyckeln ":type" (defn new-dog [name] {:type ::dog :name name}) (defn new-cat [name] {:type ::cat :name name}) (doseq [animal [(new-dog "Killer") (new-cat "Tom") (new-dog "Lufsen") (new-dog "Lady")]] (make-sound animal)) ;; Går att ha en metod som tar hand om alla typer man inte känner ;; igen. (defmethod make-sound :default [this] (println "inte en katt eller hund")) (doseq [maybe-animal ["not animal" (new-cat "Bill")]] (make-sound maybe-animal)) ;; Det går också att skapa relatationer mellan atomer (det som börjar ;; med kolon i Lisp/Clojure är en "atom" som är en form av konstant ;; utan värde). Man kan sedan frågan om 'x' är relaterad till 'y' på ;; följande sätt. (derive ::dog ::animal) ;; ::dog är ett barn till ::animal (derive ::cat ::animal) ;; ::cat är ett bart till ::animal (doseq [maybe-animal ["not animal" (new-cat "Bill")]] (if (isa? (:type maybe-animal) ::animal) (make-sound maybe-animal)))

Dold text

Multimethods är mycket mer generella än detta, man kan även använda värdet på argument som del i valet över vilken metod som anropas, något som även funktionella språk brukar stödja via s.k. pattern-matching. Kanske något för ett framtida "progblemet".

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

Standard c++ implementation

#include <string> #include <iostream> using namespace std; class Animal{ public : virtual const void Make_Sound(){ cout << name <<":"; } protected: Animal(const string name_in) : name(name_in){} private : string name; }; class Dog : public Animal{ public: Dog(const string name_in) : Animal(name_in){} const void Make_Sound(){ Animal::Make_Sound(); cout << "voff" << endl; } }; class Cat : public Animal{ public: Cat(const string name_in) : Animal(name_in){} const void Make_Sound(){ Animal::Make_Sound(); cout << "mjao" << endl; } }; int main(){ Animal * a = new Dog("cool dog"); a->Make_Sound(); delete a; a = new Cat("cool cat"); a->Make_Sound(); return 0; }

Permalänk
Medlem
Skrivet av hawy:

Standard c++ implementation

#include <string> #include <iostream> using namespace std; class Animal{ public : virtual const void Make_Sound(){ cout << name <<":"; } protected: Animal(const string name_in) : name(name_in){} private : string name; }; class Dog : public Animal{ public: Dog(const string name_in) : Animal(name_in){} const void Make_Sound(){ Animal::Make_Sound(); cout << "voff" << endl; } }; class Cat : public Animal{ public: Cat(const string name_in) : Animal(name_in){} const void Make_Sound(){ Animal::Make_Sound(); cout << "mjao" << endl; } }; int main(){ Animal * a = new Dog("cool dog"); a->Make_Sound(); delete a; a = new Cat("cool cat"); a->Make_Sound(); return 0; }

Nu var det 5 år sedan jag läste eller skrev C++ men har du inte glömt sätta name = name_in ??
Sen är det väl inte så snyggt heller att skriva ut djurets läte i två omgångar istället för en.

Visa signatur

I'm Winston Wolfe. I solve problems.

Permalänk
Medlem
Skrivet av matti4s:

Nu var det 5 år sedan jag läste eller skrev C++ men har du inte glömt sätta name = name_in ??
Sen är det väl inte så snyggt heller att skriva ut djurets läte i två omgångar istället för en.

Nej, det har jag inte glömt

Animal(const string name_in) : name(name_in)

Om man tycker det är fult eller inte är en smaksak, man kan ju även sätta

virtual const void Make_Sound() =0

om man så önskar och flytta upp koden på 2 ställen istället för ett om man av någon kostig anledning tycker att copy paste är snyggt.

Permalänk
Medlem

Då kör vi

Ett komplett exempel i Scala (inga överraskningar där!). Jag valde att krydda exemplet lite för att visa lite smågrejer jag gillar med språket. Koden är kommenterad på ett ganska fult och klumpigt vis då jag saknar sinne för utformning.

edit: skrev om kommentarerna.

/* Traits är Scalas variant av interfaces från exempelvis C# och Java. Precis som med interfaces kan man kombinera två eller flera traits. Den stora skillnaden är att man dessutom kan definiera konkreta medlemmar, se makeSound. */ trait Animal { def name: String def sound: String def makeSound = "%s makes a sound: '%s'" format (name, sound) } /* I Scala kombineras klass och konstruktor (om sådan önskas) i samma definition. Klasser kan definieras överallt; inuti andra klasser, objekt, metoder, block.. En effekt av detta är att man inte lika ofta drar sig för att abstrahera ner logik och data i en ny klass, vilket i förlängningen leder till snyggare kod. */ class Cat(val name: String) extends Animal { def sound = "meow" } /* Den abstrakta medlemmen 'name' från Animal görs i bägge klasserna Cat och Dog konkret genom konstruktordefinitionen. Nyckelordet 'val' anger att värdet är "immutable", det vill säga oföränderligt. Det finns även en mutable variant 'var' som tillåter att värdet ändras vid senare tillfälle, men det är mindre använt i Scala. Det vanliga är att man skapar ett nytt objekt med de egenskaper man önskar istället för att ändra ett som redan finns. */ class Dog(val name: String) extends Animal { def sound = "bark" } /* Till skillnad från C# och Java finns inga statiska metoder (eller klasser) i Scala, istället har man objekt. Ett objekt defineras på samma sätt som en klass (fast utan konstruktor) och instantieras automatiskt en gång när det efterfrågas. Då ett objekt inte är statiskt kan man skicka det som argument till metoder och jobba med det på ett helt annat sätt än med statics. */ object Farm extends App { /* I listan nedan listar kompilatorn automatiskt ut att listans innehållstyp är av typ 'Animal'. Denna process kallas type-inference. */ val animals = List( new Cat("Larry") , new Dog("Barry") , new Animal { val name = "Harry" val sound = "oink" } ) /* Nyckelordet 'def' definierar en ny metod. Returtypen är i detta fall Unit som kan liknas vid void i andra programmeringsspråk. */ def printSound(animal: Animal) { println(animal.makeSound) } /* Raden nedan skulle i C# skrivas: animals.ForEach(PrintSound) Det punktlösa skrivsättet kallas för infix notation och kan se ganska läckert ut efter att man vant sig vid det. I detta exempel alltså varje element i listan 'animals' till metoden printSound. */ animals foreach printSound }

Dold text
Visa signatur

Kom-pa-TI-bilitet

Permalänk
Datavetare

Go

Ett annat språk som trots att det saknar klasser och arv ändå stödjer alla de designmönster man kan åstadkomma med dessa är Go.

Faktum är att jag tycker det är lite lustigt att OO språken till mångt och dras med flera av de nackdelar som OO programmering tenderar att medföra (stela klasshierarkier som kan vara svårt/omöjligt att bygga ut för 3:e part), medan språk som inte ens är "OO" löser samma problem utan att ha nackdelarna.

Skulle säga att både Go och Clojure löst de arkitekturmässiga bitarna med OO och polymorfism långt mycket bättre än språk som Java, C++ och C#.

Go har ju även fördelen att typiskt vara snabbare än både C# och Java program, det är väldigt nära C och C++.

package main import "fmt" // Så låt oss börja med att skapa ett interface för ett djur. Det // specifierar vilka metoder en typ måste ha för att kunne användas som // ett djur. type Animal interface { // låt oss i stället retunera ljudet som en sträng makeSound() string } // Vår hund har även i detta fall bara ett namn type Dog struct { name string } // Samma sak gäller katten, bara ett namn type Cat struct { name string } // Detta lägger till metoden 'makeSound' till typen 'Dog' och typen // uppfyller därmed interfacet Animal func (dog Dog) makeSound() string { return dog.name + " : voff" } // Samma sak för 'Cat'. func (cat Cat) makeSound() string { return cat.name + " : mjao" } func main() { animals := []Animal{Dog{"Killer"},Cat{"Tom"},Dog{"Lufsen"},Dog{"Lady"}} for _, animal := range(animals) { fmt.Println(animal.makeSound()) } // Detta skulle leda till kompileringsfel då typen "string" inte // uppfyller interfacet 'Animal' // // maybe_animals = []Animal{Dog{"Lassie"},"Not an animal"} // // Det är däremot full möjligt för oss att lägga göra så att // 'string' uppfyller detta genom att lägga till metoden // // func (a_string string) makeSound() string { // return a_string + " can now be used as an animal!" // } // // Exemplet skulle nu kompilera }

Dold text

Edit: Fixade missen i "makeSound" för typen "string" som Teknocide noterade

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

Ett annat språk som trots att det saknar klasser och arv ändå stödjer alla de designmönster man kan åstadkomma med dessa är Go.

Faktum är att jag tycker det är lite lustigt att OO språken till mångt och dras med flera av de nackdelar som OO programmering tenderar att medföra (stela klasshierarkier som kan vara svårt/omöjligt att bygga ut för 3:e part), medan språk som inte ens är "OO" löser samma problem utan att ha nackdelarna.

Skulle säga att både Go och Clojure löst de arkitekturmässiga bitarna med OO och polymorfism långt mycket bättre än språk som Java, C++ och C#.

Go har ju även fördelen att typiskt vara snabbare än både C# och Java program, det är väldigt nära C och C++.

package main import "fmt" // Så låt oss börja med att skapa ett interface för ett djur. Det // specifierar vilka metoder en typ måste ha för att kunne användas som // ett djur. type Animal interface { // låt oss i stället retunera ljudet som en sträng makeSound() string } // Vår hund har även i detta fall bara ett namn type Dog struct { name string } // Samma sak gäller katten, bara ett namn type Cat struct { name string } // Detta lägger till metoden 'makeSound' till typen 'Dog' och typen // uppfyller därmed interfacet Animal func (dog Dog) makeSound() string { return dog.name + " : voff" } // Samma sak för 'Cat'. func (cat Cat) makeSound() string { return cat.name + " : mjao" } func main() { animals := []Animal{Dog{"Killer"},Cat{"Tom"},Dog{"Lufsen"},Dog{"Lady"}} for _, animal := range(animals) { fmt.Println(animal.makeSound()) } // Detta skulle leda till kompileringsfel då typen "string" inte // uppfyller interfacet 'Animal' // // maybe_animals = []Animal{Dog{"Lassie"},"Not an animal"} // // Det är däremot full möjligt för oss att lägga göra så att // 'string' uppfyller detta genom att lägga till metoden // // func (a_string string) string { // return a_string + " can now be used as an animal!" // } // // Exemplet skulle nu kompilera }

Dold text

Den sista utkommenterade func-en saknar namn.. tror jag

Håller med om att det verkar smidigt när ett interface fungerar som mall snarare än typbegränsning. Finns det mekanismer för inheritance mellan typer?

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Datavetare
Skrivet av Teknocide:

Den sista utkommenterade func-en saknar namn.. tror jag

Håller med om att det verkar smidigt när ett interface fungerar som mall snarare än typbegränsning. Finns det mekanismer för inheritance mellan typer?

Man kan åstadkomma "arv" genom att använda typ A i typ B

type A struct { a int b int } type B struct { A // Här inkluderar jag typen 'A' anonymt i typen 'B'. c int } func main() { b_inst := B{A{1,2},3} fmt.Println("värdet på fält 'a' är " + b_inst.a) }

Det är i exemplet ovan nu möjligt att använda instanser av typen B på ställen där A förväntas. Detta går även implementera "multipelarv" på detta sätt

type A struct { a int b int } type B struct { a float32 c float32 } type C struct { A B }

Instanser C kan nu användas på ställen där typen A, B eller C förväntas. Den uppmärksamme inser nu att medlemmen 'a' förekommer två gånger. Det är inte ett problem i de lägen man använder instanser av C som A eller B, men hur löser man det när man använder dem som C?

func main() { c_inst := C{A{1,2},B{3.14,2.71}} // detta blir kompileringsfel då det inte går // att avgöra vilket 'a' man refererar till fmt.Println("värdet på fält 'a' är " + c_inst.a) // man löser detta genom att explicit ange vilket 'a' man menar fmt.Println("värdet på fält 'a' från typ B är " + c_inst.B.a) }

Ett annat sätt är att faktiskt namnge sin medlem av typen A och B i typen C. Men det blir då inte ett "arv" i bemärkelsen: C kan automatiskt användas som A eller B, utan man har nu en 'has-a' relation i stället för en 'is-a' relation.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer