18 czerwca 2009

Rozdział 6. o napisach w Groovy z "Programming Groovy"

Rozdział 6. "Working with Strings" dotyczy obsługi literałów i ciągów znakowych (napisów). Tworzymy literał znakowy w Groovy za pomocą pojedynczych cudzysłowów, np. 'cześć', a napisy z podwójnymi, np. "cześć". W przeciwieństwie do Javy, w Groovy 'a' i "a" są tymi samymi ciągami znaków typu String.
 groovy:000> a1 = 'a'
===> a
groovy:000> a2 = "a"
===> a
groovy:000> a1.class.name
===> java.lang.String
groovy:000> a2.class.name
===> java.lang.String
Jeśli chcielibyśmy stworzyć egzemplarz dla pojedynczego znaku (w Javie jako char lub java.lang.Character) wystarczy skorzystać z konstrukcji as char.
 groovy:000> a1 = 'a'
===> a
groovy:000> a1.class.name
===> java.lang.String
groovy:000> a3 = a1 as char
===> a
groovy:000> a3.class.name
===> java.lang.Character
Przypominam, że nie istnieją typy proste w Groovy, a ich użycie to w rzeczywistości utworzenie egzemplarza typu opakowującego, np. dla char będzie to java.lang.Character.

W ramach pojedynczych cudzysłowów możemy umieścić podwójne.
 groovy:000> a4 = '"a"'
===> "a"
groovy:000> a4.class.name
===> java.lang.String
Różnica między literałem a napisem jest w rozwiązywaniu wyrażeń ${}.
 groovy:000> wartosc = 5
===> 5
groovy:000> tekstJakoLiteral = 'Podano wartosc: ${wartosc}'
===> Podano wartosc: ${wartosc}
groovy:000> tekstJakoNapis = "Podano wartosc: ${wartosc}"
===> Podano wartosc: 5
Napisy są niezmienne (niemodyfikowalne, ang. immutable), więc zmiana ich wartości jest niemożliwa (chyba, że korzystają z domknięć zczytujących dane ze środowiska, ale sam napis jest niezmienny). Pamiętam, że rozgłaszałem coś zupełnie odwrotnego na ostatniej mojej prezentacji Groovy i Grails we Wrocławiu za co przepraszam. Tutaj nic się nie zmienia w stosunku do Javy.

Odczyt z literału bądź napisu jest poprzez symbol tablicowy [].
 groovy:000> napis = "Ala ma kota"
===> Ala ma kota
groovy:000> napis[0]
===> A
groovy:000> napis[0..3]
===> Ala
Specjalne znaki w napisach, np. $ (dolar) możemy pozbawić ich specjalności przez poprzedzenie ich ukośnikiem lewym '\' (ang. backslash), np. \$, albo przez skorzystanie z kolejnej konstrukcji do tworzenia specjalnych napisów przez użycie ukośnika prawego '/' (ang. slash).
 groovy:000> dolary = 500
===> 500
groovy:000>
groovy:000> napis = "Za 1 PLN placimy ${dolary}\$"
===> Za 1 PLN placimy 500$
groovy:000> napisUkosnikPrawy = /Za 1 PLN placimy ${dolary}$/
===> Za 1 PLN placimy 500$
groovy:000> dolary = 200
===> 200
groovy:000> napis
===> Za 1 PLN placimy 500$
groovy:000> napisUkosnikPrawy
===> Za 1 PLN placimy 500$
groovy:000> napisUkosnikPrawy = /Za 1 PLN placimy ${-> dolary}$/
===> Za 1 PLN placimy 200$
groovy:000> dolary = 10
===> 10
groovy:000> napisUkosnikPrawy
===> Za 1 PLN placimy 10$
Jak widać w przykładzie wyżej, Groovy rozwiązuje wartości zmiennych w napisach natychmiast podczas ich tworzenia (gorliwie), a opóźniony (leniwy) odczyt jest możliwy przez zastosowanie...domknięć (bezparametrowych).

Typem dla napisów z wyrażeniami ${} w Groovy jest GString (ang. Groovy string). Groovy inteligentnie przypisuje typ do naszych potrzeb.
 groovy:000> (/napis/).class
===> class java.lang.String
groovy:000> zmienna = 5
===> 5
groovy:000> (/napis ze zmienna ${zmienna}/).class
===> class org.codehaus.groovy.runtime.GStringImpl
groovy:000> "kolejny napis".class
===> class java.lang.String
groovy:000> "kolejny napis, ale juz ze zmienna ${zmienna}".class
===> class org.codehaus.groovy.runtime.GStringImpl
Połączenie GString i domknięć pokazuje jak Groovy traktuje domknięcia. Domknięcie to po prostu blok wykonywalny, który również może być przypisany do zmiennej, a użyty wprost będzie przypisany do zmiennej anonimowej.
 groovy:000> wartosc = 4
===> 4
groovy:000> napis = "Wypiszemy napis wykonujac domkniecie ${-> wartosc}"
===> Wypiszemy napis wykonujac domkniecie 4
groovy:000> napis = "Wypiszemy napis wykonujac domkniecie $wartosc"
===> Wypiszemy napis wykonujac domkniecie 4
// definiuję domknięcie
groovy:000> domkn = {-> wartosc}
===> groovysh_evaluate$_run_closure1@f268de
// i buduję napis ze zmienną, która jest domknięciem
groovy:000> napis = "Wypiszemy napis wykonujac domkniecie $domkn"
===> Wypiszemy napis wykonujac domkniecie 4
W Groovy napisy składające się z wielu linii budujemy z potrójnym podwójnym cudzysłowem i potrójnym pojedynczym cudzysłowem. Pięknie napisane, co? Innymi słowy, trzy razy piszemy pojedynczy cudzysłów, jeśli nie korzystamy z wyrażeń, a z nimi budujemy napis opakowany trzema podwójnymi.
 groovy:000> napisBezZmiennychNaKilkaLinii = '''Zaczynam pisac,
groovy:001> Linia 2
groovy:002> Linia 3 i kilka innych dodatkow, np. wyrazenie: $wartosc'''
===> Zaczynam pisac,
Linia 2
Linia 3 i kilka innych dodatkow, np. wyrazenie: $wartosc
groovy:000> napisZeZmiennymiNaKilkaLinii = """Linia 1
groovy:001> Linia 2
groovy:002> Linia 3 i zmienna $wartosc"""
===> Linia 1
Linia 2
Linia 3 i zmienna 4
Przydatne chociażby do tworzenia plików XML, albo, jak to ujął autor, "Groovy even makes it easy to spam! (Hey, I'm just kidding.)" (str. 109). Żartowniś! :)

W Groovy mamy przeciążanie operatorów, więc odejmowanie w napisach to wycinanie napisu. Dodano metodę minus() do typu String i możemy ciąć.
 groovy:000> napis = "Ala ma kota"
===> Ala ma kota
groovy:000> napis -= "ma"
===> Ala kota
Możemy również iterować po alfabecie, co w połączeniu z napisami daje niesamowity efekt iterowania po napisach.
 groovy:000> for (str in 'ala'..'alg') {
groovy:001> print "$str "
groovy:002> }
ala alb alc ald ale alf alg ===> null
Groovy rozszerza klasę String o udostępnienie metod do pracy z wyrażeniami regularnymi. W ten sposób poza replaceFirst() oraz replaceAll() mamy ~ (tylda) (odpowiada funkcji negate()), który tworzy egzemplarz wzorca, co w połączeniu z napisami tworzonymi przez ukośniki przypomina Perla.
 groovy:000> wzorzec = ~/\w*/
===> \w*
groovy:000> wzorzec.class.name
===> java.util.regex.Pattern
groovy:000> "Ala ma kota" =~ wzorzec
===> java.util.regex.Matcher[pattern=\w* region=0,11 lastmatch=]
groovy:000> matcher = "Ala ma kota" =~ wzorzec
===> java.util.regex.Matcher[pattern=\w* region=0,11 lastmatch=]
groovy:000> matcher.size()
===> 6
groovy:000> matcher[0]
===> Ala
groovy:000> matcher[1]
===>
groovy:000> for (wyraz in matcher) {
groovy:001> println "$wyraz"
groovy:002> }
Ala

ma

kota

===> null
Nawet, jeśli nie zaczniemy pisać rozwiązań przemysłowych w Groovy, to do skryptów jest bezkonkurencyjny. Zamiast Perla możemy wykorzystać JVM z Groovy - w chwilach słabości zawsze możemy zejść na poziom Javy.

Warto również przyjrzeć się różnicy między ==~ a =~. Pierwszy z nich sprawdza pełne dopasowanie, podczas gdy drugi jedynie częściowe.
 groovy:000> matcher = "Ala ma kota" ==~ /ma/
===> false
groovy:000> matcher = "Ala ma kota" =~ /ma/
===> java.util.regex.Matcher[pattern=ma region=0,11 lastmatch=]
groovy:000> matcher = "Ala ma kota" ==~ /Ala \w* kota/
===> true
Niestety nie udało mi się napisać wyrażenia regularnego w stylu /\w{6}/ i nie wiem, dlaczego przykład poniżej zwraca false.
 groovy:000> matcher = "Ala ma kota" ==~ /\w{6}/
===> false
Ma ktoś pomysł? Chętnie poznam rozwiązanie (i później będę się nim chwalił jak swoim :)). Na koniec przykład z książki (str. 113):
 groovy:000> str = 'Groovy is groovy, really groovy'
===> Groovy is groovy, really groovy
groovy:000> (str =~ /groovy/).replaceAll('hip')
===> Groovy is hip, really hip
Jestem zauroczony!

p.s. W komentarzu do wczorajszego wpisu Dokończenie rozdziału 5. o domknięciach z "Programming Groovy" pedro podniósł temat sprawdzenia, czy domknięcie jest faktycznie domknięciem. Odpowiedź w postaci przykładu.
 groovy:000> def funkcja(domkniecie) {
groovy:001> if (domkniecie && domkniecie instanceof Closure) { return domkniecie() }
groovy:002> println "Parametr wejsciowy typu: ${domkniecie.class.name}"
groovy:003> }
===> true
groovy:000> funkcja { "Jestem domknieciem!" }
===> Jestem domknieciem!
groovy:000> funkcja "Jestem jedynie napisem"
Parametr wejsciowy typu: java.lang.String
===> null
Dodałem sprawdzenie z wykorzystaniem instanceof.

6 komentarzy:

  1. Odnosnie przykladu
    matcher = "Ala ma kota" ==~ /\w{6}/

    to przeciez \w dopasowuje sie do znaku slowa (alphanumeric or _), a w tym tekscie nie ma ciaglej sekwencji 6 takich wlasnie znakow.
    Sprobuj z \w{4} i zadziala

    OdpowiedzUsuń
  2. Próbowałem od 1 do 6 i nic. Przynajmniej będzie okazja popróbować się z wyrażeniami regularnymi poznając Groovy.

    OdpowiedzUsuń
  3. ==~ operator requires an exact match of the whole string.

    matcher = "Ala" ==~ /\w{3}/
    True
    matcher = "Alakot" ==~ /\w{6}/
    True


    matcher = "Ala ma kota" =~ /\w{3}/
    matcher.size() // 2

    matcher = "Ala ma kota" =~ /\w{6}/
    matcher.size() // 0

    To wam powinno pomóc :)

    OdpowiedzUsuń
  4. Fajne są wyrażenia co? Mi szczególnie się podoba

    def f = new File("c:/")

    f.eachFileMatch(~/.*\.ini/){
    println it
    }

    :)

    Eksperymentowałem z domknięciami.
    Na początek:


    def x ={-> println "Hello world :)"}

    def ifClosure = {domniemane, reakcja ->
    if(domniemane != null && domniemane instanceof Closure ){
    reakcja()
    }
    }

    ifClosure(x){
    println "Zaczynam zabawę"
    x()
    println "Koniec zabawy"
    }


    Proste i wiadomo co się stanie. Dobra a jeśli zrobimy tak:


    Object.metaClass.ifClosure = {reakcja ->
    if(delegate != null && delegate instanceof Closure ){
    reakcja()
    }else{
    delegate
    }
    }

    def y ={-> println "Hello world :)"}

    y.ifClosure{
    y()
    }

    println "abc".ifClosure{
    ":)"
    }


    No i teraz zgadnij jak to się zachowa? Można używać GroovyConsole ;)

    Pozdrawiam,
    Krzysztof Kowalczyk

    OdpowiedzUsuń
  5. "Ala ma kota" ==~ /(\w+\s?){3}/
    ===> true
    "Ala ma kota" ==~ /(\w+\s){2}\w+/
    ===> true
    "Ala ma kota" ==~ /\w+ \w+ \w+/
    ===> true

    OdpowiedzUsuń
  6. Witam
    Co do przykładu z closure to nie zadziała dla:

    funkcja null

    Sprawdzanie closure!=null jest zbędne, ponieważ instanceof zwróci zawsze false jak null przyjdzie.

    Wystarczy dać:

    def funkcja(domkniecie) {
    if (domkniecie instanceof Closure) {
    return domkniecie()
    }

    println "Parametr wejsciowy typu: ${domkniecie?.class?.name}" // sprawdzanie null
    }

    funkcja 3

    funkcja null

    funkcja()

    funkcja {
    funkcja 44
    }

    Pozdrawiam
    Andrzej Jóźwik

    OdpowiedzUsuń