Explain Like I'm 5 - czyli wyjaśnij mi to jak pięciolatkowi [1].
Haskell ma opinię języka trudnego i skomplikowanego, dlatego zaczniemy od kursu, nawet jeśli nie dla pięciolatków, to dla uczniów szkoły podstawowej (testowane z powodzeniem na 9-latkach).
[1] https://www.reddit.com/r/explainlikeimfive/
CodeWorld jest edukacyjnym środowiskiem programistycznym dostępnym w przeglądarce internetowej. Przy użyciu prostego modelu matematycznego dla figur i przekształceń, pozwala tworzyć rysunki, animacje, a nawet gry.
program = drawingOf(codeWorldLogo)
Program jest zbiorem (kolejność nie ma znaczenia) definicji. Na przykład:
program = drawingOf(wheel)
wheel = circle(2)
NB to jest kompletny program - wypróbuj go!
Wykonanie programu w środowisku CodeWorld zaczyna się od definicji program
(w "dorosłym" Haskellu wykonanie zaczyna się od definicji main
, ale zasada jest ta sama).
📝 Wypróbuj, z różnymi wartościami:
circle(8)
circle(0.5)
solidCircle(5)
rectangle(4,8)
solidRectangle(8,4)
lettering("W przedszkolu naszym nie jest źle")
❗ Zapisuj rozwiązania ćwiczeń w pliku tekstowym ‒ będzie potrzebny później.
Kombinację figur możemy stworzyć przy użyciu operatora &
:
program = drawingOf(design)
design = solidRectangle(4, 0.4)
& solidCircle(1.2)
& circle(2)
Czasem warto przy tym nazwać części:
program = drawingOf(design)
design = slot & middle & outside
slot = solidRectangle(4, 0.4)
middle = solidCircle(1.2)
outside = circle(2)
program = drawingOf(redWheel)
redWheel = colored(wheel, red)
wheel = solidCircle(4)
📝 Dodaj do powyższego obrazu obramowanie, tak, aby całość przypominała flagę Japonii.
program = drawingOf(tree)
tree = colored(leaves, green) & colored(trunk, brown)
leaves = sector(0, 180, 4)
trunk = solidRectangle(1, 4)
Kolory można modyfikować przy pomocy funkcji dark
, light
, translucent
. Wypróbuj je i przeczytaj o nich w dokumentacji.
program = drawingOf(overlap)
overlap = square & disk
square = colored (solidRectangle(5, 5), translucent(blue))
disk = colored(solidCircle(3), green)
📝 Sprawdź co się stanie, jeśli zmienimy definicję overlap
na disk & square
.
translated(obraz, x, y)
daje obraz przesunięty o x
w prawo i y
w górę, np:
program = drawingOf(forest)
forest = translated(tree, -5, 5)
& translated(tree, 0, 0)
& translated(tree, 5,-5)
tree = colored(leaves, green) & colored(trunk, brown)
leaves = sector(0, 180, 4)
trunk = solidRectangle(1, 4)
📝 narysuj sygnalizator drogowy ('światła' ‒ zielone i czerwone kółka wewnątrz prostokąta)
📝 narysuj szachownicę (to wymaga pewnego sprytu, za chwilę zobaczymy jak to zrobić sprawniej).
rotated(obraz, stopnie)
program = drawingOf(diamond)
diamond = rotated(square, 45)
square = solidRectangle(4, 4)
scaled(obraz, poziomo, pionowo)
program = drawingOf(oval)
oval = scaled(base, 2, 0.5)
base = solidCircle(4)
Jeżeli obie skale są równe, możemy użyć funkcji dilated
:
dilated(obraz, s) = scaled(obraz, s,s)
📝 Narysuj symbol atomu (koło jako jądro i elipsy jako orbity elektronów).
Z prawej strony definicji (po znaku =
) umieszczamy wyrażenie. Użycie definiowanej nazwy jest równoważne użyciu tego wyrażenia.
Nazywamy to przejrzystością odwołań (referential transparency). Wyrażenia mogą być też argumentami funkcji (i obowiązuje tu podobna zasada).
Przykłady wyrażeń:
4
2+2
circle(2+2)
colored(lettering("Help"), red)
rectangle(1, 4) & circle(2)
Natomiast x=1
nie jest wyrażeniem ‒ jest definicją.
Szczególnym rodzajem wyrażeń są funkcje. Podstawową operacją którą możemy wykonać przy pomocy funkcji jest zastosowanie jej do argumentów, na przykład:
rectangle
jest funkcją. Dostawszy wysokość i szerokość, produkuje obraz (prostokąt).light
jest funkcją. Dostawszy kolor, produkuje (podobny, ale jaśniejszy) kolor;.drawingOf
jest funkcją. Dostawszy obraz, konstruuje program, który rysuje ten obraz.id
jest funkcją identycznościową.
Skoro funkcje są wyrażeniami to czy mogą stać po prawej stronie definicji i być argumentami dla funkcji? Ależ tak:
rysuj = drawingOf
id(x) = x
koło = id(circle)
program = rysuj(koło(2))
(tak, można używać polskich liter)
📝 Narysuj 'gwiazdkę' złożoną z 7 wąskich prostokątów (o wymiarach (4, 0.2)
lub podobnych).
[ 1, 2, 3, 4 ]
jest listą liczb,[ circle(2), rectangle(3,5), blank ]
jest listą obrazów.[]
jest listą pustą.
Funkcja pictures
buduje obraz złożony ze wszystkich elementów listy podanej jako argument:
program = drawingOf(allThePictures)
allThePictures = pictures([
solidRectangle(4, 0.4),
solidCircle(1.2),
circle(2)
])
Łatwo domyśleć się, jaką listę oznacza wyrażenie [1..9]
. Podobnie możemy zapisać inne ciągi arytmetyczne,
na przykład [1,3..9]
. Trochę więcej myślenia wymaga [0,2..9]
.
program = drawingOf(target)
target = pictures([ circle(r) | r <- [1..5] ])
Wyrażenie [ circle(r) | r <- [1..5] ]
nazywamy ‒ nawiązując do aksjomatu wycinania w teorii mnogości ‒ wycinanką (list comprehension) ‒ skojarzenie: .
Wartość tego wyrażenia jest taka sama jak [ circle(1), circle(2), circle(3), circle(4), circle(5) ]
.
❓ Jak myślisz, co oznacza wyrażenie [ circle(r) | r <- [1..5], even r ]
?
Możemy również oprzeć wycinankę na kilku listach źródłowych:
program = drawingOf(grid)
grid = pictures([ translated(circle(1/2), x, y)
| x <- [-9 .. 9], y <- [-9 .. 9] ])
📝 Napisz krótszy program rysujący gwiazdkę przy pomocy wycinanki.
Dla ułatwienia możemy narysować siatkę współrzędnych:
program = drawingOf(coordinatePlane)
Punkty reprezentowane są jako pary współrzędnych ‒ na przykład (5,5)
.
Łamaną mozemy skonstruować przy pomocy funkcji polyline
z listą punktów jako argumentem.
program = drawingOf(zigzag)
zigzag = polyline([(-2, 0), (-1, 1), (0, -1), (1, 1), (2, 0)])
Łamaną zamkniętą możemy uzyskać przy pomocy polygon
.
❓ Spróbuj bez uruchamiania powiedzieć, co rysuje poniższy kod:
program = drawingOf(mystery)
mystery = polygon(
[(-3, -4), (0, 5), (3, -4), (-4, 2), (4, 2), (-3, -4)])
📝 Teraz uruchom program. Czy potrafisz narysować to lepiej?
📝 Wypróbuj też funkcje solidPolygon
oraz thickPolygon
. A co z thickCircle
i thickRectangle
?
Każda wartość i wyrażenie ma swój typ. Typy pojawiają się przede wszystkim w dwóch sytuacjach:
- w komunikatach o błędach (spróbuj napisać
program = drawingOf(42)
) - możemy wskazywać typy wyrażeń i definicji
Program
jest typem zmiennejprogram
,Picture
jest typem obrazów,Number
jest typem liczb (w dorosłym Haskellu używamy trochę dokładniejszych typów, jakInt
iDouble
),Color
jest typem kolorów.
Generalnie nazwy (konkretnych) typów zaczynają sie z wielkiej litery, zmienych/funkcji ‒ z małej.
Wskazania typu możemy dokonać przy pomocy ::
na przykład
wheel :: Picture
wheel = solidCircle(size)
size :: Number
size = 4
W większości wypadków deklaracje typów nie są konieczne ‒ kompilator potrafi sam wywnioskować typy. Deklaracje mają jednak co najmniej dwie zalety:
- Są cenną dokumentacją kodu (lepszą niz komentarze ‒ bo sprawdzaną przez kompilator).
- Czasem pozwalają na dokładniejsze komunikaty o błędach.
Jeśli ktoś spodziewał się, że typem list jest List
, to jest w błędzie. Wszystkie elementy listy muszą być tego samego typu.
Typ listy o elementach typu T
oznaczamy przez [T]
.
❓ Jakiego typu jest []
?
program = drawingOf(circles)
circles = pictures[ circle(r) | r <- sizes ]
sizes :: [Number]
sizes = [ 1, 2, 3, 4 ]
A co z punktami? Można powiedzieć, że są typu Point
:
program = drawingOf(polyline[start, end])
start :: Point
start = (0, 0)
end :: Point
end = (2, -4)
Wspomnieliśmy jednak, że punkty są parami liczb. Dokładniej zatem, typem punktu jest (Number, Number)
. Typ Point
jest synonimem tego typu i można ich używać zamiennie.
Krotki mogą mieć różne rozmiary (w tym 0, ale nie 1) i różne typy elementów:
(4, red) :: (Number, Color)
(3, "train", 10, blue) :: (Number, Text, Number, Color)
() :: ()
Oczywiście elementem krotki może też być inna krotka, funkcja, program, ...
Funkcje oczywiście też mają typy, postaci argument -> wynik
, na przykład:
circle :: Number -> Picture
rectangle :: (Number, Number) -> Picture
translated :: (Picture, Number, Number) -> Picture
drawingOf :: Picture -> Program
Do tej pory definiowaliśmy obiekty prostych typów. Możemy oczywiście definiować też wartości typów funkcyjnych. Czasem naturalne wydaje się sparametryzowanie definicji:
program = drawingOf(scene)
scene = house(red)
house :: Color -> Picture
house(roofColor) = colored(roof, roofColor) & solidRectangle(6, 7)
roof :: Picture
roof = translated(thickArc(45, 135, 6, 1), 0, -2)
Parametr funkcji może być dowolnego typu, może to być np. obraz:
program = drawingOf(ringOf(rectangle(1,1)))
ringOf(p) = pictures([
rotated(translated(p, 5, 0), a) | a <- [45, 90 .. 360] ])
Parametrem albo wynikiem funkcji może też być funkcja albo program, ale to nie dla dzieci 😈
if...then...else
program = drawingOf(thing(1) & thing(2))
thing(n) = if n > 1 then rectangle(n, n) else circle(n)
Możemy też powiedzieć, że definicja obowiązuje tylko "pod warunkiem":
program = drawingOf(thing(1) & thing(2) & thing(3))
thing(n)
| n > 2 = rectangle(n, 2)
| n > 1 = rectangle(n, n)
| otherwise = circle(n)
Klasycznym przykładem definicji rekurencyjnej jest silnia:
factorial :: Number -> Number
factorial(0) = 1
factorial(n) = n * factorial(n-1)
program = drawNumber(factorial(5))
drawNumber(n) = drawingOf(lettering(printed(n)))
Podobnie jak wcześniej, równanie dla factorial(n)
będzie wykorzystane tylko gdy n
różne od 0.
W grafice klasycznym przykładem rekurencji są fraktale:
program = drawingOf(fractal(10))
fractal :: Number -> Picture
fractal(0) = stem
fractal(n) = stem
& translated(part, 0, 5)
& translated(part, 0, -5)
where part = rotated(scaled(fractal(n-1), 2/3, 2/3), 90)
stem = polyline([(0, -10), (0, 10)])
Albo zobacz
program = drawingOf(forest)
forest = stepForest (5)
stepForest(0) = tree
stepForest(n) = tree
& translated(sub, -5, 5)
& translated(sub, 5,-5)
& translated(sub, 5, 5)
& translated(sub, -5,-5)
where sub = dilated(stepForest(n-1), 1/2)
tree = colored(leaves, green) & colored(trunk, brown)
leaves = sector(0, 180, 2)
trunk = solidRectangle(1, 3)
📝 Narysuj inne fraktale ‒ dywan Sierpińskiego, płatek Kocha, ...
Animacja jest funkcją typu Number -> Picture
określającą jaki obraz wyświetlić w danej chwili czasu.
Czas jest mierzony w sekundach od uruchomienia programu.
program = animationOf(propeller)
propeller :: Number -> Picture
propeller(t) = rotated(solidRectangle(10, 1), 60 * t)
Przykład animacji używającej translacji, rotacji i zakresu ‒ zależnych od czasu:
program = animationOf(wheels)
wheels(t) = pictures([
translated(rotated(tire, -60 * t), t - 10, y)
| y <- [0, 2 .. t]])
tire = circle(1) & solidRectangle(0.1, 2)
📝 Napisz swoją animację ‒ wahadło, odbijająca się piłka, ...
📝 Napisz animację pokazującą w kilkusekundowych odstępach Twoje rozwiązania poprzednich ćwiczeń (oprócz animacji).
Na tych zajęciach będziemy wykorzystywać GitHub. Jeśli jeszcze nia masz konta ‒ załóż.
Materiały są dostepne w repozytorium https://github.com/mbenke/jnp3-haskell/
(dostęp możliwy bez zakładania konta, ale konto przyda się za chwilę).
Jeśli ktoś nie zna jeszcze github, może go tu przećwiczyć: https://classroom.github.com/a/OLSr53RZ
W notatkach mogą znaleźć się błędy, takie jak literówki (niektóre umyślne). Jesli chcesz ulepszyć notatki, wykonaj fork tego repo na swoim koncie, popraw jakiś błąd (albo zaproponuj ulepszenie) i zgłoś pull request.
Zadania należy oddawać poprzez GitHub Classroom. Rozwiązania należy umieszczać w osobnej gałęzi (branch) a w celu oddania stworzyć pull request i oznaczyć prowadzącego (użytkownik mbenke
) jako recenzenta (reviewer).
Na rozgrzewkę, programy z dzisiejszych zajęć należy oddać poprzez link https://classroom.github.com/a/STA6h1TG (termin: 11.10 godz. 18:00).
Niektóre przyklady i opisy pochodzą z dokumentacji CodeWorld: https://code.world/doc.html?shelf=help/codeworld.shelf
CodeWorld jest dostępny na licencji Apache: https://github.com/google/codeworld/blob/master/LICENSE.