Skip to content

Instantly share code, notes, and snippets.

@juvuorin
Last active April 24, 2020 11:06
Show Gist options
  • Save juvuorin/f674d57ca09a8d73b11a2efb6d9b0d1f to your computer and use it in GitHub Desktop.
Save juvuorin/f674d57ca09a8d73b11a2efb6d9b0d1f to your computer and use it in GitHub Desktop.
koiramainenOhjelmointikisa2020
{-
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
@juvuorin
Copy link
Author

juvuorin commented Apr 24, 2020

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment