Röda (lausutaan rööda) on uusin ohjelmointikieleni, jossa on monia edistyksellisiä ominaisuuksia. Suurin inspiraatio kielelle on ollut Bourne shell, mutta olen ottanut mukaan toteuttamiskelpoisia ajatuksia myös muista skriptikielistä. Yleisesti ottaen Röda on hyvin erilainen kuin useimmat muut kielet.
Ohjelmointikieli | Java 8 |
Viimeisin versio | 0.13-alpha |
Kotisivu | https://github.com/fergusq/roda |
Latauslinkki | roda-0.13-alpha.jar |
Alla on esimerkki rödaskriptistä. Se on yksinkertainen ohjelma, joka lukee tiedoston ja tulostaa sen rivit rivinumeroiden kanssa.
#!/usr/bin/röda function main(file) { linenum := 0 readLines(file) | for line do print(linenum.." "..line) linenum ++ done }
Eräs tärkeimmistä Rödan ominaisuuksista on virrat, joiden avulla funktiot voivat palauttaa useita arvoja. Jokaisella funktiolla on sisääntulovirta ja ulostulovirta, joista voi lukea arvoja. Putkittamisen avulla monta funktiota voidaan kytkeä monisäikeisesti toisiinsa käsittelemään dataa ketjussa. Yllä olevassa esimerkissä readLines
työntää ulostulovirtaansa tiedoston rivit, josta ne luetaan for
-silmukalla.
Seuraavassa pätkässä haetaan Googlen kuvahaulla kuvia netistä:
sana := "<hakusana>" user_agent := "Links (2.7; Linux 3.5.0-17-generic x86_64; GNU C 4.7.1; text)" hakukone := "http://images.google.com/images?q="..sana.."&lr=lang_fi&cr=countryFI" etsitty_url := "http://t[0-9]\\.gstatic\\.com/images\\?q=tbn:[a-zA-Z0-9_-]*" i := 0 loadResourceLines(hakukone, ua=user_agent) | search(etsitty_url) | enum() | saveResource(url, i) for url, i
Seuraava koodi luo listan kaikista hakukansioissa olevista ohjelmista:
path := ENV["PATH"] / ":" komennot := [ls(dir) for dir in path]
tai
komennot := [push(ENV["PATH"]) | split(sep=":") | ls(dir) for dir]
Alla oleva esimerkki etsii tiedostosta kaikki.txt
kaikki kirjainparit, laskee jokaisen lukumäärän ja tulostaa kymmenen eniten esiintynyttä.
readLines "kaikki.txt", limit=1000 | chars | slide 2 | [_.._] | unorderedCount | [[_2, _1]] | sort | tail 10 | reverse | _ | print `$_ kpl: $_` /* tulostus: 14006 kpl: n 9154 kpl: a 8281 kpl: in 7190 kpl: i 6585 kpl: en 5497 kpl: k 5461 kpl: an 4929 kpl: ta 4828 kpl: ll 4456 kpl: si */
Komennot selitettynä:
readLines
lukee tiedostosta ensimmäiset 1000 riviä.chars
muuttaa jokaisen rivin merkeiksi.slide 2
monistaa jokaisen merkin virrassa ensimmäistä ja viimeistä lukuun ottamatta, muodostaen jokaisen mahdollisen merkkiparin.[_.._]
yhdistää jokaisen merkkiparin kahden merkin pituiseksi merkkijonoksiunorderedCount
työntää virtaan jokaista uniikkia merkkiparia kohden merkkiparin lukumäärän[[_2, _1]]
tekee jokaista merkkipari-lukumäärä-paria kohden listan, jonka ensimmäinen elementti on luumäärä.sort
järjestää lukumäärä-merkkiparilistat ensimmäisen arvon eli lukumäärän mukaan.tail 10
ottaa virrasta viimeiset 10 arvoa.reverse
järjestää arvot suurimmasta pienimpään._
"flattaa" lukumäärä-merkkiparilistat, eli työntää niiden arvot virtaan.print
tulostaa arvot.Rödassa on vahva tyypitys sekä mahdollisuus käyttää tyyppiannotaatioita. Lisäksi kielessä on tuki luokille ja reflektiolle, joiden avulla on mahdollista luoda monipuolisia kirjastoja.
P. S. Kiitokset ylläpidolle syntaksivärityksen tukemisesta!
miksi on olemassa wcat funktio? miksei vaan wget?
purkka kirjoitti:
(24.02.2016 20:22:54): miksi on olemassa wcat funktio? miksei vaan wget?
wcat
on tosiaankin väännös wget
istä. Monet Rödalla tekemäni skriptit liittyvät jotenkin Internetiin ja siksi kyseiselle komennolle on tarvetta. Se jopa tukee lippuja -U
ja -O
, jotka on nimetty wgetin vastaavien lippujen mukaan.
En kuitenkaan halua, että Rödan komennot sekoittuvat Unix-komentoihin. Siksi nimesin komennon miten se on nimetty. Röda-ohjelma voi käyttää myös wget-komentoa seuraavalla koodilla:
wget := { |a...|; {} | exec "wget" *a | {} } /* {}| ja |{} sulkevat sisään- ja ulostulovirrat */
Tällä hetkellä suurin huoleni on komento time
, jolla on Unix-vastine, joka tekee jotain aivan muuta. Minun pitäisi keksiä sille jokin hyvä toinen nimi. Huomasin myös sattumalta, että järjestelmääni on asennettu komento print
, mutta tämä ei ole niin suuri ongelma, sillä kyseinen komento ei ole niin tunnettu tai käytetty.
Uusi versio 0.9-alpha on nyt kehityksen alla! Se tuo mukanaan unless
- ja until
-lauseiden lisäksi helpotetun syntaksin funktiokutsuille ja aritmetiikalle.
Testailin tässä huvikseni kieltäni ohjelmoimalla yksinkertaisia testejä. Huomasin hauskuudekseni, että alkulukujen laskeminen on mahdollista seuraavalla pienellä koodilla:
alkuluvut := (2) seq 3 100 | for i do if [ i % a != 0 ] for a in alkuluvut; do print i alkuluvut += i done done
Eli on mahdollista lisätä ehdon perään for alkio in taulukko
, jos ehdon pitää päteä kaikille alkioille. En edes tajunnut, että niin voi tehdä, mutta asia on selvästi näin johtuen ehtolauseen semantiikasta. Kuten Bourne shellissäkin, myös Rödassa ehto on komento ja jokaisen komennon perään voi laittaa for
in, jotta se suoritettaisiin monta kertaa. Ehtolauseen vartalo suoritetaan vain, jos yksikään ehtolauseen paluuarvo ei ole epätosi.
Käyttämällä lyhennyssyntaksia (for
vartalon jälkeen), koodi voidaan tiivistää seuraavaksi, mutta en tiedä, onko se enää kovin luettavaa:
alkuluvut = (2) seq 3 100 | { alkuluvut += i if [ i % a != 0 ] for a in alkuluvut } for i print a for a in alkuluvut
Röda on muuten nopeampi generoimaan alkulukuja kuin Bash vastaavalla koodilla, mikä on minusta hieman yllättävää, vaikka Bashia ei kyllä ole varmaankaan tarkoitettu siihen.
Lisäys:
Tässä on esimerkki oikeasta ohjelmasta, jonka olen tehnyt Rödalla: http://kaivos.org:25565/. Se on simppeli wrapperi Wikialle, joka on välillä suorastaan ärsyttävä JavaScript-himmeleineen, jotka lagaavat puhelimilla ja joskus tietokoneellakin. On paljon nopeampi selata tätä. Koko HTTP-palvelin on koodattu alusta alkaen Rödalla, koodi siihen löytyy Githubista.
Olen päättänyt uudistaa kielen syntaksin kokonaan. Aiemmin se oli sekava ja monitasoinen ja saman asian tekemiseen oli monta eri tapaa. Aion yhdenmukaistaa syntaksia ja tehdä siitä selkeämpää ja ymmärrettävämpää.
Alla on lueteltu tähän mennessä tekemäni muutokset ja aivan lopussa on muutama kysymys. Olisin kiitollinen, jos joku vastaisi niihin.
Selitys | Röda 0.9-a | Röda 0.10-a |
---|---|---|
Funktiokutsulause | print "olen " ikä "-vuotias" | print "olen ", ikä, "-vuotias" |
Listan luominen | ("Maija" "Anne" "Liisa") | ["Maija", "Anne", "Liisa"] |
Laskutoimituksen tekeminen | print $(pisteet*3) | print pisteet*3 |
Upotettu yhden arvon palauttava funktiokutsu | palvelin = ![server 25565] | palvelin = server(25565) |
Upotettu kutsu ja putkitus | salasana = ![cat "salasana.txt" | head] | salasana = cat("salasana.txt") | head() |
Upotettu monta arvoa palauttava funktiokutsu | käyttäjät = !(cat "nimet.txt") | käyttäjät = [cat("nimet.txt")] |
Neliölistan luominen 1 | !(seq 1 1000 | push $(x*x) for x) | [seq(1, 1000) | push(x*x) for x] |
Neliölistan luominen 2 | !(push $(x*x) for x in !(seq 1 1000)) | [push(x*x) for x in [seq(1, 1000)]] |
Mikä olisi hyvä syntaksi tyyppiparametreille ja -argumenteille?
Vanhassa Rödassa tyyppiargumentit määriteltiin nimen perässä kulmasulkeilla. Tämä ei aiheuttanut ongelmaa, sillä pienempi kuin ja suurempi kuin -operaattoreita ei saanut käyttää kuin aritmetiikkatilassa.
print ![funktio<tyyppi1, tyyppi2> 2]
Koska nyt aritmetiikkatilaa ei enää ole, esimerkiksi seuraava lause voi aiheuttaa ongelmia:
print(funktio<tyyppi1, tyyppi2>(2)) print((funktio<tyyppi1, tyyppi2>)(2)) /* oikea tulkinta */ print((funktio<tyyppi1), (tyyppi2>2)) /* toinen tulkinta */
Muutamia saamiani ehdotuksia:
print(funktio:[tyyppi1, tyyppi2](2)) print(funktio<@tyyppi1, @tyyppi2>(2)) print(funktio[@tyyppi1, @tyyppi2](2)) print(funktio<<tyyppi1, tyyppi2>>(2)) print(funktio!(tyyppi1, tyyppi2)(2))
Pitäisikö sulut pakottaa myös lausetason funktiokutsuihin?
Nyt lausetasolla sulut voi jättää pois. Ne ovat kuitenkin joskus pakollisia, sillä joissakin tapauksissa koodi jäsennetään väärin ilman niitä. Pitäisikö ne pakottaa aina?
print nimi, " ", ikä /* ilman sulkuja */ print(nimi, " ", ikä) /* sulkujen kanssa */ print [1] /* yrittää ottaa "print-listan" toisen alkion ja kutsua sitä -> virhe */ print([1]) /* tulostaa listan "[1]" */ print time() | write "log.txt" /* tulkitaan: */ print(time() | write("log.txt")) /* -> virhe, sillä write() ei palauta mitään */ print(time()) | write "log.txt" /* kirjoittaa ajan ja rivinvaihdon lokiin */
Pitäisikö +=
-operaattori poistaa?
Tällä hetkellä +=
-operaattoria voi käyttää asian lisäämiseksi listaan esimerkiksi seuraavasti:
lista = [] lista += "kissa" lista += 2 print(lista) /* tulostaa [kissa, 2] */
Pitäisikö tämä operaattori poistaa, kuten eräät ovat minulle ehdottaneet? Itse pidän sitä todella käytännöllisenä. Rödassa on myös .=
-operaattori, joka yhdistää listaan toisen listan.
Kielen suurimpia laatutekijöitä ja tekijän kohtaamia vaikeuksia on yksityiskohtien yhteensopivuus. Yhteensopimattomuus ilmenee mm siten, että sama asia voidaan tehdä monella tavalla. Kysymyksiin yksityiskohdista on hyvin vaikeaa vastata ilman syvällistä perehtymistä. Gallupista ei ole ole juuri koskaan apua.
vinsentti kirjoitti:
(02.06.2016 17:07:00): Kielen suurimpia laatutekijöitä ja tekijän...
Vastaaja ei tainnut huomata, että minä en kysynyt yksityiskohdista enkä edes siitä, mille asioille tulisi olla oma syntaksi. Kysymykseni sisälsi pintapuolisia ja esteettisiä ongelmia, joihin jokainen täällä oleva voi luultavasti muodostaa jonkinlaisen subjektiivisen kannan. Jokaisesta vastauksesta, myös huonoista, on minulle hyötyä ja saan niistä vertailukohtia muodostaakseni oman kantani. Lopullisen päätöksen teen minä itse, tiedänhän oman kieleni parhaiten. Gallupissa ei ole mitään vikaa.
Mutta olet oikeassa siinä, että gallupista ei ole välttämättä apua. Ohjelmointiputkassa on näet uskomattoman vähän ihmisiä, jotka jaksavat ylipäätänsä vastata gallupeihin, saatikka sitten kiinnostuneet minun kielestäni. Edellinen vähänkään kiinnostunut ihminen ragequittasi saatuaan kuulla sen (kieltämättä järkyttävän) faktan, että Forth ei sovi aloittelijoille. Minä olen myös yrittänyt olla kiinnostunut muiden henkilöiden vastaavista projekteista, heikoin tuloksin. Ohjelmointiputkan keskustelukulttuuri on uskomattoman epäkannustava.
fergusq kirjoitti:
Ohjelmointiputkassa on näet uskomattoman vähän ihmisiä, jotka jaksavat ylipäätänsä vastata gallupeihin, saatikka sitten kiinnostuneet minun kielestäni. Ohjelmointiputkan keskustelukulttuuri on uskomattoman epäkannustava.
Koko maailmassa on vähän ihmisiä, jotka ovat kiinnostuneita sellaisistakin asioista kuin rakenteellinen ohjelmointi tai olio-ohjelmointi. Kannanottoja kaverin aikaansaannoksiin, esim ohjelmointikieleen rajoittaa kyllä vaadittava effortti. Usein on vaikea sanoa, onko kysymys periaatteesta vai yksityiskohdasta. Sanonnan "piru piilee yksityiskohdissa" sisältö on juuri tämä: pitäisi jaksaa perehtyä.
Olet kyllä oikeassa siinä, että hyvin harva jaksaa täysin perehtyä niinkin monimutkaisen asian kuin ohjelmointikieli yksityiskohtiin. Varsinkin, kun on kyse Rödan kaltaisesta erittäin epätavallisesta kielestä, jonka ajattelumalli virtoineen ja säikeineen erilainen kuin muissa kielissä (paitsi juuri komentosarjakielissä). En esimerkiksi usko, että kukaan täällä täysin ymmärtää, mitä rivi print(time()) | write("log.txt")
tekee ja miten se toimii. (Aion muuten varmaan rikkoa tuon ja lopettaa I/O:n käsittelyn pääfunktion virroissa, sillä nyt funktio ei voi tulostaa mitään, jos se palauttaa arvon tai jos sitä kutsutaan funktiosta, joka palauttaa arvon, mikä on hieman ärsyttävää.)
Esittämäni kysymykset, varsinkin ensimmäinen, liittyvät kuitenkin enemmän kauneuteen kuin ohjelman logiikkaan. Minä annan useamman ehdotetun vaihtoehdon tyyppiargumenttien syntaksiksi, joista jokainen voi valita suosikkinsa tai esittää uusia vaihtoehtoja. Minkäänlaista ymmärrystä mistään Rödan ominaisuudesta ei vaadita.
Itse laittaisin tyyppeihin vaikka @-merkin ja mieluiten ihan tavalliset sulut. En ihan ymmärrä (enkä jaksanut selvittää), mitä ovat nämä tyyppiargumentit funktion kutsuvaiheessa, ts. mitä tekisi jokin tällainen funktio@(A,B)(1)
. Jos tyypit vaikuttavat suoraan ohjelman toimintaan eivätkä vain tuota funktiosta erilaisia versioita (kuten C++:n malleissa), onko edes tarpeen käsitellä tyypit ja data erikseen vai voisiko nämä yhdistää (kuten JavaScriptissa)?
fergusq kirjoitti:
Nyt funktio ei voi tulostaa mitään, jos se palauttaa arvon.
Sehän on tavallaan hieno ominaisuus ja auttaa tekemään funktionaalisten kielten tyyliin porttautuvia funktioita, joilla on vähemmän sivuvaikutuksia kuten odottamatonta tulostetta.
Metabolix kirjoitti:
Itse laittaisin tyyppeihin vaikka @-merkin ja mieluiten ihan tavalliset sulut. En ihan ymmärrä (enkä jaksanut selvittää), mitä ovat nämä tyyppiargumentit funktion kutsuvaiheessa, ts. mitä tekisi jokin tällainen
funktio@(A,B)(1)
. Jos tyypit vaikuttavat suoraan ohjelman toimintaan eivätkä vain tuota funktiosta erilaisia versioita (kuten C++:n malleissa), onko edes tarpeen käsitellä tyypit ja data erikseen vai voisiko nämä yhdistää (kuten JavaScriptissa)?
Tyyppiparametreja ja -argumentteja voi käyttää vaikka näin (sinun syntaksillasi):
record MyRecord(v) { val : integer = v } function f@(A,B)(p : A) { return new B(p) } function main() { print(f@(integer,MyRecord)(2)) }
On jotenkin selkeämpää, että tyypit ovat omassa nimiavaruudessaan. JavaScript kärsii suuresti siitä, kuinka hankalasti hahmotettava tyypin käsite on. Nyt funktiosta ei varsinaisesti luoda uutta versiota (esimerkiksi niin, että sen voisi laittaa muuttujaan f = funktio@(integer)
, mutta tällainen ominaisuus on helposti mahdollista toteuttaa.
Saman voi toki tehdä myös reflektiolla:
function f(my_type, p) { return my_type.newInstance(p) } function main() { print(f(reflect MyRecord, 2)) }
fergusq kirjoitti:
kuinka hankalasti hahmotettava tyypin käsite on
Tyyppiparametreja sanotaan joskus (ainakin C++) geneeriseksi paradigmaksi. Vain taivas on rajana, kun ajatellaan, mitä kaikkea niillä voidaan tehdä. Siitä ylennys paradigmojen kastiin, joista jokainen vaatii veronsa harjoittajaltaan. Verotus on kovaa, kun kielessä on paljon paradigmoja. Menee vaikeaksi.
Tyyppiparametreille käyttäisin Metabolixin ehdottamaan syntaksia. Sulut pitäs pakottaa selkeyden vuoksi ja += on kiva lisä .= lisäksi.
Tyyppiparametrien merkintä voisi yhtä hyvin olla myös tällainen: funktio(@A, @B, 2)
. Tyyppien ei olisi välttämätöntä olla samassa, vaan ne voisi funktion esittelyssäkin luetella muiden parametrien joukossa loogisemmissa kohdissa.
Haluaisin, että tyyppiparametrien syntaksi olisi johdonmukainen sekä tietueita että funktioita käsiteltäessä. Tietueilla voi olla myös tavallisia parametreja (jotka muodostavat eräänlaisen konstruktorin), mutta näitä ei tietenkään anneta esimerkiksi kentän tyypin määriteltäessä, joten tyyppiparametrien laittaminen niiden sekaan olisi melko sekavaa.
record LinkattuLista@(T)(t) { sisältö : T = t linkki : LinkattuLista@(T) } function teeLinkattuLista@(T)(arvot : list@(T)) { lista := new LinkattuLista@(T)(arvot[0]) kohta := lista for arvo in arvot[1:] do uusi_kohta := new LinkattuLista@(T)(arvo) kohta.linkki = uusi_kohta kohta = uusi_kohta done return lista }
Jos tyyppiparametrit sotkettaisiin muiden parametrien sekaan, tulos voisi olla hieman sekava:
record LinkattuLista(@T, t) { /* <-- tyyppiparametri ja tavallinen parametri */ sisältö : @T = t linkki : LinkattuLista(@T) /* <-- vain yksi argumentti */ } function teeLinkattuLista(@T, arvot : @list(@T)) { lista := new @LinkattuLista(@T, arvot[0]) /* <-- kaksi argumenttia */ kohta := lista for arvo in arvot[1:] do uusi_kohta := new @LinkattuLista(@T, arvo) kohta.linkki = uusi_kohta kohta = uusi_kohta done return lista }
Tällä hetkellä Röda tukee annotaatioita, jotka käyttävät niin ikään @-merkkiä. Teknisesti se, että tyypeillä ja annotaatioilla on sama etuliite, ei aiheuta ongelmia, mutta se voi olla hieman sekavaa.
Miten minun kannattaisi nimetä seuraava ominaisuus?
split
-funktiolla on kaksi tilaa. Ensimmäinen tila työntää ulostuloon pilkotun merkkijonon osat sellaisenaan. Toinen tila työntää ulostulovirtaan arrayn, joka sisältää pilkotun merkkijonon osat.
Siis näin:
/* split pilkkoo oletuksena välilyönnistä */ ulostulo := [split("kissa söi kalaa", "koira söi lihaa")] print(ulostulo) /* ["kissa", "söi", "kalaa", "koira", "söi", "lihaa"] */ ulostulo := [split(:c, "kissa söi kalaa", "koira söi lihaa")] print(ulostulo) /* [["kissa", "söi", "kalaa"], ["koira", "söi", "lihaa"]] */
Molemmilla tiloilla on omat käyttötarkoituksensa. Ensimmäinen tila on hyödyllinen, jos merkkijonoja on vain yksi, kun taas toinen on kätevämpi usean merkkijonon kanssa. Käytännön esimerkki voisi olla esim. "abc;def;ghi"
-tyylisen merkkijonon parsiminen (tämä on aito pätkä eräästä ohjelmastani):
/* split(:s, "c") pilkkoo c-merkin kohdalta, structure on merkkijono */ push(structure) | split(:s, ";") | split(:c, :s, "") | for group do /* group on lista merkkejä */ done
(split voi ottaa syötteen siis joko argumenttina tai sisääntulovirrastaan putkituksen avulla)
Minusta :c
on rumaa syntaksia (samoin :s
). Siksi aionkin jakaa split-funktion kahdeksi funktioksi. Mitkä olisivat hyvät nimet näille funktioille, joista toinen laittaa saman merkkijonon osat listaan ja toinen palauttaa ne kaikki samassa?
Tuntuu omituiselta, että split
hyväksyy useamman kuin yhden splitattavan argumentin. Jos funktioiden yhdistely on helppoa, jokainen voi ajaa vain yhden asian.
Eikö olisi yleiskäyttöisempää tarjota map
-funktio ja ilmaista tuo :c
-versio muodossa map split
, kuten monissa muissa kielissä? Ensimmäinen versio voisi olla map split | concat
tai miten se nyt kielessä ilmaistaankaan. Haskellissa tämä on niin yleinen asia, että standardikirjastosta löytyy myös concatMap split
.
Huomasit juuri yhden epäloogisuuden, joka Rödassa on. Periaatteessa kielessä on kahdenlaisia funktioita: sellaisia, jotka ottavat syötteen sisääntulovirrasta (split, match, search, replace, ...), sekä sellaisia, jotka ottavat argumentteja (fileExists, seq, ...).
Sisääntulovirrasta lukevat funktiot liittyvät suurin osa jotenkin merkkijonojen käsittelyyn. Niissä ikään kuin oletetaan, että sisääntulovirta olisi jotenkin yhtenäinen asia, kuten se usein on. Esimerkiksi tiedostoja luettaessa voi kirjoittaa
readLines("file.txt") | split() | replace("\\W", "") | for word do /* ... */ done
readLines
palauttaa työntää tiedoston rivit merkkijonoina virtaan, minkä jälkeen jokainen pilkotaan välilyöntien kohdalta ja lopuksi erikoismerkit poistetaan. For-silmukassa käydään siis läpi jokainen tiedoston kirjaimista koostuva sana.
Toki on epäselvää, että jotkin funktiot tukevat tätä ja jotkut eivät. Lisäksi split ja match tukevat vielä lisäksi argumentteja käytännöllisyyden vuoksi, mikä entisestään monimutkaistaa asiaa.
Rödassa on sisäänrakennettu syntaksi map-funktion kaltaiselle toiminnolle. Jos esimerkiksi fileExists
-funktiota haluaisi käyttää keskellä virtaa, voisi kirjoittaa seuraavasti:
readLines("fileNames.txt") | fileExists(fileName) for fileName | for doesExist do /* ... */ done
Tällaista tarvitsee melko harvoin, eikä tuo esimerkkikään ole kovin järkevä (miksi kukaan haluaisi loopata totuusarvoja?). Jos oikeasti haluaa tehdä noin, on selkeämpää kirjoittaa se eksplisiittisesti. Siksi kaikkiin funktioihin ei ole rakennettu tukea virroille.
Yksi ratkaisu olisi poistaa virtatoiminto kaikista funktioista, mutta en haluaisi tehdä sitä, sillä ominaisuus on todella käytännöllinen varsinkin, jos putkitettuja komentoja on hyvin monta peräkkäin. Alla on kuvitteellinen esimerkki, joka on kirjoitettu ilman virtatoimintoa.
readLines("file.txt") | split(line) for line | replace("\\W", "", fragment) for fragment | for word do /* ... */ done
Kuvitteellinen siksi, että replace ei tällä hetkellä tue merkkijonon ottamista kolmantena argumenttina. Esimerkistä näkee, kuinka sekavaksi koodi muuttuu for-rakenteiden lisäämisen jälkeen.
Eräs vaihtoehto olisi tehdä jokaisesta funktiosta kaksi eri versiota: virrasta lukeva ja argumentteja ottava. Olisiko se hyvä ratkaisu? Mikä olisi hyvä nimeämiskäytäntö näille funktioille?
(PS. Uudelleennimesin cat
in readLines
iksi, se on selvempi. Nuo syntaksiväritykset ovat aika vanhentuneet, joten ehkä olisi parempi, jos pelkät avainsanat värjättäisiin. Funktioiden lista muuttuu aika nopeasti, koska kieli ei ole vielä valmis. Avainsanat on lueteltu täällä. Kiitos paljon moderaattorille kielen tukemisesta!)
Unohda tuo funktio-ideologia. Keskity pelkästään virta-ideologiaan.
Määrittele siten, että () on vakiomuotoinen virta (atomi), funktionimi on nimettyvirta (kiinnitetty muuttuja) ja | putkituskomento eli yhdiste kahdelle virralle.
Nyt voit suorittaa "funktio kutsun" vaikkapa seuraavasti: (("file.txt")|tiedosto)|readLines
missä (("file.txt")|tiedosto) tarkoittaa,että tiedostonimi "file.txt" syötetään virralle tiedosto ja tiedosto syötetään virralle (funktiolle) readLines. Tällätavoin pääset eroon funktioiden kahden version ongelmasta.
fergusq kirjoitti:
readLines("file.txt") | split(line) for line | replace("\\W", "", fragment) for fragment | for word do /* ... */ doneKuvitteellinen siksi, että replace ei tällä hetkellä tue merkkijonon ottamista kolmantena argumenttina. Esimerkistä näkee, kuinka sekavaksi koodi muuttuu for-rakenteiden lisäämisen jälkeen.
Eräs vaihtoehto olisi tehdä jokaisesta funktiosta kaksi eri versiota: virrasta lukeva ja argumentteja ottava. Olisiko se hyvä ratkaisu? Mikä olisi hyvä nimeämiskäytäntö näille funktioille?
Jos suurin syy kahteen eri versioon on koodin sekavuus, parempi ratkaisu olisi minusta keksiä for-rakenteita kätevämpi syntaksi.
For-rakenteet muistuttavat vähän lambdoja. Miten olisi vaikka tällainen, kuten Scalassa:
readLines("file.txt") | split(_) | replace("\\W", "", _) | for word do /* ... */ done
tepokas kirjoitti:
Unohda tuo funktio-ideologia. Keskity pelkästään virta-ideologiaan.
Olisi kiintoisaa tehdä kieli, joka perustuu kokonaan virroille. Valitettavasti Rödaa on vaikea viedä siihen suuntaan, sillä funktioiden argumenteilla on tarkoituksensa. Esimerkiksi replace-funktion argumenteilla annetaan tietoa suoritettavasta operaatiosta, kun taas virta sisältää käsiteltävän datan. Pitäisikö funktioilla olla monta sisäänmenovirtaa erityyppisille argumenteille?
Kieli, jossa on pelkkiä virtoja, on kuitenkin niin kiva idea, että voisin toteuttaa joskus sellaisen. En kuitenkaan usko, että siitä saisi kovin käyttökelpoisen.
jlaire kirjoitti:
Jos suurin syy kahteen eri versioon on koodin sekavuus, parempi ratkaisu olisi minusta keksiä for-rakenteita kätevämpi syntaksi.
For-rakenteet muistuttavat vähän lambdoja. Miten olisi vaikka tällainen, kuten Scalassa:
readLines("file.txt") | split(_) | replace("\\W", "", _) | for word do /* ... */ done
Tuo kuulostaa hyvältä idealta. Alaviivasyntaksin avulla mitä tahansa funktiota voi käyttää helposti virran keskellä.
Funktiot voisi jakaa niiden käyttötarkoituksen mukaan seuraaviin luokkiin:
Tavoite olisi, että funktiosta pystyisi helposti päättelemään, miten sitä käytetään. Arvoja käsittelevien funktioiden kanssa käytettäisiin alaviivaa ja virtaa käsittelevien funktioiden kanssa ei.
/* Ottaa aakkosjärjestyksessä 10 ensimmäistä sanaa */ readLines("sanat.txt") | split(_) | sort() | head(10) | writeLines("sanat2.txt")
Toteutin kieleen muuten Python-tyyliset nimetyt argumentit. Nyt split-funktiolle voi antaa erotinmerkin syntaksilla split(sep=",")
, eikä tarvitse käyttää rumaa split(:s, ",")
-syntaksia.
Funktion parametrit voi nähdä vaikkapa siten että merkinnässä (("file.txt")|tiedosto) on merkkijono "file.txt" on _tyyppiä_ tiedosto tai tiedosto on parametrin "file.txt" nimi. Tai että tiedosto nimeltä "" toimitetaan könttänä Readlines-operaation inputiin ja Readlines poimii siitä rivit. Miten sen sitten haluaa ilmaista.
Virta-olio voi tulkata saapuvaa tietoa ja toimia parametrien tyypin tai nimen mukaan, miksi sitä sitten sanotkaan. Tällöin ei tarvita kahtaa eri syöte- tai inputti-virtaa.
Kirjassa nimeltä "Concepts, Techniques, and Models of Computer Programming" on esitetty monia eri näkökantoja ohjelmointikielen toteutukseen ja kielen oikeaksi todistamiseen, jos isommin alkaa todistaminen kiinnostamaan, kannattaa lukea. Kirja oli ennen vapaasi netti-levityksessä, nykypäivästä en tiedä.
tepokas kirjoitti:
Kirjassa nimeltä "Concepts, Techniques, and Models of Computer Programming" on esitetty monia eri näkökantoja ohjelmointikielen toteutukseen ja kielen oikeaksi todistamiseen, jos isommin alkaa todistaminen kiinnostamaan, kannattaa lukea. Kirja oli ennen vapaasi netti-levityksessä, nykypäivästä en tiedä.
Mitä tarkoitat ohjelmointikielen todistamisella oikeaksi? Mitä se on englanniksi? Aion kyllä määritellä kielen formaalisti jossain vaiheessa, kunhan jaksan. Löysin kirjan netistä ja se vaikuttaa ihan mielenkiintoiselta.
tepokas kirjoitti:
"Concepts, Techniques, and Models of Computer Programming"
Kirjassa puhutaan ohjelmista päättelemisestä, mutta ei mennä asiaan tavanomaista enempää. Kääntäjät tavanomaisesti päättelevät esim tyyppien yhteensopivuudesta, mutta eivät päättele, että ohjelma vastaa speksiä.
Kirjan käyttämä kieli Oz on kaikkien paradigmojen opetus- ja tutkimusalusta. Toisessa ääripäässä Meyerin Object Oriented Software Construction-kirjan Eiffel kieli on puhtaasti olioparadigmaan perustuva käytännön ohjelmointikieli, joka menee päättelyasiassa tavanomaista pidemmälle.
Aikaa on kulunut ja Röda on kehittynyt. Olen lisännyt nykyiseen versioon paljon kaikkea.
Toteutin muun muassa jlairen ehdottaman alaviivasyntaksin, joka onkin helpottanut suuresti eräitä asioita. Esimerkiksi map-operaatiosta on tullut hyvin yksinkertainen. Alla oleva koodi työntää virtaan luvut 1–10, neliöi jokaisen ja tulostaa ne sitten:
seq 1, 10 | [_ ^ 2] | print _
Hakasulut ovat siis sama asia kuin push(...)
, eli ne työntävät arvon virtaan. Yllä olevassa koodissa tämä työntöoperaatio suoritetaan siis jokaiselle virran arvolle erikseen.
Tämän ominaisuuden avulla keksin myös helpon tavan toteuttaa reduce
-funktio:
function reduce(val) { if tryPeek x do /* Jos sisääntulovirrassa on arvoja */ pushBack(val) /* Työnnetään val sisääntulovirtaan ensimmäiseksi */ else /* Muulloin */ push(val) /* Työnnetään val ulostulovirtaan */ done } /* Käyttöesimerkki: */ seq(1, 10) | reduce(_*_) /* Laskee arvojen 1–10 tulon */
reduce
siis käyttää virtaa pinona. reduce(_*_)
:n alaviivat ottavat virrasta kaksi arvoa, ja reduce
työntää yhden tilalle, kunnes virta on tyhjä, jolloin lopullinen tulos työnnetään ulostulovirtaan.
Keksin vähän samankaltaisen funktion lukujonojen generoimiseksi. Alla määritelmä ja esimerkki.
function makeSeq(cond, values...) { if [ cond ] do /* Jos annettu ehto on tosi */ pushBack *values[::-1] /* Työnnetään sisääntulovirtaan annetut arvot */ push values[-1] /* Työnnetään viimeinen arvo ulostulovirtaan */ done } /* Käyttöesimerkki: */ [1, 1] | makeSeq(_2<55, _2, _1+_2) /* Työntää virtaan Fibonaccin jonon arvot 55:n asti */
Alaviivan perässä oleva numero kertoo siis, kunka mones virrasta otettu arvo on kyseessä. Fibonaccin jono generoidaan siis siten, että virrasta otetaan kaksi arvoa _1
ja _2
ja tilalle työnnetään _2
ja _1+_2
. Samalla _1+_2
työnnetään myös ulostuloon. Tätä toistetaan, kunnes _2
on 55.
Kysymykseni on: Ovatko nämä ominaisuudet mielestänne järkeviä? Ovatko reduce
ja makeSeq
järkeviä nimiä näille funktiolle, vai sopisiko jokin muu paremmin?
Onko kenelläkään mitään ajatuksia? Olenko selittänyt nämä Rödan ominaisuudet tarpeeksi hyvin? Muissa kielissä ei oikein ole niitä, joten niille ei ole kunnolla sanastoa ja niistä on vaikea puhua...
Ei nyt herää kovin syvällisiä ajatuksia, kun en käytä Rödaa. Muutama pieni huomio kuitenkin:
En ihan hahmota, miten reduce toimii. Tarkastaako tryPeek, että virrassa on vielä kaksi arvoa? Miten tämä näkyy? Vai onko yksi arvo virrasta automaattisesti jo luettu, kun funktiossa ylipäänsä ollaan, eli katsooko tryPeek aina toista arvoa? Miten tarkastettaisiin kolmannen arvon olemassaolo? Mitä tapahtuu, jos tarkastusta ei tehdä ja virta loppuukin kesken? Olisiko f(_,_) sama kuin f(_1,_2) vai f(_1,_1), vai haetaanko virrasta aina lisää arvoja jokaisella parametrin käyttökerralla funktiossa? Ynnä muita kysymyksiä usean alaviivan käytön erikoisuuksista.
pushBack on mielestäni hämmentävä nimi, kun tulos on päinvastoin pushFront. Ymmärrän kyllä, mistä back tässä tulee, mutta ei se ole itsestään selvää. Ehkä kuvaavampi nimi olisi vaikka unpull tai reinput.
makeSeq ilmeisesti ”hävittää” alkuperäiset arvot, eli esimerkissä Fibonaccin lukujono alkaisikin sitten 2, 3, 5. Saako tähän jotenkin myös alkuperäiset arvot mukaan?
Dokumentaatiossa olisi kiva, jos navigaation otsikot (Constants, Functions ym.) jotenkin osoittaisivat olevansa klikattavia, esim. cursor:pointer ja jokin pieni hover-efekti.
f(_,_)
on sama kuin f(_1,_2)
. Yleisesti alaviiva ilman numeroa vastaa yhtä suurempaa numeroa, kuin edellinen alaviiva. Esimerkiksi f(_1,_)
on sama asia kuin f(_1,_2)
, vaikka f(_,_1)
on sama kuin f(_1,_1)
. Jos numero on mainittu, niin myös sitä pienempien numeroiden oletetaan olevan olemassa, esimerkiksi f(_2,_)
on sama kuin f(_2,_3)
ja se lukee virrasta kolme arvoa, hylkää ensimmäisen ja antaa kaksi muuta f
-funktiolle.
tryPeek x
palauttaa totuusarvon, joka on tosi, jos virrassa on arvoja ja epätosi, jos virta on tyhjä. Eli kun sitä kutsutaan, virrasta on otettu kaksi arvoa, ja se tarkistaa, onko virrassa kolmas arvo. Jos on, kahden arvon yhdistelmä työnnetään takaisin sisääntulovirtaan kolmannen arvon eteen, jolloin virrassa on ainakin kaksi arvoa, jotka voidaan seuraavalla kierroksella lukea sieltä uudestaan. Jos virta on tyhjä, tryPeek
palauttaa epätoden arvon ja kahden äsken luetun arvon yhdistelmä työnnetään sisääntulovirran sijasta ulostulovirtaan.
pushBack
on tosiaan hieman harhaanjohtava nimi. Pitää pohtia, jos vaikka tuo unpull
olisi parempi.
makeSeq
tosiaankin tuhoaa kaksi ensimmäistä arvoa. Niiden saaminen mukaan lukujonoon on pulmallista, sillä tällä hetkellä jokainen kierros on samanlainen, mutta toisaalta vain ensimmäisellä kierroksella virran arvot haluttaisiin työntää ulostulovirtaan. Pohdin ongelmaa, kun minulla on enemmän aikaa.
Lisäsin dokumentaatioon cursor:pointer
-efektin.
Jaa ei olekaan curly bracket kieli. { ja } tosin avainsanoina yhdessä kohtaa.
Minulla on pieni ongelma: Tällä hetkellä listojen, karttojen ja muiden tietorakenteiden hash-koodit lasketaan niiden sisältämien arvojen perusteella. Jos lista sisältää itsensä, syntyy ikuinen silmukka, mikä ei ole kovin hyvä juttu. Eräs ohjelmani kaatui jokin aika sitten juuri tähän, enkä keksi kunnollista ratkaisua. Javassa, jolla tulkki on tehty, on sama ongelma.
Ongelmani syntyi siis tällaisesta tietueesta:
record Tietue { kenttä : list = [self] }
Koodin voisi varmaan kirjoittaa siten, että tietue ei sisältäisi itseään, mutta olisi toki kiva, jos se toimisi sellaisenaan. Hash-koodeja lasketaan vain tietyissä tilanteissa, joten tällainen toimimaton tietue saattaa aiheuttaa ongelmia vasta pitkän ajan kuluttua sen kirjoittamisesta.
vinsentti kirjoitti:
Jaa ei olekaan curly bracket kieli. { ja } tosin avainsanoina yhdessä kohtaa.
Bourne Shell -kielen ja sukulaisten inspiroimana Rödassa aaltosulkeet merkitsevät yleensä funktiota (joko nimettyä tai nimetöntä) tai tietueita, kun taas ohjausrakenteita merkitään kirjaimista koostuvilla avainsanoilla. Kielen syntaksia on helpompi parsia, kun nimettömät funktiot eivät sekoitu muihin lohkoihin.
fergusq kirjoitti:
Tällä hetkellä listojen, karttojen ja muiden tietorakenteiden hash-koodit lasketaan niiden sisältämien arvojen perusteella. Jos lista sisältää itsensä, syntyy ikuinen silmukka, mikä ei ole kovin hyvä juttu.
Voit tehdä ongelmallisen luokan ympärille wrapperin, jonka hashCode palauttaa jonkin vakioarvon, jos sitä kutsutaan uudestaan.
class MyHashMap<K,V> extends java.util.HashMap<K,V> { private boolean hashCodeCalculating = false; @Override public synchronized int hashCode() { if (hashCodeCalculating) { return 0xdeadbeef; } hashCodeCalculating = true; int code = super.hashCode(); hashCodeCalculating = false; return code; } } public class tmp { public static void main(String[] args) { java.util.HashMap<Integer, Object> a = new MyHashMap<Integer, Object>(); a.put(0, a); System.out.println(a.hashCode()); } }
Huomaathan, että equals-metodissa on sama ongelma. Siitä lienee yksinkertaisinta palauttaa false, jos tulee päällekkäisyyksiä.
fergusq kirjoitti:
Bourne Shell -kielen ja sukulaisten inspiroimana Rödassa aaltosulkeet merkitsevät yleensä funktiota.
Muissa komentorakenteissa käytetään avainsanoja, mutta proseduurimäärittelyssä sulkeita, kun ajatellaan määrittelyssä voivan olla vain kaksi avainsanaa: alku ja loppu. Muissa komentorakenteissa: avainsana ... avainsana ....loppu avainsanoilla ei ole mitään tekemistä sulkujen kanssa, niillä ei ole sellaista tasapainoehtoa kuin suluilla. Tästä syystä ei sulkuja. Tosiasiassa proseduurimäärittelyssäkin on ajateltavissa monenlaisia avainsanojen rajaamia osastoja esim:
pre ehto
do komentoja
post ehto
end
Tai tietysti on se vaihtoehto, että tyydytään vähään.
Aihe on jo aika vanha, joten et voi enää vastata siihen.