O pracy u podstaw

Maj 27th, 2016
Okładaka:Software Craftsmanship

Tytuł: Software Craftsmanship
Autor: Sandro Mancuso
Rok: 2014 (PL 2016)
ISBN: 978-832-4685-03-5

Nie kupujcie tej książki w języku polskim. Po prostu tłumaczenie w wykonaniu Helionu jest straszne. I by nie być gołosłownym, mała próbka nowatorskich pojęć:

  • baza kodowa – zapewne chodzi o code base, czyli o kod aplikacji, po prostu.
  • „Projekt był realizowany w stylu kaskadowym” – yyy… jak już to w modelu kaskadowym…
  • Oprogramowanie pośredniczące – w oryginale middleware.

Na początku bawi. Później tylko utrudnia czytanie. A książka?

Sama książka bardzo fajna. Ruch Software Craftmanship staje się coraz popularniejszy, a co za tym idzie, potrzebujemy odpowiedniego opisu, czym on jest. Autor ma doświadczenie, wiedzę oraz pasję. Zresztą słowo pasja przewija się przez całą książkę. Z rzeczy ważnych i przydatnych. Rozdział o rekrutacji to lektura obowiązkowa dla każdego. Ciekawy jest ten poświęcony starszeństwu oraz awansowi w branży IT. Paradoks tej książki polega na tym, że traktuje ona o umiejętnościach „twardych” w „miękki” sposób.

Nie wszystko jest tu jednak fajne. Mam wrażenie, że książka nie jest do zaaplikowania na naszym polskim korpo-podwórku. Z wielu powodów, ale chyba najważniejszym jest „hinduizacja” rynku IT. Ciekaw jestem reakcji autora na słowa Filipiaka o zastępowaniu ekspertów studentami. Z tego punktu jest to bardzo pesymistyczna książka. Jednak warto ją przeczytać, jeżeli tylko traktujesz programowanie jako pasję, a nie tylko metodę na zarabianie mamuta.

Funkcje wyższego rzędu w Kotlinie

Maj 26th, 2016

Wiemy już, że w Kotlinie są funkcje i możemy wywnioskować, że są bytami podstawowymi. W języku ogólno-informatycznym mówimy też o funkcjach wyższego rzędu (ang High-order functionW (HOF). Kotlin ogarnia te oba pojęcia (są one bliskoznaczne).

Funkcje wyższego rzędu – co to?

Zacznijmy od maleńkiej teorii. Funkcją wyższego rzędu nazywamy taką funkcję, która jako parametr przyjmuje inną funkcję lub wynikiem jej działania jest funkcja. Z funkcjami wyższego rzędu nierozerwalnie są związane dwa kolejne pojęcia.

Currying

Jest to operacja polegająca na zamianie funkcji o dwóch argumentach zwracających pewną wartość na funkcję o jednym argumencie zwracającym funkcję, która zwraca dana wartość. Inaczej mówiąc:

F(A×B) → C » F(A) → G(B) → C

Proces odwrotny nosi nazwę uncurring. Nazwa pochodzi od nazwiska Haskella CurregoW

Częściowa aplikacja (ang. partial applicationW)

Jest to proces fiksowania jednego z argumentów funkcji wieloargumentowej w celu zmniejszenia liczby argumentów:

F(A×B) → C » F(n×B) → C

Te dwa pojęcia są ze sobą często mylone ponieważ efektywnie dają podobne rezultaty – funkcję o mniejszej liczbie parametrów. Jest jeszcze częściowa ewaluacjaW, która jest jednak czymś zupełnie innym.

Różnice pomiędzy tymi pojęciami oraz w sumie cały „pałer” funkcji wyższego rzędu w Kotlinie przedstawia poniższy listing:

Listing 1. Currying, częściowa aplikacja i HOF w Kotlinie

    println(sum(2, 3))

    val curried = curr(::sum)
    val partial = first2(::sum)
    val duplicate = first2(::dup)


    println(curried.toString() + " → " + curried(3)(2))
    println(partial.toString() + " → " + partial(3))
    println(duplicate.toString() + " → " + duplicate("A"))

}

fun sum(a: Int, b: Int): Int {
    return a + b
}

fun <T, U, R> curr(f: (T, U) -> R): (T) -> (U) -> R {
    return fun(t: T): (U) -> R {
        return fun(u: U): R {
            return f(t, u)
        }
    }
}

fun <T, R> first2(f:(Int, T) -> R): (T) -> R {
    return fun(b: T): R {
        return f(2, b);
    }
}

Na listingu 1. mamy pełne spektrum możliwości jakie daje nam kotlin. Po pierwsze możemy funkcję przypisać do zmiennej np. curried czy partial. Po drugie możemy przekazać funkcję jako parametr first2(::sum). Po trzecie funkcja może być zwrócona jako wynik działania funkcji curr i first2, po czym można ją wywołać przekazując „brakujące” argumenty curried(3)(2) i partial(3).

Oczywiście całość można wykorzystać też w inny sposób:

Listing 2. Duplikacja Stringa

fun main(args: Array<String>) {

    val duplicate = first2(::dup)
    
    println(duplicate("A"))

}

operator fun Int.times(s: String): String {
    fun con(a: String, i: Int): String {
        return a + s;
    }
    return 1.rangeTo(this).fold("", ::con);
}

fun dup(a: Int, b: String): String {
    return a * b
}

fun <T, R> first2(f:(Int, T) -> R): (T) -> R {
    return fun(b: T): R {
        return f(2, b);
    }
}

Mamy tu funkcję wewnętrzną con, która jest przekazywana do fold jako operator. Dodatkowo typ Int rozszerzamy o operację mnożenia Stringów. Mając HOF mamy bardzo duże możliwości w tworzeniu kodu.

New vs of, czyli o tym jak tworzyć kolekcje

Maj 25th, 2016

W czasie dyskusji pod jednym z poprzednich wpisów, bodajże na wykopie, pojawiło się pytanie dlaczego w jednym z listingów używałem new ArrayList(), a na innych guavowego Lists.newArrayList. Cóż… wszystko jest kwestią wygody. Czasami wygodniej jest mi utworzyć listę za pomocą guavy. Kiedy indziej tworzę po prostu obiekt z użyciem new. Jednak na poziomie projektu można wymóc pewną konwencję.

Zalety

Główną zaletą jest spójność tworzonego kodu. Wszędzie mamy taką samą konwencję. Pozwala to na narzucenie określonego standardu. Kolejna sprawa to zasada nie używania new. W takim podejściu przerzucamy pełną odpowiedzialność za tworzenie zależności do kontenera DI. Jest to oczywiście ekstremalne podejście do problemu, ale pozwala nam na ćwiczenie się w tworzeniu odpornego kodu.

Kolejna sprawa to czytelność. Obie formy zarówno metoda fabrykująca jak i konstruktor, jeżeli tylko są używane spójnie w całym projekcie, to podnoszą czytelność kodu. Pozwalają na wyrobienie w głowie pewnych schematów w rodzaju „Jeżeli tu mam newArrayList, to zapewne już później nie będę do niej nic dodawać” albo „O new ArrayList, czyli zaraz będziemy dodawać”.

Wady

Jeżeli pojawia się problem to jest on systemowy. Bez łamania konwencji w jednym punkcie nie możemy go naprawić. Aklimatyzacja nowych osób. Czasami tego typu regułki są wkurzające. Kiedyś pracowałem w projekcie gdzie zakazane było używanie importów statycznych. Jednocześnie masowo wykorzystywano metody statyczne z klas wewnętrznych. Bolało.

Innym problemem jest sama implementacja kolekcji w Javie. Nie są one niezmienne, co powoduje, że trzeba się nagimnastykować by taką utworzyć. Nawet jak już utworzymy kolekcję niezmienną, to nadal nie jest ona kowariantna, tak jak w kotlinie. W efekcie tracimy kilka przydatnych elementów.

Podsumowanie

W ramach ćwiczenia warto napisać sobie projekt bez używania new. Pomóc w tym może używanie odpowiednich metod fabrykujących. Z drugiej strony warto pozostawić sobie swobodę, ale też wypracować własną strategię tak by osoba czytająca kod rozumiała co oznacza użycie tej czy innej konstrukcji dla kolejnych kroków.

Zakresy i iteracja

Maj 24th, 2016

Kolejny mały temat, zanim powrócimy do funkcji. Jak wiemy, Kotlin pozwala na przeciążanie operatorów. Jednym z nich jest operator podwójnej kropki – .., który służy do stworzenia zakresu. A do czego może posłużyć nam zakres? Na przykład do uproszczenia zapisu w pętli:

Listing 1. Wykorzystanie zakresu Int

fun main(args: Array<String>) {
    for (i in 1..5)
        println(i)
}

Własne zakresy

Oczywiście możemy utworzyć własny zakres. W tym celu należy przeciążyć operator .. oraz utworzyć klasę rozszerzającą ClosedRange. Dodatkowo klasa, która będzie wyposażona w zakres musi implementować Comparable.

Listing 2. Przygotowanie klasy Version do działania w zakresach

data class Version(val v: Int) : Comparable<Version> {
    override fun compareTo(other: Version): Int {
        return this.v.compareTo(other.v)
    }
}

Kolejnym krokiem jest dodanie odpowiedniego operatora.

Listing 3. Dodanie operatora .. do klasy Version

operator fun Version.rangeTo(b: Version): VersionRange {
    return VersionRange(this, b)
}

W tym miejscu wiemy już, że potrzebujemy klasy VersionRange, która będzie implementować ClosedRange.

Listing 4. Klasa VersionRange

class VersionRange(override val start: Version, override val endInclusive: Version) : ClosedRange<Version>

W ten sposób mamy już skonfigurowany nasz zakres. Jednak nadal nie możemy użyć go w pętli. Sam zakres opisuje tylko pewien zbiór wartości. Nie mówi nic o tym, jak należy się w nim poruszać. W tym celu należy zaimplementować interfejs Iterable:

Listing 5. Klasa VersionRange

class VersionRange(override val start: Version, override val endInclusive: Version) : ClosedRange<Version>, Iterable<Version>{

    override fun iterator(): Iterator<Version> = VersionIterator(start, endInclusive)

}

Implementacja interfejsu pociąga za sobą konieczność stworzenia klasy VersionIterator:

Listing 6. Klasa VersionIterator

class VersionRange(override val start: Version, override val endInclusive: Version) : ClosedRange<Version>, Iterable<Version>{

    override fun iterator(): Iterator<Version> = VersionIterator(start, endInclusive)

    private class VersionIterator(first: Version, last: Version) : Iterator<Version> {
        private var next = first
        private val finalElement = last
        private var hasNext: Boolean = first <= last

        override fun hasNext(): Boolean = hasNext

        override fun next(): Version {
            val value = next
            if (value == finalElement) {
                hasNext = false
            } else {
                next++
            }
            return value
        }

    }
}

operator fun Version.inc(): Version {
    return copy(v + 1);
}

Gotowe. By móc wykorzystać ++ musieliśmy jeszcze dodać przeciążenie tego operatora, ale to już tylko czynność ekstra, by uprościć zapis. Możemy teraz napisać:

Listing 7. Przykładowe użycie

fun main(args: Array<String>) {
    for (v in Version(1)..Version(5))
        println(v)
}

Podsumowanie

Tworzenie własnych zakresów bywa przydatne. Szczególnie tam gdzie klasy domenowe mają „naturalną” właściwość do bycia zakresami. Wszelkiej maści wersje, numery seryjne, numery dokumentów są świetnymi kandydatami do tej operacji.

PS

Iterator można zaimplementować z wykorzystaniem AbstractIterator:

Listing 8. Wykorzystanie AbstractIterator

class VersionRange(override val start: Version, override val endInclusive: Version) : ClosedRange<Version>, Iterable<Version> {

    override fun iterator(): Iterator<Version> = VersionIterator(start, endInclusive)

    private class VersionIterator(var first: Version, val last: Version) : AbstractIterator<Version>() {
        override fun computeNext() {
            if(first > last)
                return done();
            setNext(first++)
        }
    }
}

Czyni to kod bardziej zwięzłym.

Wstęp do testowania w Kotlinie ze Spek

Maj 23rd, 2016

Gdy półtora miesiąca temu popełniłem pierwszy tekst poświęcony w całości Kotlinowi zamieściłem tam testy napisane na modłę xUnita. Dziś wrócimy do tamtego tematu, ale testy napiszemy z wykorzystaniem Spek. Framework ten jest czymś pomiędzy podejściem TDD, a BDD. Sami twórcy mocno bronią się przed zakwalifikowaniem go jako narzędzia BDD. Swoją drogą Spek to po holendersku boczek, zatem BDD można rozwinąć do Becon Driven Development. Dla mnie koszerne.

Specyfikacja

Pisząc testy w Spek, używamy DSLa, zdefiniowanego przez ten framework. Sam zestaw testów jest specyfikacją, która pozwala na opisanie zachowania i spodziewanych wyników.

Listing 1. Prosta specyfikacja

class SpekVersionTest : Spek({

    describe("When we have Version 0") {
        val v0 = Version(0)

        it("Should return new object after incrementation") {
            val v1 = +v0;
            assertEquals(1, v1.version)
            assertNotEquals(v0, v1)
        }

    }
})

Mamy tu kilka elementów. Po pierwsze nasza klasa testowa musi rozszerzać Spek. Konstruktor jako parametr przyjmuje opis testu (technikaliom tego wyrażenia przyjrzymy się kiedy indziej). W sekcji describe umieszczamy kod przygotowujący stan początkowy. Ma ona sens metody setUp. Może występować wielokrotnie. Sekcja it ma sens pojedynczego testu. Służy do weryfikacji zachowania. Może być wiele sekcji tego typu w ramach jednej sekcji description.

W stylu BDD

Spek udostępnia też trochę inną notację, która jest oparta o model given/when/then. Przy czym nazywa ją given/on/it. Powyższy test przepisany na takie podejście będzie wyglądał następująco:

Listing 2. Inny sposób zapisu

class SpekVersionTest2 : Spek({

    given("Version 0") {
        val v0 = Version(0)
        on(" Increment ") {
            val v1 = +v0;
            it(" returns new version 1") {
                assertEquals(1, v1.version)
            }

            it(" are not equals ") {
                assertNotEquals(v0, v1)
            }
        }
    }
}

Pomiędzy tymi dwoma podejściami nie ma większych różnic technicznych. Różnica polega na sposobie prezentacji danych (dodałem kilka testów):


specVT

spekVT2

W przypadku drugiej wersji mamy „głębsze” drzewo, co przekłada się na trochę inny sposób organizacji testów.

Podsumowanie

Spek jest ciekawym i prostym rozwiązaniem, które pozwala na pisanie testów w trochę inny sposób. Ma przyjazny DSL, który może też być wykorzystany w Javie. To, co zauważyłem, to błędy w dokumentacji, które na początku mogą powodować problemy ze zrozumieniem zasady działania całości.

Kolekcje w Kotline, czyli małe rzeczy robią dużą różnicę

Maj 22nd, 2016

Jak przystało na nowocześnie zaprojektowany język programowania, Kotlin posiada kolekcje niezmienne i zmienne „na równych prawach”. W przeciwieństwie do Javy, gdzie kolekcje mutowane są preferowane na poziomie API (by zrobić kolekcję niemutowaną, trzeba się naklepać). BTW, wynika to prostego faktu – przed Javą 1.5 nie było metod o zmiennej liczbie argumentów, więc nie można było zaprojektować jakiegoś sensownego API.

Listing 1. Przykład tworzenie kolekcji

fun main(args: Array<String>) {
    
    val immute = listOf("Ania", "Ela", "Wojtek")
    val mutant = mutableListOf("Ania", "Ela", "Wojtek")
    mutant.add("Henio")
    
    println(immute)
    println(mutant)
}

Na tym poziomie nie ma tu nic zaskakującego. Ogólna konwencja w API jest taka, że mamy pary metod xOf i mutableXOf. Co ważne można w ten sposób tworzyć też mapy:

Listing 2. Tworzenie map

fun main(args: Array<String>) {

    val pierscienie = mapOf(9 to "ludzie", 7 to "Krasnoludów", 3 to "Elfy", 1 to "Sauron")

    println(pierscienie)

}

W sumie nie ma tu nic niezwykłego. Jest jednak jedna drobna rzecz, która powoduje, że Kotlin wyróżnia się spośród innych języków. Twórcy założyli, że kolekcje niezmienne będą kowariantne. To znaczy, jeżeli napiszemy:

Listing 3. Kowariancja kolekcji niezmiennych

fun main(args: Array<String>) {

    val rectangles = listOf(Rectangle())
    val shapses: List<Shape> = rectangles

}

To będzie to prawidłowy kod z punktu widzenia systemu typów. Inaczej mówiąc, kowariancja oznacza, iż lista kwadratów jest podtypem listy kształtów, ponieważ kwadrat jest podtypem kształtu. Tak samo zapisany kod dla list kształtów, która jest mutująca:

Listing 4. A tu się wywali

fun main(args: Array<String>) {

    val rectangles = listOf(Rectangle())
    val shapses: MutableList<Shape> = rectangles

}

Zwróci błąd nieprawidłowego typu. Mała rzecz, a cieszy. Wytłumaczenie tego zachowania jest oczywiste. Jeżeli lista kształtów może zmieniać zawartość, a „pod spodem“ jest lista kwadratów, to dodanie na przykład trójkąta spowoduje błąd. Tyle na dziś.

Postanowiłem być złym moderatorem

Maj 21st, 2016

Przynajmniej przez pewien czas. To będzie eksperyment społeczno-programistyczny.

Dotychczas

Zakładałem, że każdy kiedyś się uczył i każdy ma prawo się mylić. Dlatego też nie zwracałem uwagi na drobnostki w postaci bezsensownych działań, niektórych użytkowników. Tłumaczyłem, pokazywałem, poprawiałem.

Jak będzie

Teraz na pewien czas nastąpi zmiana. Przede wszystkim trzy podstawowe pytania:

  • Czy masz testy do tego kodu?
  • Czy testy są prawidłowe?
  • Co stanie się, jak do testów podstawisz dane, które powodują powyższy błąd?

I będę obserwował proces gnicia.

„Mały” polimorfizm ad hoc z rozszerzeniami w Kotlinie

Maj 20th, 2016

Grozą powiało z tytułu. Na początek dwa słowa wyjaśnienia. Polimorfizm ad hoc jest forma polimorfizmu, która pozwala na wywołanie metody bez dokładnej wiedzy, jakiego typu jest obiekt, na którym metodę wywołujemy. W uproszczeniu można powiedzieć, że znany ze scali mechanizm implicit conversion, jest tu dobrym przykładem. Jego działanie można opisać w następujący sposób:

Listing 1. Przykład działania implicit conversion

class Person (val name:String){}

class Node(val value:String) {}

def printNode(n:Node) = {
    n.value
}

implicit def toNode(p:Person) = new Node(p.name)

printNode(new Person("Staś"))
  • Wywołaj metodę printNode, ale
  • Jako parametr podano jednak obiekt typu Person, ale
  • Istnieje funkcja toNode, która zamieni Person na Node.

I dlatego kompilacja projektu w Scali jest tak potwornie długa. Kompilator musi poszukać odpowiednich konwersji. Jednocześnie metoda printNode jest polimorficzna, bo jako argument możemy przekazać jej obiekt dowolnego typu pod warunkiem, że gdzieś istnieje odpowiednia konwersja. Zostanie ona wykonana za nas. Dlatego też nazywa się to polimorfizmem ad hoc. Typ Person nie wie, że jest pożeniony z typem Node.

Po co mi to?

Kotlin nie ma mechanizmu niejawnych konwersji. Szkoda, bo jest to bardzo przydatna rzecz. Co prawda może spowodować ciekawe fakapy w kodzie, a i masakrycznie spowalnia, ale zawsze. Zamiast tego posiada mechanizm nazwany extension. Działa on inaczej, ale na podstawowym poziomie ma podobne możliwości i daje podobne efekty. Zanim się mu dokładnie przyjrzymy popatrzmy, do czego możemy go wykorzystać.

Załóżmy, że mamy prostą aplikację, w której mamy dwie data class (DC):

Listing 2. Przykładowa aplikacja

data class Person(val name: String, val boss: Person?)
data class Workflow(val name: String, val owner: Person)

fun main(args: Array<String>) {
    val ania = Person("Ania", null);
    val ela = Person("Ela", ania);
    val kawusia = Workflow("kawusia", ela);
}

Teraz chcemy wypisać nasz Workflow w postaci JSONa. Metod jest kilka. Jednak te najbardziej oczywiste w świecie Javowym łączą się z koniecznością modyfikacji kodu naszych DC. Względnie możemy pokombinować z interfejsem i domyślnymi metodami. Jak by nie kombinował zawsze będzie źle. Dodatkowym minusem jest „zmuszanie” DC do posiadania wiedzy o swojej reprezentacji. Pisząc Lottomat mieliśmy do czynienia z tym problemem.

Dodawanie metod do klas

Uwaga: jeżeli mówię o metodach w klasie, to oczywiście są to funkcje. Jednak jak pisałem wczoraj – funkcja w klasie, powinna być traktowana jak metoda.

Extensions to nic innego niż mechanizm pozwalający na dodanie do klasy metody bez ingerowania w kod klasy. Zacznijmy od dodania funkcji zmiany na JSONa do klasy Workflow:

Listing 3. Nowa metoda w Workflow

fun Workflow.toJson(): String {
        return """{
    name: $name,
    owner: $owner
}"""
}

I teraz możemy napisać:

Listing 4. Wypisanie JSONa

data class Person(val name: String, val boss: Person?)
data class Workflow(val name: String, val owner: Person)

fun main(args: Array<String>) {
    val ania = Person("Ania", null);
    val ela = Person("Ela", ania);
    val kawusia = Workflow("kawusia", ela);

    println(kawusia.toJson())
}

I wszystko będzie ok. Na tej samej zasadzie możemy dodawać pola.

Dlaczego „mały”?

Jest jednak pewien „mały” problem. Extensions są rozwiązywane statycznie. Oznacza to, że metoda do wywołania jest wskazywana na podstawie typu określonego przez wywołującego, a nie na podstawie rzeczywistego typu obiektu, na którym wywołujemy daną funkcję. Na przykładzie:

Listing 5. Statyczne wyznaczenie wywołanej metody

data class Person(val name: String, val boss: Person?)
data class Workflow(val name: String, val owner: Person)

fun main(args: Array<String>) {
    val ania = Person("Ania", null);
    val ela = Person("Ela", ania);
    val kawusia = Workflow("kawusia", ela);

    printJson(kawusia)
}

fun <T> printJson(t:T){
    println(t?.toJson())
}

fun <T> T.toJson():String = ""

fun Workflow.toJson(): String {
        return """{
    name: $name,
    owner: $owner
}"""
}

Wypisze pusty ciąg znaków. Dlaczego tak? W momencie, gdy określamy jakiego typu jest parametr printJson to otrzymujemy typ T. Następnie szukamy pasującego rozszerzenia T.toJson. Zatem to ono zostanie użyte. Rozszerzenie Workflow.toJson zostanie zignorowane, ponieważ Workflow nie jest RÓWNY T. I to boli. Najzwyczajniej w świecie boli, ponieważ nie daje takiej elastyczności jak w przypadki konwersji niejawnych.

Dlatego też użyłem określenia „mały”. Nie jest to w pełnoprawny polimorfizm ad hoc. Funkcja musi coś niecoś wiedzieć o tym, co zostało jej przekazane. Jak sobie z tym poradzić? Na przykład w ten sposób:

Listing 6. Wypisanie JSONa – kompletny program

data class Person(val name: String, val boss: Person?)
data class Workflow(val name: String, val owner: Person)

fun main(args: Array<String>) {
    val ania = Person("Ania", null);
    val ela = Person("Ela", ania);
    val kawusia = Workflow("kawusia", ela);

    jsonPrinter(kawusia)
}

fun <T> jsonPrinter(t: T) {

    fun Person.toJson(): String {
        return """{
    name: ${name},
    boss: ${boss?.toJson()}
}"""
    }

    fun Workflow.toJson(): String {
        return """{
    name: $name,
    owner: ${owner.toJson()}
}"""
    }

    when (t) {
        is Workflow -> println(t.toJson())
        else -> ""
    }
}

Choć nadal nie jest to najlepsze rozwiązanie, ponieważ musi być zachowana kolejność definiowania rozszerzeń.

Podsumowanie

Mechanizm rozszerzeń jest bardzo ciekawym i przydatnym rozwiązaniem. Jednocześnie ze względu na swój statyczny charakter nie pozwala w pełni wykorzystać możliwości polimorfizmu ad hoc. Dlatego też mam obawę, że będzie on źródłem naprawdę porytego kodu.

Funkcje w Kotlinie – podstawy

Maj 19th, 2016

Pisząc o Kotlinie, dość często w przykładach używam funkcji. Czym jednak są funkcje i co można z nimi zrobić? Dziś wprowadzenie, a w kolejnych wpisach funkcje inline i lambdy.

Funkcje, a metody oraz czym jest main

Jeżeli funkcja została zdefiniowana w ramach klasy:

Listing 1. Klasa z funkcją

class Example(){
   fun someFun(){
      //...
   }
}

to można ją traktować jako metodę. Ma ona oczywiście wszystkie własności funkcji. Jednak jest dowiązana do konkretnego obiektu i może wchodzić w interakcję z jego niepublicznymi składowymi. Jeżeli funkcję zdefiniujemy w obiekcie towarzyszącym, to sprawa ma się podobnie. Z tą różnicą, że będziemy myśleć o niej jak o metodzie statycznej.

Specjalne znaczenie ma funkcja main. Jest ona odpowiednikiem Javowego public static void main, czyli to od niej rozpoczyna się wykonanie programu.

Listing 2. Funkcja main

fun main(args: Array<String>) {

    println("Hello world!")

}

Funkcje zdefiniowane

Na listingu 2, użyliśmy funkcji println. Kotlin domyślnie dostarcza wielu funkcji, które należą do tych najczęściej używanych. Przykładowo, jeżeli chcemy utworzyć sekwencję, coś zrobić z każdym z elementów, a na koniec wypisać OK tyle razy ile było elementów, napiszemy:

Listing 3. Funkcje zdefiniowane

fun main(args: Array<String>) {

    sequenceOf(1, 2, 3, 4, 5, 6).map { TODO("Do something") }.forEach { e -> println("OK!") }

} 

Ciekawą funkcją jest TODO, która zwróci wyjątek NotImplementedError. Piękna sprawa. Szczególnie w połączeniu z TDD, gdzie jesteśmy w stanie śledzić czy testy padły, bo coś zostało źle napisane, czy też nie napisano tego w ogóle. Funkcja sequenceOf pozwala na zbudowanie sekwencji. Podobne funkcje są dostępne dla list, map, setów. Inną ciekawą funkcją jest synchronized, która zakłada blokadę na czas wykonania:

Listing 4. Przykład wykorzystania synchronized

fun main(args: Array<String>) {

    var value = 1;

    synchronized(value, {println(value++)} )

    println(value)

}

ctrl + spacja twoim przyjacielem.

Definiowanie funkcji

By zdefiniować funkcję, która nie jest składową klasy, w pliku piszemy:

Listing 5. Definiowanie funkcji

fun mojaFunkcja(){}

W tym przypadku funkcja nic nie robi. Zwraca jednak obiekt typu Unit. Typ ten ma tylko jedną wartość – Unit. Nie trzeba też podawać explicite, że będzie zwracany. Jeżeli już jesteśmy przy typie zwracanym, to warto pamiętać, iż musimy go podać:

Listing 6. Typ zwracany jest obowiązkowy (za wyjątkiem Unit)

fun mojaFunkcja(): String {
    return ""
}

Oczywiście funkcje mogą też przyjmować parametry. Kotlin dostarcza kilku przydatnych elementów w swojej składni, które pozwalają na wygodniejsze przekazywanie parametrów:

Listing 7. Parametry funkcji – wartości domyślne i parametry nazwane

fun main(args: Array<String>) {

    mojaFunkcja(toUpper = true, str = "make me up")
    mojaFunkcja("don't make me up")

}

fun mojaFunkcja(str:String, toUpper:Boolean = false): String {

    when{
        toUpper -> return str.toUpperCase();
        else -> return str;
    }
}

Parametry mają nazwy, więc w wywołaniu możemy je zamienić kolejnością (jeżeli ktoś tego potrzebuje). Drugi parametr, toUpper, ma podaną wartość domyślną, która zostanie wykorzystana, jeżeli parametru nie podamy. Jest to przydatne rozwiązanie, gdy chcemy np. stworzyć funkcję budującą konfigurację:

Listing 8. Wykorzystanie parametrów domyślnych

data class H2Connection(val username: String, val password: Array<String>, val host: String)

fun createConnection(username: String = "sa", password: Array<String> = arrayOf(""), host: String = "localhost",
                     port: String = ""): H2Connection {
    return H2Connection(username, password, "$host:$port")
}

fun main(args: Array<String>) {

    val con = createConnection(username = "me", host = "some.server")

    println(con)

}

Notacja infixowa

Na początku wspomniałem, że funkcje zdefiniowane w klasach można traktować jako metody. W szczególnych przypadkach można jednak użyć tzw. notacji infixowej w stosunku do tych funkcji (oraz w stosunku do rozszerzeń). Przykładowo:

Listing 8. Notacja infixowa dla metody

data class H2Connection(val username: String, val password: Array<String>, val host: String) {

    infix fun join(pool: Pool) {
       //... 
    }
}

//----------- albo wykorzystując rozszerzenia
infix fun H2Connection.join(pool: Pool) {
       //... 
}
//-----------

fun main(args: Array<String>) {

    val pool = Pool("extPool")
    val con = createConnection(username = "me", host = "some.server")

    con join pool;

}

Będzie poprawnym programem, a con join pool jest równoważnym zapisem con.join(pool). Jest to taki lukier składniowy, który pozwala budować proste „zdanka”.

Podsumowanie

Funkcje jak funkcje. Nie ma w nich nic ciekawego. Przynajmniej na podstawowym poziomie. Jednak są one przydatnym narzędziem, które może ułatwić tworzenie konstrukcji, które w Javie wymagały kombinowania.

Bez poprawnej matury z informatyki nie będzie dobrych systemów publicznych

Maj 18th, 2016

Miało być o czymś innym, ale będzie o maturze. Wczoraj była ta z informatyki. I było nie za dobrze… Generalnie można powiedzieć, że gównoburza i w ogóle, ale po pobraniu plików ze strony CKE chyba jednak nie do końca.

Dane w plikach zawierają błędy, choć według oficjalnych założeń nie powinny.

Wnioski

Jeżeli państwo nie jest w stanie przygotować poprawnych danych na egzamin maturalny, to nie można wymagać od państwa, że będzie w stanie sprawdzić, czy zakupiony produkt albo usługa informatyczna jest OK. Kupowane produkty i usługi są znacznie bardziej skomplikowane niż zadanie weryfikacji danych:

Listing 1. Weryfikacja poprawności danych – ręcznie przez porównanie wypisanych liczb

cat dane_6_2.txt | wc -l && cat dane_6_2.txt | grep [0-9]| wc -l

Listing 2. Weryfikacja poprawności danych – automatycznie przez komunikat

#!/bin/bash

ALL_LINES=`cat dane_6_2.txt | wc -l` 
VALID_LINES=`cat dane_6_2.txt | grep "[A-Z]+ [0-9]{0,4}"| wc -l`

if [ $ALL_LINES != $VALID_LINES ]
then
   echo "DANE NIEPOPRAWNE"
fi

Na ten problem nie pomoże zatrudnienie fachowców na pozycjach managerskich, bo oni nie mają zazwyczaj od lat styczności z kodem i pewne rzeczy im umykają. Jakoś nie wyobrażam sobie typowego menadżera, który robi CRki.

Jak zaradzić temu problemowi?

Rozwiązanie jest jak zwykle przy tego typu problemach, wielotorowe.

Upublicznić kod

Po pierwsze należy upublicznić kod źródłowy projektów realizowanych dla instytucji państwowych. To posunięcie jest tak naprawdę piekłem dla słabych programistów. Pamiętacie zapewne sprawę kalkulatora dla PKW. Skopanie pani Agnieszki za to, że jest słaba w programowanie, było zapewne uroczą krotochwilą dla wielu osób. Chętnie bym spędził jeden czy dwa wieczory w tygodniu, by toczyć bekę z programistów ePUAPu.

Na serio. Publiczny kod źródłowy uruchamia mechanizm jego weryfikacji przez wielu LOSOWYCH programistów. Ta losowość jest oczywiście nie do końca taka, jak myślimy. Jeżeli ktoś ma ochotę na przeglądanie kodu na githubie, zgłaszanie błędów czy tworzenie poprawek, to jest zapewne takim typem programisty, o którym możemy powiedzieć, że jest rzemieślnikiem (recenzja książki o SC za niedługo). Weryfikacja obejmuje też różne aspekty. Począwszy od jakości kodu, poprzez jego architekturę, a kończąc na bezpieczeństwie. Tu spotkałem się z argumentem, że „przyjdą blackhaty i będą crackować”… No publicznej części kodu Linuxa jakoś mało kto crakcuje. Jak już trafiają się 0day to w kodzie własnościowym. Ponieważ poza blackhatamiW są też whitehatyW i greyhatyW, którzy po znalezieniu błędu go zgłoszą (czasami wraz z poprawką).

Zmienić podejście do IT

Po drugie musi nastąpić zmiana podejścia do technologii na poziomie systemu reprezentowanego przez państwo. Dziś jest ona często traktowana jako zło konieczne, w dodatku drogie. Trzeba natychmiast skończyć z akcjami typu „wydrukuj wniosek złożony przez internet”, czy też „przesłanie emailem się nie liczy”. To wymaga jednak wdrożenia systemu, który zapewniałby bezpieczeństwo – państwowy system podpisu cyfrowego. Skoro mamy mieć dowód biometryczny, to chyba znajdzie się tam miejsce na certyfikat i klucz?

BTW, na rewersie każdego dowodu na dole jest zestaw znaczków pozwalających na automatyczne wprowadzanie danych. Czy ktoś kiedyś z tego korzystał?

Po trzecie trzeba też w końcu zrozumieć, że państwa nie stać na chujnie. Niestety w państwowym IT, poza bardzo wąską grupką ludzi z misją pracują osoby mierne. Parafrazując minister Bieńkowską „złodzieje albo idioci”. Nie widzę innego wytłumaczenia dla podjęcia pracy za pieniądze znacznie poniżej rynkowych. I wiem, co mówię, bo „pierwszy etat” był na państwowym garnuszku.

Wykorzystać potencjał naukowy

Ostatnim elementem jest zaangażowanie środowisk akademickich w proces informatyzacji. Jednak nie na poziomie wymyślania oderwanych od rzeczywistości koncepcji. Uczelnie publiczne mają odpowiednie zaplecze naukowe, by opracowywać rozwiązania, które mogą być następnie wdrażane. Bardzo podoba mi się model, w jakim pracuje NASA. Agencja opracowuje pewną część technologii, prowadząc badania podstawowe. Następnie ogłasza konkurs na praktyczną implementację rozwiązania. Firmy prywatne traktują te konkursy jako możliwość wypromowania własnego produktu. To, że poniosą straty na kontrakcie z NASA, nie jest istotne, bo zarobią na późniejszych wdrożeniach na rynku cywilnym i na innych kontraktach z państwem, które wykorzystują opracowane na potrzeby konkursu technologie.
Uczelnie mogą poza badaniami podstawowymi prowadzić ocenę przydatności poszczególnych implementacji i wdrażać je w formie programów pilotażowych. Oczywiście z zachowaniem otwartości.

Jeszcze o maturze

Oczywiście każdy prosty, czteropunktowy plan rozwiązania skomplikowanego problemu jest słaby. Jednak nie chcę, by powyższe rozwiązanie było traktowane jak gotowiec. Mam świadomość, że za tym prostym opisem kryje się wiele problemów. Techniczne stanowią tylko niewielką część. Znacznie poważniejsze są te prawne czy społeczne. Bardzo ciężko jest zmienić prawo tak, by pasowało do wizji otwartych projektów. Jeszcze ciężej jest zmienić ludzką mentalność, by zaakceptowała prymat maszyny.

Nie doczepiłem się jeszcze do treści zadań. I nie doczepię się, bo nie ma do czego. Matura ma sprawdzać wiedzę z całego okresu nauki. Ciężko też od licealisty wymagać znajomości zaawansowanych technik programistycznych. Same zadania, te nieszczęsne liczby skojarzone, nie były takie złe. Podejrzewam, że nie jedno kata z TDD czy innego programistycznego BSDM mogło, by je wykorzystać.


Translate »