środa, 1 czerwca 2016

Czytelność typów a procent prosty

"Kot rozszerza Animala" - tego typu przykłady są zazwyczaj tłem do nauki programowania. Czy to pomaga? Niedawno ktoś gdzieś podrzucił taki obrazek "Stefan mamy zlecenie na sklep ze zwierzętami - w końcu!! w końcu szkolenia na coś się przydadzą - Cat extends Animal ..."

Czasem trudno zrozumieć siłę pewnych mechanizmów programowania gdy przykłady użyte do ilustracji tychże mechanizmów są tak słabe jak polskie seriale komediowe. Ok ale co w zamian? Otóż można upiec dwie pieczenie na jednym ogniu i zarówno nauczyć się nie tylko jak ładnie programować rzeczy ale równocześnie a nawet bardziej jak ładnie programować przydatne rzeczy. A co jest bardzo przydatne? Bardzo przydatną wiedzą jest wiedza jak działa piniondz.

Nie tylko temat sam w sobie jest ciekawy ale podejście ludzi w jego kierunku jest ciekawe również : Tutaj jest Link do projektu reformy monetarnej w Islandii . O ile mi wiadomo Islandia została najbardziej wydymana na kryzysie i tam też ludzie są zdesperowani do znajdywania odpowiedzi na wiele ekonomicznych pytań - i ważne jest jak oni to robią. Dokument w linku ma 110 stron i to nie jest Lorem Ipsum, znajdują się tam wykresy, uzasadnienia, wykresy i uzasadnienia i tka dalej.

Dla odmiany co chwila na fejsbuku ktoś "udowadnia" (zazwyczaj swoje) tezy ekonomiczne demotywatorami, które zazwyczaj składają się z :
element 1) śmieszny obrazek
element 2) śmieszny komentarz do obrazka

I to w zasadzie tyle. (zazwyczaj każdy najpierw tworzy sobie tezę a później znajduje pasujące doń dowody to już inna kwestia...)

W każdym razie temat wydaje się dobry i rozwiązuje problem wymyślania ciekawych przykładów na warsztaty czy choćby do tego bloga. Na drodze pojawił się jednak pewien problem - otóż sam mam na ten temat niewiele pojęcia...

Rachunek odsetek prostych

Podświadomym zbiegiem okoliczności odkurzyłem tę oto książkę (młodsi czytelnicy mogą nie poznawać tego przedmiotu ale jest to popularna swego czasu tzw "papierowa książka") :

Która po nawet i ciekawym wstępie otwiera wzorem na liczenie odsetek prostych. I jest to już na tyle interesująca rzecz, że można przy jej okazji zbadać dosyć ciekawą technikę "ucztelniania kodu" dostępną w tzw. nowszych językach.

  • Ko - kapitał początkowy
  • r - stopa odsetek w skali jednostki czasu (dni, miesiące,lata)
  • T - liczba dni w jednostce czasu
  • t - ile dni kapitał pracował

Wzór jednocześnie jest prosty ale w kontekście jego implementacji może budzić masę kontrowersji i prowadzić do wielogodzinnych dyskusji o czytelności kodu (do wycieńczenia).

Implementacja

Zerknijmy na coś takiego :

def odsetki(kapital:Double,stopaOdsetek:Double,czas:Int,iloscDniWJednostce:Int)=
    kapital/100 * stopaOdsetek/iloscDniWJednostce * czas

Poza brakującym scaladokiem, który by tłumaczył czym dokładnie są argumenty reszta wygląda całkiem prosto i nawet można by to nazwać "prostym jednolinijkowcem". Również przez chwilę będziemy udawać, że operacje finansowe na Double to dobry pomysł aby ułatwić pokaz edukacyjny. Niebezpieczeństwo nadchodzi jednak z innej strony...

Pierwsza rzecz, która nas nie będzie interesować w tym artykule ale jest na tyle ciekawa, że warto o niej wspomnieć to ilość argumentów. Generalnie mało która zasada czytelności kodu ma pokrycie w kilkudziesięciu latach badań psychologicznych. Tutaj można sobie o tym więcej poczytać : Magiczna libcza 7 a aspekt praktyczny jest taki, że jak np. mamy 3 argumenty i każdy będzie wchodził w interakcję z innymi to mamy 2^3 kombinacji czyli "8" a według badań możemy maks 9 niezależnych elementów ogarnąć czyli 4 argumenty to już 2^4 - za dużo. To jest naprawdę ciekawe, że ta zasada ma jakiekolwiek potwierdzenie w badaniach a nie jest po prostu wygodna dla konsultanta żeby zwiększyć sumę na fakturze,

W każdym razie wywołanie :

odsetki(150.0,5.7,63,360)

Czy jesteś w stanie powiedzieć "co jest co?". Pierwszą próbą uczytelnienia kodu może być nazwanie argumentów (bo w scali można a w javie swego czasu pamiętam, że rozwiązaniem tego problemu było stworzenie buildera i konstrukcja w stylu new Domain.withParam1(...).andSomethingElse(...))

odsetki(
      kapital=150.0,
      stopaOdsetek = 5.7,
      czas=63,
      iloscDniWJednostce = 360)

Czyta się lepiej ale tylko czyta niestety. Generalnie poprawność tego wywołania zabezpiecza jedynie percepcja programisty, którą łatwo zachwiać nieprzyjemną sytuacją na drodze, kłótnią z teściową czy brakiem kawy w pracy (problemy pierwszego świata można tutaj mnożyć). Można do pomocy zaprząc kompilator zabezpieczając wywołanie odpowiednimi typami :

class Money(val value:BigDecimal) extends AnyVal
class InterestRate(val r:BigDecimal) extends AnyVal
class Days(val t:Int) extends AnyVal
class DaysInTimePeriod(val amount:Int) extends AnyVal

def interests(money:Money, rate:InterestRate, time:Days, daysinTimePeriod:DaysInTimePeriod)=
    money.value/100 * rate.r/daysinTimePeriod.amount * time.t

AnyVal i Domena

Klasa AnyVal jest tutaj bardzo istotna , ma ciekawą naturę gdyż w źródłach opisuje ją jakieś 100 razy więcej scaladoca aniżeli sama klasa ma kodu źródłowego. Generalnie jest to ukłon od kompilatora, który rozpakuje klasy rozszerzane przez AnyVal i w runtime zamieni jest w prymitywy - czyli w skrócie będzie "szybciej w runtime" ale "wolniej w kompilacji". Zawsze jest coś za coś. Jak ktoś myśli, że coś jest za darmo to jak młoda sarenka łyka wszystkie przekazy populistyczne. W każdym razie...

Wywołanie teraz wygląda jak poniżej :

interests(new Money(BigDecimal(150.0)),new InterestRate(BigDecimal(5.7)),
new Days(63),new DaysInTimePeriod(360))


Teraz jest bezpieczniej ale znowu mniej czytelnie i można by ponazywać parametry ale gdyby, gdyby gdyby możliwe było coś takiego :
interests("150.0" zloty,"5.7" ratePercent,63 days,360 daysPerYear)
Wygląda czytelniej (ocena subiektywna). (w ostatnim parametrze przekazujemy ilość dni w roku bo jest to wartość umowna - czyli tzw. część domeny. Książka twierdzi, ze bardzo często to wartość 360)

AnyVal i Opsy

Maszyneria jest do dyspozycji, wystarczy tylko zrobić odpowiednie "konwersje niejawne" :

 object InterestsImplicits{
    implicit class MoneyOps(val v:String) extends AnyVal{
      def zloty = new Money(BigDecimal(v))
    }

    implicit class InterestRateOps(val v:String) extends AnyVal {
      def ratePercent=new InterestRate(BigDecimal(v))
    }

    implicit class DaysOps(val v:Int) extends AnyVal{
      def days=new Days(v)
    }

    implicit class PeriodOps(val v:Int) extends AnyVal{
      def daysPerYear=new DaysInTimePeriod(360) //to mozna sobie gdzies zapamiętac by nie tworzyc za kazdym razem
    }
  }

I oto jeszcze raz ten kawałek zadziała :
 interests("150.0" zloty,"5.7" ratePercent,63 days,360 daysPerYear)

Jak już ktoś tykał implicitów to wie, że potrafią one budzić kontrowersje bo nie wiadomo co jest co w którym momencie. Tutaj raczej tego problemu nie ma bo wykorzystujemy ten mechanizm tylko i wyłącznie by uzyskać czytelniejsza formę "metod fabrykujących" i zamiast zloty("200.0") mamy "200.0" zloty co czyta się po ludzku i jest moim niezmiernie skromnym zdaniem również poprawną ilustracją użycia tego ficzeru, że nie trzeba kropki pisać.

No dobra a co z tymi dodatkowymi klasami? Ano nic bo ich tak naprawdę nie będzie w runtime przez to, że dziedzicza po anyval (i znowu kosztem jest czas kompilacji). Ba nawet to zastosowanie jest wymienione w oficjalnej dokumentacji http://docs.scala-lang.org/overviews/core/value-classes.html w sekcji Extension methods

Kod wydaje się i czytelny i zabezpieczony a ten ostatni temat "metody rozszerzone" jest dobrą okazją aby zbadać temat od trochę innej strony.

Extension Methods

Poniższy Kod to już język Kotlin a kod wydaje się jakby mniej skomplikowany i prostszy do ogarnięcia (ale zaraz będzie jedno ważne ale):

class Money(val value: BigDecimal)
class InterestRate(val r: BigDecimal)
class Days(val t:Int)
class DaysInTimePeriod(val amount:Int)

//extensions!
fun String.zloty(): Money = Money(BigDecimal(this))
fun String.ratePercent()= InterestRate(BigDecimal(this))
fun Int.days()= Days(this)
fun Int.daysPerYear()= DaysInTimePeriod(360)

interests2("150.0".zloty(), "5.7".ratePercent(), 63.days(), 360.daysPerYear())

Przede wszystkim ten przykład ma pokazać, że to podejście nie jest tylko "scalowe" . Druga kwestia to zrozumienie jak pewne konstrukcje językowe określają jego charakter. Extension Methods (lepiej zostać przy angielskim żeby było wiadomo co jest co) są dużo prostsze do ogarnięcia w Kotlinie aniżeli w Scali gdyż w tej drugiej wymagają znajomości potężniejszego mechanizmu Implicits. AAAALLLLEEE z drugiej strony jak już znamy Implicits możemy zaimplementować inne mechanizmy jak choćby TypeClasses a Extension Method służy... no chyba tylko do Extension Methods. Tak czy inaczej Java nie ma ani jednego ani drugiego a "project lombok" jest uważany w pewnych kręgach za szczyt ku*wa technologii kosmicznej 22 wieku

"Bo kod się częściej czyta..."

Czasem przy okazji emtikonek w sali w stylu "(_+_)" zagorzali wielbiciele nawiasów i średników lubią podkreślać, że kod się częściej czyta niż pisze. I jest to jak najbardziej prawda ale nie jest to żadnym argumentem na rzecz barokowego języka. Chociaż czytelność to rzecz subiektywna i jest wskazane aby team (pol. drużyna) sama sobie ustaliła warianty jakości kodu to jednak ten artykuł mam nadzieję pokazał, że bogatsze języki umożliwiają pisanie czytelniejszego i jednocześnie bezpieczniejszego kodu aniżeli klasyczna Java.

Brak komentarzy:

Prześlij komentarz