Last active
April 24, 2020 11:06
-
-
Save juvuorin/f674d57ca09a8d73b11a2efb6d9b0d1f to your computer and use it in GitHub Desktop.
koiramainenOhjelmointikisa2020
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{- | |
Sekalaisia huomioita: | |
1. Ohjelma käyttää merkkijonojen käsittelyyn Text-tyyppiä (Data.Text) Stringien sijaan. | |
Näin pienessä ohjelmassa String olisi mennyt ihan ok, mutta en ollut ennen käyttänyt | |
Text-tyyppiä, joten päätin kokeilla. | |
2. Koska tehtävänannossa ei otettu kantaa siihen miten ohjelma saa syötteensä, päätin | |
lukea arvosanat tiedostosta, että edes jotain dataa tulee ohjelmalle I/O-kerroksen kautta. | |
Tiedostosta luku olettaa, että maailma on täydellinen - | |
tiedoston grades.txt pitää löytyä ja sen on oltava oikein muotoiltu: | |
head-funktiota käytetään täysin siekailematta, ja yhtä pokerilla castataan teksti numeroksi | |
read-kutsulla. Arvosanojan osumista välille 4-10 ei tarkisteta. Jos ohjelma käyttäisi | |
useampaa kuin yhtä moduulia, voisi arvosanasta luoda omassa moduulissa newtypellä tyypin, josta | |
exportattaisiin näkyväksi ainoastaan ns. smart constructor "mkGrade" tms., joka ei suostuisi | |
luomaan virheellistä arvosanaa. Mutta nyt mennään näin. | |
Oppiaineet ja niiden kategoriat määritellään vakiona suoraan koodissa. Teeskennellään, että | |
nekin ovat peräisin jostain "oikeasta maailmasta". | |
3. sumOfCategoryGrades-funktion käyttämät paikalliset funktiot | |
categorySubjects ja subjectsTotalGrade eivät sisällä tyyppimäärittelyjä, koska ilmeisesti | |
paikallisille määrittelyille ei ole tapana näitä niin tiukasti aina lisätä. | |
Funktioiden tyypit ovat | |
categorySubjects :: CategoryName -> [SubjectName] | |
subjectsTotalGrade :: [SubjectName] -> Int | |
categorySubjects kävi läpi pienen evoluution. | |
Määrittelin sen alunperin näin: | |
map subjectName $ filter (\s -> categoryName s == category) subjects | |
mutta sitten tuli mieleen, että voisin hyödyntää List-tyypin monad-instanssia: | |
filter (\s -> categoryName s == category) subjects >>= return . subjectName | |
Valitsin aluksi tämän jälkimmäisen tavan, koska se tuntui jotenkin luontevammalta. Hetken kuluttua | |
ymmärsin syyksi sen, että sehän muistuttaa kovasti vastaavan asian kirjoittamista C#:ssa LINQ:lla, | |
jota käytän töissä lähes päivittäin: | |
subjects.Where(s => s.CategoryName == categoryName).Select(s => s.SubjectName); | |
Luonteva jatko oli kokeilla, mitä seuraa jos syötän listan bindin ruoaksi heti kättelyssä: | |
subjects >>= \s -> if categoryName s == category then [subjectName s] else [] >>= return . subjectName | |
Tämä oli selvästi väärä suunta. Mutta samalla lamppu taas syttyi, ja tajusin, että olin | |
oikeastaan tekemässä tätä: | |
[subjectName s | s <- subjects, categoryName s == category] | |
Näinhän sen pitää olla! Tähän muotoon jätin toteutuksen. Asiat voi tehdä monella tavalla ja Haskellin | |
hauskimpia puolia on tähän asti ollut etsiä ja löytää eri tapoja. | |
-- | |
Tehtävä oli mukavan suppea (nykyinen elämäntilanne ei salli kovin massiivisia koodausrupeamia, aikaa on | |
iltaisin tunti-pari) mutta antoi silti mahdollisuuden kokeilla ja oivaltaa uusia asioita. | |
Toki pääsin myös vahvistamaan jo entuudestaan tuttuja asioita - eikö kertaus ollutkin jotain sukua opnnoille? | |
-} | |
{-# LANGUAGE OverloadedStrings #-} | |
module Main where | |
import qualified Data.Text as T | |
import Control.Monad (forM_) | |
-- Muutama tyyppialias mukavammin luettavan koodin ja sekaannusten välttämiseksi | |
type SubjectName = T.Text | |
type CategoryName = T.Text | |
type SubjectGrade = (SubjectName, Int) | |
type CategoryGrade = (CategoryName, Int) | |
categories :: [CategoryName] | |
categories = ["Metsästys", "Pihatyöt", "Muut"] | |
data Subject = Subject | |
{ subjectName :: SubjectName | |
, categoryName :: CategoryName | |
} deriving Show | |
subjects :: [Subject] | |
subjects = | |
[ Subject { subjectName = "Pupun jäljestys", categoryName = "Metsästys" } | |
, Subject { subjectName = "Hirven jäljestys", categoryName = "Metsästys" } | |
, Subject { subjectName = "Linnun noutaminen", categoryName = "Metsästys" } | |
, Subject { subjectName = "Lumen pöllyytys", categoryName = "Pihatyöt" } | |
, Subject { subjectName = "Kukkapenkkien kaivaminen", categoryName = "Pihatyöt" } | |
, Subject { subjectName = "Parvekkeen vahtiminen", categoryName = "Muut" } | |
, Subject { subjectName = "Piilotetun luun löytäminen", categoryName = "Muut" } | |
, Subject { subjectName = "Oman hännän jahtaaminen", categoryName = "Muut" } | |
, Subject { subjectName = "Kuun ulvonta", categoryName = "Muut" } | |
] | |
main :: IO () | |
main = do | |
grades <- fmap (parseSubjectGrades . T.pack) (readFile "grades.txt") | |
{- | |
Tämän alla olevan sumOfCategoryGrades-kutsun olisi voinut lisätä äskeiseeen kompositioon, | |
mutta ehkä tämä on mukavampi omana kutsunaan. Näin tuo ensimmäinen rivi | |
liittyy puhtaasti datan noutamiseen ja parsimiseen ja varsinainen | |
ohjelmalogiikkaa suoritetaan omana kutsunaan. | |
-} | |
let categoryGradeSums = sumOfCategoryGrades grades | |
-- Tänä menisi mapM_:llä yhtä hyvin, mutta, nyt tehdään näin: | |
forM_ categoryGradeSums | |
(\x -> | |
let categoryName = (T.unpack . fst) | |
grade = (show . snd) | |
in | |
putStrLn $ categoryName x ++ " " ++ grade x) | |
parseSubjectGrades :: T.Text -> [SubjectGrade] | |
parseSubjectGrades gradeData = | |
let grades = map (T.split (\c -> c == ',')) (T.lines gradeData) | |
in | |
[(head line, read . T.unpack $ last line) | line <- grades] | |
sumOfCategoryGrades :: [SubjectGrade] -> [CategoryGrade] | |
sumOfCategoryGrades grades = | |
[(c, subjectsTotalGrade $ categorySubjects c) | c <- categories] | |
where | |
categorySubjects category = | |
[subjectName s | s <- subjects, categoryName s == category] | |
subjectsTotalGrade subjectNames = | |
sum . map snd $ filter ((`elem` subjectNames) . fst) grades |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Näppärä ratkaisu, jossa data luetaan tiedostosta ja tekijä on myös ottanut kantaa ohjelman "riskipaikkoihin".
Tiedon "pakkocastaaminen" on ihan ok, koska siihen on otettu kantaa. Tässä tiedon voidaan olettaa olevan kuranttia.
Kommentti head-funktion "siekailemattomasta" käytöstä kielii vähintään kohtuullisesta ellei peräti hyvästä virheenhallintataidosta Haskell-kielessä. Komposition käyttö siellä, missä edellisen ja jälkimmäisen funktion evaluointi tuottaa yhteensopivaa (esim. sum . map, show . snd jne.) dataa, osoittaa "päällekäyvää kätevyyttä" asiassa.
Tekijä kokeillut Text tietotyyppiä, mikä on erittäin hyvä juttu. Sitä kuitenkin käytetään useissa Haskell-kielen funktioissa, String tyypin sijaan.
Tekijän taitoja kuvaa paitsi käsitteidn vertailu LINQ:n ja Haskellin välillä, myös moduulirakenteen pohtiminen ja funktioiden (rajapinnan) tekeminen näkyväksi moduulista, niin, että sitä käytettäisiin toisesta moduulista.
Näppärä ratkaisu tämäkin.
Tsemppiä ohjelmointihommiin!