Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Scala — pierwsze kroki cz.4

Dzięki opanowaniu materiału z poprzednich części kursu. Jesteśmy już blisko, aby móc wykorzystać zdobyte już umiejętności do konkretnego zastosowania w skryptach. Do tego potrzebujemy jeszcze kilku elementów całej układanki. W tym w szczególności umiejętności czytania i zapisywania do plików. Po przeczytaniu tej części powinniśmy być już w stanie to zrobić. Pisanie programów w IDE kompilowanych do bytecodu wymagać będzie znajomości klas i obiektów, co przedstawię w następnej ostatniej już części.

Krok ósmy - operatory

Język C++ pozwala na przeładowywanie operatorów, umożliwiając pisanie klas, w których operacje na nich mogą być wykonywane naturalnie za pomocą tychże operatorów. W Javie natomiast nie można zdefiniować operatorów dlatego zastępuje się operatory metodami o nazwach takich jak add, mutliple itp. W Scali natomiast wszystko zostało zupełnie przeprojektowane. Nie istnieje podział na metody i operatory. Polega to na tym, że metody mogą mieć prawie dowolne nazwy, w tym mogą to być różne znaki uznawane za operatory. Natomiast metody, które mają tylko jeden argument można wywoływać bez użycia kropki i nawiasów:

1 + 2

Możemy równie dobrze napisać jako:

1.+(2)

Aby zrozumieć przykład prześledźmy w jaki sposób można wywoływać metody.

Definiujemy liczbę typu Long (litera L na końcu)

val liczba = 0L liczba: Long = 0

Teraz możemy zamienić ją na String, wywołując wbudowaną metodę toString

liczba.toString() res3: java.lang.String = 0

Równie dobrze możemy tę samą metodę wywołać bez nawiasów. Dotyczy to wszystkich metod bez parametrów:

liczba.toString

Więcej, możemy wywołać tę samą metodę na literałach. Kompilator gdy zorientuje się jakiego są typu wywoła metodę z odpowiedniego obiektu:

12345.toString res4: java.lang.String = 12345 "34564".toInt res5: Int = 3456

W przypadku obiektów i metod możemy również zrezygnować z użycia nawiasów i kropki, oddzielając nazwę metody od parametru spacją.

Więc 1 + 2 to wywołanie metody + na obiekcie Int reprezentowanej przez jej obiekt o wartości 1. czyli 1.+(2)

Tego efektu nie można stosować w stosunku do funkcji, musimy użyć nawiasów. Funkcje natomiast mogą zawierać wiele różnych znaków w nazwie w tym cyfry np:

def !#(a:Int) = a*10 $bang$hash: (a: Int)Int !#(30) res8: Int = 300

Zdefiniowano tutaj i użyto funkcji o nazwie !#.

Nazwy funkcji i metod mają jednak pewne ograniczenia. Nie mogą zaczynać się od cyfry i niektórych znaków jak np. #, $, &. Oczywiście nie mogą być nazwami kluczowymi, w tym pojedynczymi znakami używanymi przez składnie. Przykładowo funkcja nie może się nazywać : ani :nazwa ponieważ dwukropek wykorzystuje się przy określaniu typu zmiennej. Ale już użycie nazwy :# lub :: jest dopuszczalne ponieważ te znaki nie występują nigdy razem w składni. Często dodaje się na końcu metod znaku zapytania ? jednak musimy poprzedzić go znakiem podkreślnika:

def parzysta_?(i:Int) = i % 2 == 0 parzysta_$qmark: (i: Int)Boolean

parzysta_?(567) res13: Boolean = false

W przypadku metod umożliwiają one tworzenie bardziej intuicyjnych bibliotek i DSLi. Więcej na temat nazw metod dowiemy się przy okazji omawiania definicji klas i obiektów.

Krok dziewiąty - rozpoznawanie stylu funkcyjnego

Jak wiemy Scala nie jest językiem wymuszającym pisanie w stylu funkcyjnym. Ułatwia to przenoszenie kodu z innych języków jak np. Javy. Pomaga też początkującym programistom odnaleźć się szybko w składni Scali i nie tracić zbytnio produktywności rozgryzając nieustannie jak napisać dany kod funkcyjnie. Ma to również swoje minusy i może nie podobać się purystom językowym. Jednak Scala powstała z czysto praktycznych powodów i nie stara się utrudnić życia programiście w imię jakiś wyższych ideałów.

Jednym z elementów stylu funkcyjnego jest unikanie iterowania po dodatkowej zmiennej np.:

def drukujArg(args: Array[String]):Unit = { var i = 0 while (i < args.length) { println(args(i)) i += 1 } }

Możemy zastąpić:

def drukujArg(args: Array[String]):Unit = { for (arg <- args.length) println(args(i)) } Lub jeszcze zwięźlej:

def drukujArg(args: Array[String]):Unit = { args.foreach(println) } W tym ostatnim używamy wbudowanej w kolekcje metody foreach i uproszczonej składni anonimowej funkcji. (możemy ją rozpisać jako; (arg) => println(arg) )

Ostatnia wersja jest najkrótsza i możemy ją przetłumaczyć w prosty sposób jako: każdy argument zmiennej args drukuj w nowej linii. Taki sposób pisania na dłuższą metę jest opłacalny, ponieważ skraca długość kodu i pomaga wyłapywać błędy jakie powstają przy bardziej rozwlekłym kodzie. Nie wymaga również stosowania pośrednich zmiennych.

Powyższe funkcje mają jednak pewien minus, generują uboczny efekt w postaci wydruku danych na konsolę. Nie jest to jeszcze idealnie funkcyjny styl. Lepiej byłoby zrobić następująco:

def formatujArg(args: Array[String]) = args.mkString("\n") println(formatujArg(args))

Metoda formatujArg jest w pełni funkcyjna tzn.: nie używa zmiennych mutowalnych i nie ma efektów ubocznych. Tworzy nowy obiekt w formie Stringu, który można użyć np. do wydrukowania na ekran. Oryginalna tablica nie ulega zmianie i jest bezpieczna. (Domyślnie każdy parametr metody lub funkcji jest typu val, jeśli nie chcemy aby tak było dopisujemy na początku var.)

Powyższa metoda ułatwia również testowanie kodu, ponieważ można łatwo sprawdzić, czy logika działania jest prawidłowa:

val doDruku = formatujArg(Array("jeden", "dwa", "trzy")) assert(doDruku == "jeden\ndwa\ntrzy"

Przykład oczywiście jest banalny, ale ukazuje ideę, że kod pisany w stylu funkcyjnym jest łatwiejszy do testowania.

Warto pamiętać, że sam wygląd kodu polegający na łańcuchowym przetwarzaniu danych przez wbudowane funkcje nie jest jeszcze prawdziwym programowaniem funkcyjnym. Przykładowo podobny kod do funkcyjnego pisze się używając jQuery. Jednak mimo podobieństwa trudno nazwać go stylem funkcyjnym, ponieważ operuje się na zmiennych mutowalnych. Głównym wyróżnikiem stylu funkcyjnego jest właśnie używanie niemutowalnych danych. Gdy chcemy zmienić te dane to tworzymy i zwracamy nowy obiekt nie zmieniając starego. Obiekty powinny być zabezpieczone przez zmianą. Dlatego w stylu funkcyjnym używamy typu zmiennych val.

Krok dziesiąty - czytanie danych z pliku

Aby pisać proste skrypty najczęściej niezbędna jest praca na plikach oraz możliwość odczytu i zapisu danych. Do czytania danych z pliku używamy obiektu scala.io.Source

import scala.io.Source if(args.length > 0) { for(line <- Source.fromFile(args(0)).getLines) print(line.length + " " + line) } else Console.err.println("Błędna nazwa pliku")

Po zapisaniu kodu w pliku i uruchomieniu z argumentem będącym ścieżką do pliku:

scala nazwa_programu.scala sciezka_do_pliku

uzyskujemy wydruk zawartości pliku z ilością danych w każdej linii.

Source potrafi również czytać z sieci (po podaniu URL), strumieni oraz tablic z danymi (Array[Byte])

np:.

import scala.io.Source if(args.length > 0) { for(line <- Source.fromURL(args(0)).getLines) print(line.length + " " + line) } else Console.err.println("Brak URL")

Podajemy w tym przypadku pełny URL (z http).

Metoda fromFile zwraca Iterator[String] dający dostęp do danych. Aby zamienić go na kolekcję wykonujemy metodę:

val linie = Source.fromFile("plik").getLines.toList

Czytanie z konsoli

Do czytania używamy metody readLine("Pytanie"):

import scala.io.Source val http = readLine("Podaj HTTP: ") for(line <- Source.fromURL(http).getLines) print(line.length + " " + line)

Zapis do pliku

W standardowych bibliotekach Scali nie ma osobnego sposobu zapisu danych do pliku. Korzysta się z klas Javy. W przypadku pliku tekstowego możemy wykonać zapis następująco:

val wyjscie= new java.io.FileWriter("plik.txt") wyjscie.write("Witaj pliczku!") wyjscie.close

Jak już wspomniałem jest to przedostatnia już część kursu. Z góry zapraszam na ostatnią, w której przedstawię aspekty programowania obiektowego w Scali.  

programowanie

Komentarze

0 nowych
  #1 22.09.2014 00:02

Jak zwykle super!

budda86   9 #2 22.09.2014 09:33

"co przedstawię w następnej ostatniej już części"

Dlaczego ostatniej? O Scali można jeszcze dużo powiedzieć ;)

"Równie dobrze możemy tę samą metodę wywołać bez nawiasów. Dotyczy to wszystkich metod bez parametrów:

liczba.toString"

To akurat w ogóle mi się nie podoba. Po to mamy nawiasy, żeby na pierwszy rzut oka odróżniać pola od metod.

"Jednym z elementów stylu funkcyjnego jest unikanie iterowania po dodatkowej zmiennej"

A tak naprawdę to nie unikanie, tylko schowanie tej iteracji w wywołaniu jakiejś metody, tu z biblioteki standardowej ;)

"Powyższa metoda ułatwia również testowanie kodu, ponieważ można łatwo sprawdzić, czy logika działania jest prawidłowa"
"Przykład oczywiście jest banalny, ale ukazuje ideę, że kod pisany w stylu funkcyjnym jest łatwiejszy do testowania."

Nie przekonuje mnie to, że kod funkcyjny jest łatwiejszy do testowania. W przypadku kodu niefunkcyjnego też "można łatwo sprawdzić, czy logika działania jest prawidłowa" - o ile kod jest dobrze napisany.

  #3 22.09.2014 09:50

@budda86: "To akurat w ogóle mi się nie podoba. Po to mamy nawiasy, żeby na pierwszy rzut oka odróżniać pola od metod. "
Ustaw sobie kolorowanie składni zależne od kontekstu.

mikolaj_s   13 #4 22.09.2014 15:40

@budda86: "Dlaczego ostatniej? O Scali można jeszcze dużo powiedzieć ;) "

Można mówić w nieskończoność ;) Tyle, że kurs napisałem w celu zainteresowania Scalą i jako okazję do poznania jej podstaw. Jak ktoś się zainteresuje to znajdzie mnóstwo wpisów o Scali, weźmie do ręki książkę (są co najmniej dwie pozycje po polsku, choć IMO najlepsza to angielska Oderskiego). Nie będę przynudzał na blogu, który czyta wielu ludzi o różnorodnych zainteresowaniach ;) Myślę, że na bardziej szczegółowe informacje jest miejsce na specjalistycznych blogach i forach.

"To akurat w ogóle mi się nie podoba. Po to mamy nawiasy, żeby na pierwszy rzut oka odróżniać pola od metod. "

Podobna zasada jest w Ruby. W Scali jest zalecenie, aby nie pomijać nawiasów gdy metoda ma efekt uboczny. Jeśli takiego nie ma to jaka jest dla Ciebie różnica czy wywołujesz funkcję czy pobierasz wartość jakiegoś pola? Z punktu widzenia użytkownika biblioteki nie interesuje Cię to zbytnio. Natomiast można wymusić, aby nie dało się napisać nawiasów w wywołaniu metody i właśnie robi się to wtedy, żeby pokazać, że metoda nie ma żadnych skutków ubocznych. W drugą stronę niestety się nie da, ale jak ktoś nie chce pisać dodatkowych nawisów to po co go zmuszać? W zespole piszącym kod możecie ustalać własne reguły tego typu. Ja osobiście prawie nigdy nie korzystam z możliwości uruchamiania metod z jednym parametrem z pominięciem nawiasów i kropek, bo uważam, że jest to mniej czytelne. Język Scala na pewno ma więcej różnorodnych konstrukcji i pod tym względem więcej trzeba opanować niż w takiej Javie. Tylko później łatwiej jest coś wyrazić w prostszy sposób, choć można też bardziej zaciemnić kod pisząc byle jak.

"A tak naprawdę to nie unikanie, tylko schowanie tej iteracji w wywołaniu jakiejś metody, tu z biblioteki standardowej ;) "

Pewnie tak, ale jeśli w niej nie ma błędów to ja nie zrobię błędu przez nieopatrzną zmianę wartości iteratora. Przypomina mi się jeden programista C, który udowadniał, że C++ jest mu zbędny, bo potrafi napisać kod w taki sposób, że wykonuje to samo co obiektowy C++. I faktycznie to co pokazywał wyglądało jakby na prawdę realizowało dziedziczenie i polimorfizm. Tyle, że było dość skomplikowane. Nie brał pod uwagę faktu, że to wszystko co on robi zrobili już twórcy języka C++ i nie musi wymyślać koła od nowa ;)

"Nie przekonuje mnie to, że kod funkcyjny jest łatwiejszy do testowania. W przypadku kodu niefunkcyjnego też "można łatwo sprawdzić, czy logika działania jest prawidłowa" - o ile kod jest dobrze napisany"

Nie chodzi o to, że kodu niefunkcyjnego nie da się testować, lub tylko funkcyjny da się testować. Chodzi o to, że skupiając się tylko na pisaniu w stylu funkcyjnym równocześnie zapewniamy sobie lepszą testowalność kodu. To samo możemy uzyskać stosując inny styl kodu, ale pewne zasady są podobne. Mamy więc dwie pieczenie na jednym ogniu ;) W Javie definiujesz dobry kod jako testowalny, a tu jako funkcyjny zaś testowalność robi się sama.

budda86   9 #5 22.09.2014 15:49

@mikolaj_s: "Język Scala na pewno ma więcej różnorodnych konstrukcji [...] później łatwiej jest coś wyrazić w prostszy sposób, choć można też bardziej zaciemnić kod pisząc byle jak"

Mnogość różnorodnych konstrukcji może też powodować, że styl kodowania przyjęty przez jeden zespół będzie mało czytelny dla innego zespołu. Ciekaw jestem, jak to wygląda w praktyce.

"Przypomina mi się jeden programista C, który udowadniał, że C++ jest mu zbędny, bo potrafi napisać kod w taki sposób, że wykonuje to samo co obiektowy C++"

Ja znam jednego programistę C, który twierdzi, że C++ jest mu niepotrzebny, bo to całe dziedziczenie i polimorfizm to absolutnie zbędny bullshit :) Własnoręczna implementacja obiektowości w C to jakiś absurdalny horror.

Z ciekawości - piszesz zawodowo w Scali? W komercyjnych projektach? W Polsce?

mikolaj_s   13 #6 22.09.2014 16:13

@budda86: Dlatego pewne konwencje promują sami twórcy Scali o czym można poczytać na ich stronie. Tym niemniej IMO to nie konwencje są to niebezpieczne, bo to czy na końcu pojawią się nawiasy czy nie to nie jest problem. Problemem jest tak jak w każdym języku pisanie niechlujne. Scala tego nie wybacza, choć daleko jej do Perla. ;) Dobrze napisany kod zrozumie członek innego zespołu niezależnie od stylu, czasem tylko trochę trzeba do tego przywyknąć. Łatwo to sprawdzić porównując kod z różnych projektów Open Source.

"Z ciekawości - piszesz zawodowo w Scali? W komercyjnych projektach? W Polsce?"

Zawodowo zajmuję się edukacją (również uczę Scali). Piszę kod jako freelancer, mam też własny projekt. Pracowałem przez jakiś czas z trójmiejskim guru od Lifta Łukaszem Kuczerą w projekcie komercyjnym. Z czytaniem jego kodu nigdy nie miałem problemu (co najwyżej mogłem nie rozumieć algorytmu, ale to już zupełnie inna historia ;) ). Teraz Łukasz ma własną firmę i piszą kod wyłącznie w Scali (scalac.io)

invader92   5 #7 22.09.2014 21:11

Uczepię się tylko jednego aspektu.
Ja chyba jestem za oporny na pewne rozwiązania, miedzy innymi operator overloading. Używanie operatorów dla obiektów nie-matematycznych, za wyjątkiem (==, +=, +) dla łańcuchów, budzi u mnie wrażenie nieczytelności i uważam, że powinno być ograniczane tak bardzo jak tylko się da, ale jak wspomniałem, to tylko moje zdanie.

Co do artykułu, jak najbardziej mi się podoba.

Autor edytował komentarz.
mikolaj_s   13 #8 22.09.2014 22:56

@invader92: Trudno jest projektantom języka określić kiedy można przeładowywać operatory. O tym musi już decydować sam programista. W Scali mamy możliwość tworzenia dziwnych nowych operatorów, więc trzeba na to uważać i kierować się rozsądkiem. IMHO język albo daje nam dużą swobodę i dzięki temu dużą ekspresywność, możliwość pisania zwartego i nierozwlekłego kodu, wtedy programista sam musi dbać o nie nadużywanie, albo nas pilnuje i wtedy jesteśmy mocno ograniczeni. Na szczęście można wybrać język, który nam pasuje pod tym względem ;)

invader92   5 #9 23.09.2014 18:35

Oby tylko używać ich z rozsądkiem, a nie przeładowywać dla każdego możliwego typu operanda, bo to nie powoduje, że kod jest zwięzły, tylko stanowi śmieć, którego czytelność jest zerowa i trzeba się wgryzać w kolejne wywołania operatorów bo jakiś kretyn zaimplementował jedno przeładowanie z błędem.

W środowiskach, gdzie załoga nie jest stała przez cały czas trwania projektu, a zmienia się w rozsądnych przedziałach czasowych, takie rzeczy to katorga.

Bynajmniej, nie jest to uwaga w Twoim kierunku, żeby mnie ktoś źle nie zrozumiał. Takie tylko głośne myslenie :)

mikolaj_s   13 #10 23.09.2014 18:44

@invader92: Na szczęście większość twórców bibliotek nie nadużywa tego mechanizmu. Za to czasem się przydają operatory, które są skrótami często wywoływanych metod, ułatwiającymi pisanie kodu.

invader92   5 #11 23.09.2014 19:09

I to jest właśnie w miarę dobry pattern, gdzie wywołanie operatora to tylko delegacja do właściwej metody.
Pozdrawiam :)

Autor edytował komentarz.
  #12 24.09.2014 22:02

Szkoda, że Scala nie radzi sobie w przypadku bardziej zaawansowanych konstrukcji np. transduktorów (bo wysiada jej prymitywny system typów), ale dla typowego klepacza javy może być interesująca. Z drugiej strony przy Java 8 nauka Scali nie ma większego sensu.