Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Krzywa możliwości produkcyjnych a tworzenie oprogramowania

Okazuje się, że dylematy trapiące wielu programistów, znane są ekonomistom nie od dziś. Na przykładzie armat i masła P. Samuelson w swoim podręczniku ekonomii tłumaczy pojęcie krzywej możliwości produkcyjnych.

Obrazuje ona wybór, jaki ma społeczeństwo decydując, jakie nakłady przeznaczyć na obronę kraju, a jakie na dobra konsumpcyjne. Im więcej na dobra konsumpcyjne, tym przyjemniej się żyje, ale i zwiększa się ryzyko, że to się skończy z wkroczeniem do kraju wrogich wojsk. Tu powinna być widoczna pierwsza analogia do tworzenia oprogramowania, gdzie można się skupić na szybkim dostarczaniu dóbr w postaci nowych funkcjonalności, ryzykując, że nagle przestanie być przyjemnie na skutek fatalnego błędu na serwerze produkcyjnym.


Nakłady używane do produkowania czegokolwiek dzielą się na trzy kategorie: praca ludzi, ziemia i zasoby naturalne, oraz kapitał. Kapitałem nazywamy różne rzeczy już wytworzone, które można użyć do wytworzenia innych (również niematerialne), np traktory, huty, technologia, ewentualnie kasa, za którą można to wszystko kupić.

Nasuwa się pytanie: ile nakładów przeznaczyć na produkcję teraz, a ile na inwestycje kapitałowe, które sprawią, że w przyszłości będzie można wyprodukować więcej? Oczywiste jest, jaką rolę w projektach IT gra praca ludzi, oraz ziemia i zasoby naturalne (żeby uprawiać niezbędną programiście kawę). Czym jest w tym kontekście kapitał też nietrudno odgadnąć - "technologia", jakość kodu, testy automatyczne, dokumentacja, ciągła integracja, "one-click deployment", itd ... Trudniej określić jaka jest miara wartości na obu osiach wykresu, jak to przełożyć na pieniądze ...

"Working tommorow for better today" Ludzie zaciągają kredyty na mieszkania, a programiści tzw. dług techniczny. Pieniądze na mieszkanie w ramach kredytu "daje" bank i to bank na tym zarabia. U kogo zaciągany jest dług techniczny? Kto na tym zarabia? Zadłużanie się w tym przypadku jest odwrotnością inwestycji kapitałowych i polega na tym, że nakłady niezbędne do utrzymania istniejącego kapitału (np naprawy traktorów, konserwacji maszyn, refaktoryzacja kodu) przeznacza się na bieżącą konsumpcję, przez co w przyszłości będzie można wyprodukować mniej. Chyba, że zaciągnie się kolejny dług, ale tam gdzie zbyt długo narasta dług, tam są bankructwa ...

a tam gdzie bankructwa, tam instrumenty pochodne typu Credit Default Swap, oraz oparte na nich indeksy, oraz opcje na te indeksy ... nie, proszę, nie idźmy tą drogą ...


Extractor story, czyli znów o Scali

W Scali można w ciekawy sposób tworzyć i używać wyrażenia regularne. Na przykład, wyciągnąć z numeru pesel rok, miesiąc i dzień urodzenia można w taki sposób:

val PeselRegex = """(\d\d)(\d\d)(\d\d)\d{5}""".r
val PeselRegex(year, month, day) = "78111015918"
println(s"Birth date: $day-$month-$year")

Druga linia może przypominać zastosowanie funkcji, lub stworzenie instancji klasy przypadku (ang. case class), ale tak jakby na odwrót – i całkiem słusznie. Btw, jeśli dziwi cię wywołanie .r na obiekcie typu String, zajrzyj do mojego postu o konwersjach niejawnych. W podanym przykładzie, gdy pesel będzie niepoprawny zostanie wyrzucony wyjątek. Jeśli chcemy tego uniknąć, możemy użyć dopasowywania wzorców (ang. pattern matching):

 val PeselRegex = """(\d\d)(\d\d)(\d\d)\d{5}""".r

  "78111015918" match {
    case PeselRegex(year, month, day) => println(s"Birth date: $day-$month-$year")
    case _ => println("Invalid pesel")
  }

W tym przykładzie PeselRegex jeszcze bardziej przypomina klasę przypadku. A to dlatego, że PeselRegex, tak samo jak klasy przypadku, jest ekstraktorem. To znaczy, że implementuje metodę unapply (lub unnaplySeq, ale o tym później). Nic nie stoi na przeszkodzie, by tworzyć własne ekstraktory, przykład

  object WeakPeselExtractor {
    def unapply(pesel: String): Option[(String, String, String)] =
      if (pesel.length == 11 && pesel.forall(_.isDigit))
        Some((pesel.substring(0, 2), pesel.substring(2, 4), pesel.substring(4, 6)))
      else None
  }

  "78111015918" match {
    case WeakPeselExtractor(year, month, day) => println(s"Birth date: $day-$month-$year")
    case _ => println("Invalid pesel")
  }

Zastanawiająca może być ilość wartości. W przypadku WeakPeselExtractor spodziewamy się trzech, ale twórcy klasy Regex przecież nie wiedzieli ile wartości programiści będą chcieli uzyskać tworząc wyrażenia regularne. Ale to nie problem ze dzięki wspomnianej wcześniej metodzie unapplySeq. Poniżej implementacja unapplySeq klasy Regex:

  def unapplySeq(s: CharSequence): Option[List[String]] = {
    val m = pattern matcher s
    if (runMatcher(m)) Some((1 to m.groupCount).toList map m.group)
    else None
  }

Dzięki temu uzyskujemy zmienną ilość parametrów i możemy dopasowywać np tak:

  "some string" match {
    case SomeRegex(a) => ???
    case SomeRegex(a,b) => ???
    case SomeRegex(a,b,c @  _*) => ???
    case _ => ???
  }

Więcej na ten temat:


Niejawne konwersje i parametry w Scali

Ostatnio programuję trochę w Scali, więc trzeba by wziąć na tapetę coś z tego języka. Pokażę na przykładach użycie niejawnych konwersji i parametrów.

Niejawne parametry

package conversions

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

object ImplicitsDemo extends App {

  val f = Future("hello")
  println(Await.result(f, 1 second))
}

Są prostsze sposoby w Scali na wypisanie "hello" niż używanie do tego Future, ale gdyby w tego Future opakowane było jakieś bardziej wzniosłe zadanie, np. ściągnięcie tweetów z tagiem scala, to byłby to z życia wzięty przykład użycia niejawnych parametrów. Gdzie tu jest niejawny parametr? Oto sygnatura metody Future.apply:

def apply[T](body: ⇒ T)(implicit executor: ExecutionContext): Future[T]

Dzięki temu, że executor jest parametrem niejawnym, nie trzeba go podawać w Future("hello"). Nie bierze się też znikąd, kod by się nie skompilował bez:

import scala.concurrent.ExecutionContext.Implicits.global

a tam się znajduje:

implicit lazy val global: ExecutionContextExecutor = impl.ExecutionContextImpl.fromExecutor(null: Executor)

Kompilator widząc taki niejawny parametr, szuka zdefiniowanej wartości o odpowiednim typie z modyfikatorem implicit. Jest kilka rzeczy, na które trzeba uważać:

  • implicit dotyczy całej listy parametrów, zatem jeśli funkcja jest zdefiniowana:

     def f(a: String)(implicit b: Int, c: Int) = ???

    to b, c są niejawne.

  • kompilator wyszukuje wartość do wstawienia po typie, więc lepiej używać jakichś typów w miarę specyficznych dla naszego kodu - np zamiast użyć Int, albo String, lepiej opakować to we własny typ.

  • wstawiany "implicit" musi być dostępny jako pojedynczy identyfikator. Czyli, gdyby w przykładzie z Future trzeba zaimportować scala.concurrent.ExecutionContext.Implicits.global, a nie scala.concurrent.ExecutionContext.Implicits, bo kompilator nie wstawi Implicits.global.

Niejawne konwersje

Poniżej test z użyciem spec2 wzięty z szablonu "scala-test" z typesafe activators.

package specs2

import org.junit.runner.RunWith
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner

@RunWith(classOf[JUnitRunner])
class ListSpec extends Specification {
  
  "Calling isEmpty" should {
    "return true for a List with 0 elements" in {
      List().isEmpty must beTrue
    }
    "return false for List with > 0 elements" in {
      List(1, 2, 3).isEmpty must beFalse
    }
  }
}

String nie ma metody should, ale to działa dzięki niejawnym konwersjom, zdefiniowanym w kodzie spec2:

 implicit def described(s: String): Described = new Described(s)
 
  class Described(s: String) {
    def should(fs: =>Fragment) = addFragments(s, fs, "should")

 // reszta pominięta
  }

Kompilator widząc, że String nie ma odpowiedniej metody, szuka implicita, który może skonwertować String w typ, który taką metodę ma (tu: Described). Podobnie jest, gdy funkcja spodziewa się parametru o określonym typie, ale dostaje inny - jeśli jest zdefiniowana odpowiednia niejawna konwersja, to kompilator ją zastosuje.

Źródła:


O spotkaniach wzorowanych na Daily Scrum

Przez niecały rok miałem przyjemność pracować w zwinnym zespole, który z powodzeniem wprowadził Scrum - w całości. Byłem zaskoczony jak bardzo można poprawić sposób pracy nad projektem IT. Zmieniło się też moje podejście do jednej z praktyk Scrumowych: Codzienny Scrum (ang. The Daily Scrum).

Jest to praktyka wyglądająca na najprostszą do wprowadzenia ze wszystkich praktyk Scrum, dlatego często idzie na pierwszy ogień podczas prób uczynienia pracy zespołu zwinniejszą. No cóż, poprawne przeprowadzenie takiego spotkania rzeczywiście nie jest trudne, ale pod warunkiem, że jest osadzone w ramach Scruma. Gdyby jednak próbować robić podobne spotkania w odizolowaniu od reszty praktyk, to wręcz nie da się tego zrobić "zgodnie ze sztuką". Oczywiście, nawet krótkie codzienne spotkania zespołu bez przejmowania się jakimikolwiek zasadami Scruma mogą usprawniać pracę, ale to tylko ułamek potencjalnych korzyści.

Zajrzyjmy do Scrum Guide:

Codzienny Scrum jest spotkaniem dla Zespołu Deweloperskiego, ograniczonym czasowo do piętnastu minut, podczas którego bieżące zadania są synchronizowane i powstaje plan działania na najbliższe 24 godziny. Jest to osiągane poprzez inspekcję prac, które zostały wykonane od ostatniego Codziennego Scruma i prognozowaniu prac, które mogą zostać wykonane przed kolejnym spotkaniem.

Scrum Guide

Codzienny Scrum jest dla Zespołu Deweloperskiego. Nie jest po to, żeby zdać raport menadżerowi, czy jakiejś innej nadzorującej projekt osobie. Wyrazistym antywzorcem jest sytuacja, gdy menadżer przepytuje po kolei każdą osobę w zespole, a ci którzy już są przepytani, lub czekają na swoją kolej, zajmują się innymi rzeczami, bo to o czym mowa nie jest dla nich istotne, albo dlatego, że spotkanie trwa już 40 minut. Codzienny Scrum to przede wszystkim wymiana informacji pomiędzy członkami Zespołu Deweloperskiego i powinna być na tyle konkretna, by każdy każdego słuchał. Trudno uniknąć problemów na tym polu, gdy rzeczywiste role w zespole nie odpowiadają rolom ze Scruma.

Podczas Codziennego Scruma bieżące zadania są synchronizowane i powstaje plan na następne 24 godziny. Zespół decyduje które zadania najlepiej zrobić w ciągu następnego dnia oraz kto konkretnie je zrobi. Gorzej, jeśli nie ma w tej kwesti wiele do gadania, bo zdecydował o tym ktoś inny i jeśli zrobią inaczej to "będzie dym"... Kolejny możliwy problem polega na tym, że zespół nie może zaplanować dobrze pracy, bo potrzebuje do tego pomocy z zewnątrz zespołu.

Pierwsze z pytań Codziennego Scruma brzmi "co zostało zrobione", a nie "co było robione". Nie zrealizuje się jednak takiego podejścia w praktyce, jeśli praca nie jest rozbita na zadania, które da się zrobić w jeden dzień. Do tego trzeba wcześniejszego Planowania Sprintu (ang. Sprint Planning) oraz jeśli nie poświęca się regularnie czasu na Pielęgnowanie Rejestru Produktu (ang. Backlog Refinement). Nawet wtedy rozbicie pracy na takie zadania może być niemożliwe, jeśli dług techniczny jest zbyt duży.

Ponadto, bez Retrospektyw można nawet nie zauważyć, że coś jest nie tak.

Zespół, który chce być zwinny, prędzej czy później natrafia na jakąś Scrumową zasadę, którą w jego przypadku ciężko wdrożyć. Może wtedy ją olać. Może próbować ją wdrożyć "na pałę" i dojść po krótkim czasie do wniosku, że to bez sensu. Prawdziwa poprawa następuje dopiero wtedy, gdy rozpoznaje się kryjący za tym wszystkim problem i podejmuje odpowiednie środki naprawcze.

PS. W zgodzie z najnowszymi trendami na blogach IT dorzucam jakieś zdjęcie: kaczki i gołąb z Parku Łazienkowskiego.



Wrażenia po Scalar Conference

W sobotę w Warszawie odbyło się bardzo ciekawe wydarzenie, w którym miałem przyjemność brać udział: Scalar Conference. Była to pierwsza w Polsce konferencja dla entuzjastów języka Scala. Uważam ją za niezwykle udaną - na duży plus prelekcje, atmosfera spotkania, organizacja... nawet pogoda dopisała.

Trzy prezentacje, które spodobały mi się najbardziej to:

  • "The Dark Side Of Scala" Tomasza Nurkiewicza.
    Konkretna i rzeczowa prezentacja. Uważam, że konferencja bardzo zyskała na tym, że mówiło się na niej również o tym co nie jest fajne w programowaniu w Scali.
  • "Lambda Implementation In Scala 2.11 And Java 8" Grzegorza Kossakowskiego
    Oprócz tematu, dużą zaletą był przejrzysty sposób poprowadzenia prelekcji. Zrozumiałem jak działa invokedynamic, jak różni się reprezentacja wyrażeń lambda w bytecodzie po skompilowaniu kodu w Scali i kodu w Javie, oraz dlaczego ta różnica nie jest taka znacząca jakby mogło się to na pierwszy rzut oka wydawać.
  • "The Tale Of The Glorious Lambdas & The Were-Clusterz (i.e. Scala + Hadoop)" Mateusza Fedoryszaka i Michała Oniszczuka
    Na tej prezentacji prelegenci uchylili rąbka tajemnicy na temat tego co robią w ICM i opowiedzieli nieco o projekcie http://coansys.ceon.pl/. Pokazali różnicę pomiędzy kodem potrzebnym do stworzenia prostego zadania na Hadoop (zliczanie słów) przy pomocy Javy i Scali + Scooby/Scalding.


Aproksymacja liczby e w Clojure

W tym poście pokażę jedną z różnic pomiędzy programowaniem funkcyjnym a imperatywnym, na przykładzie szacowania wartości liczby e.

Odrobina teorii

Do szacowania liczby e wykorzystam jej rozwinięcie w szereg:

Pomiędzy elementami takiego szeregu istnieje zależność, dzięki której można zmniejszyć ilość obliczeń. Wzór rekurencyjny:

Szacowanie będzie polegać na zsumowanie pierwszych n elementów szeregu - im wyższe n, tym lepsze oszacowanie.

Jak to zaimplementować w Javie?


Jest tu kilka zmiennych, pętla for. W każdej iteracji obliczamy kolejny wyraz ciągu, nowe przybliżenie liczby e. Krótki, imperatywny kod.

A jak w Clojure?


W tym kodzie nie ma żadnej pętli, nie ma zmiennych. Są "wiązania" (naturals, e-taylor-serie, first-n-elems), ale bez nich też dałoby się ten kod napisać - wprowadziłem je dla lepszej czytelności, bo dzięki ich nazwom, nawet nie znając clojure, można się domyślić, że są tu 4 kroki: zdefiniowanie liczb naturalnych, kolejnych elementów szeregu, wybranie n pierwszych elementów i zsumowanie ich.

Nasuwać się może pytanie, co to za struktura danych, co przechowuje wszystkie liczby naturalne? Jest to leniwa sekwencja. Jej elementy elementy obliczane są dopiero w momencie użycia. Oznacza to, że może być ona nieskończonej długości, czyli na przykład być ciągiem liczb naturalnych.

Funkcja iterate przyjmuje dwa parametry: jednoargumentową funkcję f i wartość początkową. Generuje ona nieskończoną, leniwą sekwencję o elementach równych:

Czyli (iterate inc 1) wygeneruje nieskończoną leniwą sekwencję liczb naturalnych (inc to f(x)=x+1).

Funkcja reductions przyjmuje dwuargumentową funkcję f, wartość początkową x i sekwencję a. Rezultatem jest leniwa sekwencja. Pierwszy element tej sekwencji to f(x , a0), a kolejne to rezultat zastosowania funkcji f na poprzednim wyniku i kolejnym elemencie sekwencji a. Przykład:

(reductions / 1.0 natural) zwróci sekwencję z kolejnymi elementami rozwinięcia e.

Funkcja take jest najprostsza do zrozumienia ze wszystkich tu wymienionych. (take n s) bierze n elementów z sekwencji s. Jeśli sekwencja s jest leniwa, to te n elementów zostanie w tym momencie obliczone.

Pisałem wcześniej, że można napisać podobny kod, ale bez użycia wiązań. Przykład:

Wnioski

W programowaniu funkcyjnym operuje się na nieco innych abstrakcjach niż w imperatywnym. Implementacja w Javie opierała się modyfikowaniu stanu kilku zmiennych, wykonywanym interacyjnie. W kodzie Clojure zostało zdefiniowanych kilka przekształceń danych w ogólnej postaci (np. jak z liczb naturalnych zrobić rozwinięcie liczby e w szereg), a dopiero później zostały wykonane obliczenia. Moje subiektywne odczucie jest takie, że programując w języku funkcyjnym łatwiej odzwierciedlić w kodzie różne matematyczne zależności.


Jak doskonalić znajomość Clojure? Rozwiązuj zadania z 4clojure.com

Nauka języka funkcyjnego, to coś więcej niż nauka nowego języka programowania. Jak ktoś programuje w C♯, to nie sprawi mu większego problemu napisanie generatora liczb pierwszych w Javie. Inaczej jest przy pierwszym spotkaniu z Clojure, albo ze Scalą. Okazuje się, że zaimplementowanie czegoś prostego, wymaga wysiłku i, że trzeba się przestawić na nieco inny sposób myślenia. Można przeczytać książkę, ale teoria szybko się ulatnia z głowy. By temu zaradzić, trzeba ćwiczyć, rozwiązując zadania.

Zbiór takich zadań znajduje się na 4clojure.com. Do rozwiązania najprostszych wystarczy Repl Online, czyli nie trzeba nawet konfigurować środowiska, żeby zacząć. Zadania mają narastającą trudność, żeby nie zrazić się zbyt trudnymi zadaniami na początek, oraz nie zanudzić zbyt dużą ilością bardzo łatwych. I co najważniejsze, po rozwiązaniu zadania, można przejrzeć kod innych (trzeba wcześniej wybrać paru userów). Ja się przez to dużo nauczyłem i nie raz byłem pod wrażeniem innych rozwiązań.

Ponadto portal został napisany w Clojure, a jego kod znajduje się na Github'ie. I nie jest tego kodu dużo, podejrzewam, że gdyby napisać tego typu portal w Javie, było by go więcej...


"Mity i problemy w Agile" - recenzja książki Wiktora Żołnowskiego

Najgorszym wrogiem wiedzy nie jest niewiedza, ale iluzja wiedzy - Stephen Hawking

Ten cytat został przytoczony na samym początku książki "Mity i problemy w Agile", co według mnie jest bardzo trafne. Obecnie chyba mało który programista nigdy nie słyszał nic o Scrumie, ani o Agile. Ja jeszcze na początku poprzedniego roku byłem przekonany, że wiem dużo na temat metody SCRUM. Przecież przeczytałem tyle postów na blogach na ten temat, obejrzałem kilka krótkich filmów na youtube, a dawno temu nawet przejrzałem Scrum Guide... Profesjonalne szkolenie Tomka Włodarka wyleczyło mnie z tej iluzji wiedzy. Przez ostatnie pół roku zdobyłem trochę praktycznego doświadczenia w związku z pracą podług metody Scrum, ale po przeczytaniu "Mity i problemy w Agile", zdałem sobie sprawę, że nadal na wiele problemów nie zwracałem uwagi. Dlatego twierdzę, że książka podejmuje ważny temat.

Książkę w wersji elektronicznej można kupić przez portal leanpub.com nawet za 3.60 USD. Nie jest długa, ma niecałe 130 stron i przeczytałem ją w dwa wieczory. Wiktor Żołnowski prowadzi bloga blog.testowka.pl na którym można przeczytać więcej o jego działalności.