Tässä pieniä otteita komentorivillä ajetusta Javascript-sessiosta. Käytän Rhinoa. Sama toimi myös Firebugin JS-konsolissa, joten kai tämä on sitten se toivottu toimintatapa tässä jalossa kielessä.
js> [] + {} [object Object]
Okei, kaipa sitä laskemalla yhteen tyhjän taulukon ja JS-objektin saa objektin. Mitähän tapahtuu, jos sen tekee toisin päin?
js> {} + [] 0
Ahaa. Nyt tulos on 0. Tietenkin näin.
Vielä pitää laittaa samat jutut taulukkoon ihan kokeilun vuoksi.
js> [ [] + {} , {} + [] ] [object Object],[object Object]
Ookei. Nyt jälkimmäisestä yhteenlaskusta ei tullutkaan 0.
Mutta hei, määritelläänpä pari muuttujaa ja lasketaan niiden summa, sekä suoritetaan vertailun vuoksi sama lasku suoraan annetuilla arvoilla.
js> var x = [] js> var y = {} js> y + x [object Object] js> {} + [] 0
Asia alkaa hiljalleen tulla selväksi. Javascriptissä {} + [] on siis joko 0 tai [object Object], riippuen laskutavasta ja -paikasta. Tämä tietenkin vastaa hyvin jokaisen ohjelmoijan intuitiivisia käsityksiä +-operaattorin merkityksestä! Kukapa nyt tyytyisi summaamaan lukuja tai yhdistelemään merkkijonoja, kun sillä voi tehdä niin paljon muutakin.
Onneksi tätä ominaisuutta ei tarvitse tuntea tehdessään tyypillisiä JS-ohjelmia. Voisin silti esittää otsikon kysymyksen vielä uudestaan ihan ohjelmointikielten suunnittelun ja määrittelyn kannalta. Mitä? Helkuttia?
(En pahastu, jos kukaan ei tarjoa kysymykseeni vastausta :-)
Pekka Karjalainen kirjoitti:
Tämä tietenkin vastaa hyvin jokaisen ohjelmoijan intuitiivisia käsityksiä +-operaattorin merkityksestä!
Eiköhän intuitiivinen käsitys ole, että taulukon ja olion yhteenlaskusta ei tule mitään järkeävää. Eli sikäli saamasi tulokset vastaa ennakkokäsitystä.
Sen verran kuitenkin huomauttaisin, että itse en saa [] + {} :sta [object Object] vaan "[object Object]"
Kaikki muut tulokset mielestäni vaikuttaa muutenkin loogisilta, paitsi miksi {} + [] tulee 0.
Kysehän on loppupeleissä siitä, että + -operaattorin käytössä toimitaan seuraavasti:
1) Jos molemmat puolet on numeroita, lasketaan ne yhteen
2) Jos molemmat puolet on merkkijonoja, yhdistetään ne peräkkäin
3) Jos molemmat puolet voi muuntaa numeroiksi, muunnetaan ja lasketaan yhteen
4) Muunnetaan molemmat merkkijonoiksi ja yhdistetään ne peräkkäin
Testiesi perusteella vaikuttaisi että objektin määrittelyn perään laitetulla +-merkillä on erityismerkitys, eli siinä ei tapahdukaan objektin ja taulukon normaali (edellisten sääntöjen mukaan menevä) yhteenlasku. Tähän viittaisi jälleen sekin, että ({})+"" on "[object Object]"
Ja näinhän se meneekin:
http://www.2ality.com/2012/01/object-plus-object.html:
After the previous explanations, you should not be surprised about the following result, any more:
> {} + []
0
Again, this is interpreted as a code block followed by +[].
Grez kirjoitti:
Pekka Karjalainen kirjoitti:
Tämä tietenkin vastaa hyvin jokaisen ohjelmoijan intuitiivisia käsityksiä +-operaattorin merkityksestä!
Eiköhän intuitiivinen käsitys ole, että taulukon ja olion yhteenlaskusta ei tule mitään järkeävää. Eli sikäli saamasi tulokset vastaa ennakkokäsitystä.
Yksi mahdollinen ennakkokäsitys olisi sellainen, että virheellisestä tai järjettömästä operaatiosta seuraisi virheilmoitus tai poikkeus.
Grez kirjoitti:
Sen verran kuitenkin huomauttaisin, että itse en saa [] + {} :sta [object Object] vaan "[object Object]"
Minulla Rhino ei tulosta näissä esimerkeissä lainausmerkkejä. Firebugin konsoli taitaa niin kuitenkin tehdä, ja varmaan niin teki moni toinenkin JS-ympäristö. Onkohan Rhinossa vakavakin virhe? Ehkäpä kielen määrittelyyn ei kuulu sellainen tieto, miten merkkijonoja pitäisi tulostaa vuorovaikutteisessa ympäristössä.
Grez kirjoitti:
Kaikki muut tulokset mielestäni vaikuttaa muutenkin loogisilta
Minä en välittäisi opetella tällaista logiikkaa yhtään enempää kuin on tarve.
Grez kirjoitti:
Kysehän on loppupeleissä siitä, että + -operaattorin käytössä toimitaan seuraavasti:
1) Jos molemmat puolet on numeroita, lasketaan ne yhteen
2) Jos molemmat puolet on merkkijonoja, yhdistetään ne peräkkäin
3) Jos molemmat puolet voi muuntaa numeroiksi, muunnetaan ja lasketaan yhteen
4) Muunnetaan molemmat merkkijonoiksi ja yhdistetään ne peräkkäin
Tämä ei riitä selittämään sitä, miksi muuttujien summaus toimi eri tavalla kuin literaalien summaus.
Grez kirjoitti:
Testiesi perusteella vaikuttaisi että objektin määrittelyn perään laitetulla +-merkillä on erityismerkitys, eli siinä ei tapahdukaan objektin ja taulukon normaali (edellisten sääntöjen mukaan menevä) yhteenlasku. Tähän viittaisi jälleen sekin, että ({})+"" on "[object Object]"
Ja näinhän se meneekin:
http://www.2ality.com/2012/01/object-plus-object.html:
After the previous explanations, you should not be surprised about the following result, any more:
> {} + []
0
Again, this is interpreted as a code block followed by +[].
Tämän tiedon kanssa selviää sentään, miksi muuttujat toimivat eri tavalla kuin literaalien summaus. Merkintä {} ei olekaan tyhjä objekti-literaali silloin, kun se on tyhjä koodiblokki. Tämä on taas yksi hieno lisätieto, joka osoittaa JS-kielen huolellisesta suunnittelusta.
Onko nyt hyvä olo, kun olet nähnyt näin paljon vaivaa selittämään asian, jonka tietäminen on ihan turhaa kaikille paitsi JS-kielen toteutuksien ohjelmoijille? :)
Sinänsä en halua moittia JS:ää hirveästi. Siinä on hyviäkin puolia(*) ja oudot puolet selittyvät pitkälti sen avulla, että Brendan Eichillä oli kymmenen päivää aikaa tehdä ensimmäinen JS-toteutus Netscapen selaimeen, sitten kun sai luvan. Kaikkia hienouksia ei varmasti kerkeä kovin tarkasti sellaisella aikataululla miettiä.
Huumoriahan tämä vain on. Mutta tällaisenkin tuloksen sain Rhinossa aikaan:
js> + {} + [] NaN
Jos tälle ei saa vähän nauraa, niin mille sitten?
(*) Moni on varmasti nähnyt sen netissä kiertelevän kuvan, jossa on kaksi kirjaa vierekkäin. Paksumpi kirja on kuvaus koko Javascript-kielestä, ja ohuempi on Crockfordin kirja "Javascript: the Good Parts". Ja se toinen on tosiaan selvästi ohuempi.
Pekka Karjalainen kirjoitti:
Yksi mahdollinen ennakkokäsitys olisi sellainen, että virheellisestä tai järjettömästä operaatiosta seuraisi virheilmoitus tai poikkeus.
Ei se ole virheellinen operaatio. Selitinkin jo mitä tehdään. Sitten taas aika harva kieli pystyy päättelemään ohjelmoijan toivomukset järjettömiksi, ja antamaan sillä perusteella poikkeuksia.
Positiivistahan koko jutussa on, että kaikki tulkit näyttivät tuottavan saman tuloksen, eli mitään todellista ongelmaa ei ole.
Pekka Karjalainen kirjoitti:
Minulla Rhino ei tulosta näissä esimerkeissä lainausmerkkejä.
No joo, pointtini oli kuitenkin lähinnä, että tulos on merkkijono.
Kuten sanoitkin, niin tuskin missään määrityksessä on sanottu, miten merkkijono pitää tulostaa konsoliin, mutta itse kyllä pidän siitä että näen suoraan tuloksesta onko muuttujassa 17 vai "17" vai [17]. Eli kehittäjäkonsoliin mielestäni huono ominaisuus jättää ilmaisematta.
Pekka Karjalainen kirjoitti:
Minä en välittäisi opetella tällaista logiikkaa yhtään enempää kuin on tarve.
No miksiköhän sitten toit koko asian esille? Tuollainenhan ei tule missään vaiheessa vastaan jos oikeasti koodaat JS:llä etkä vaan etsi sieltä "omituisuuksia".
Pekka Karjalainen kirjoitti:
Onko nyt hyvä olo, kun olet nähnyt näin paljon vaivaa selittämään asian, jonka tietäminen on ihan turhaa kaikille paitsi JS-kielen toteutuksien ohjelmoijille?
No jos vastauksesta oli sinulle iloa, niin sitten on hyvä olo. Tuskin näin juurikaan enempää vaivaa kuin itsekään tuohon kysymykseen. Ja asian perinpohjaisesta selvittämisestä oli itsellekin iloa ihan näin "akateemisesta mielenkiinnosta".
Grez kirjoitti:
Pekka Karjalainen kirjoitti:
Yksi mahdollinen ennakkokäsitys olisi sellainen, että virheellisestä tai järjettömästä operaatiosta seuraisi virheilmoitus tai poikkeus.
Ei se ole virheellinen operaatio. Selitinkin jo mitä tehdään. Sitten taas aika harva kieli pystyy päättelemään ohjelmoijan toivomukset järjettömiksi, ja antamaan sillä perusteella poikkeuksia.
Aika usea kieli antaa virheitä puheen aiheena olevan kaltaisesta operaatiosta (joskus käännösaikana), tai nostaa poikkeuksen.
Pythonissa taulukon ja sanakirjan (dict) summaus nostaa poikkeuksen.
Luassa kahden taulukon summausyritys on myös virhe. (Tulkissa voi kirjoittaa " return {} + {} " että saa virheilmoituksen.)
En tiedä mitä tarkoitat kielen pystymisellä päättelemään jotakin. Varmasti kielten suunnittelijat ovat ne jotka päättelevät, mitä operaatioita tarjoavat kielen käyttäjille ja mitkä kielen toteutusten pitäisi tulkita virheiksi, ja millä tavalla tai missä vaiheessa virheet ilmenevät. Tietenkin ratkaisu, että järjettömät operaatiot vain tulkitaan jotenkin sopivien sääntöjen avulla ja niistä saa jonkin tuloksen on yksi tapa ratkaista tämä ongelma, mutta tuskin haluat puolustaa sitä mitenkään erityisen suositeltavana yleisperiaatteena.
Puhumme nyt jotenkin ohi toisistamme tässä kohtaa. En minäkään tietenkään oleta, että tämä melko pieni puute nyt Javascriptistä korjataan, tai edes joskus. Kuten muualla toteamme melkein yhteen ääneen, se ei ole tärkeä tavallisessa käytössä.
Grez kirjoitti:
Pekka Karjalainen kirjoitti:
Minulla Rhino ei tulosta näissä esimerkeissä lainausmerkkejä.
No joo, pointtini oli kuitenkin lähinnä, että tulos on merkkijono.
Kuten sanoitkin, niin tuskin missään määrityksessä on sanottu, miten merkkijono pitää tulostaa konsoliin, mutta itse kyllä pidän siitä että näen suoraan tuloksesta onko muuttujassa 17 vai "17" vai [17]. Eli kehittäjäkonsoliin mielestäni huono ominaisuus jättää ilmaisematta.
Juu, tämä kohta on selvä. Hyi Rhinoa.
Grez kirjoitti:
Pekka Karjalainen kirjoitti:
Minä en välittäisi opetella tällaista logiikkaa yhtään enempää kuin on tarve.
No miksiköhän sitten toit koko asian esille? Tuollainenhan ei tule missään vaiheessa vastaan jos oikeasti koodaat JS:llä etkä vaan etsi sieltä "omituisuuksia".
Toin asian esille enimmäkseen huumorimielessä. Taisin jo sanoakin ensimmäisessä viestissäni, että tätä tuskin tarvitsee tuntea tehdessään tyypillisiä JS-ohjelmia.
Jos nyt jotakin perustetta pitää hakea tällaiselle kevyelle lauantaivalitukselle, niin tässä on.
Varmaankin on selvää, että jos kielessä on kaikenlaisia mutkia, joiden ymmärtäminen vaatii kiemuraista logiikkaa, siihen ymmärtämiseen tuhlattu aika on tuhlattua aikaa, jos tiedolle ei ole mitään muuta käyttöä. Kielen ja sen toteutuksien tekijöillä on parempaakin tekemistä kuin laatia ja opetella sääntöjä, joita ei voi soveltaa missään muualla. Ja jos outoon ominaisuuteen voi mitenkään törmätä huonosti kirjoitetussa koodissa (eli siis joku työkaveri tms. tehnyt :), niin kielellä ohjelmoijankin pitää tietää siitä jotakin. Tässä nyt näyttäisi olevan kyse sellaisesta logiikasta, joka on ominaista vain ja ainoastaa Javascriptille. Kuka sellaisesta välittäisi?
Grez kirjoitti:
Pekka Karjalainen kirjoitti:
Onko nyt hyvä olo, kun olet nähnyt näin paljon vaivaa selittämään asian, jonka tietäminen on ihan turhaa kaikille paitsi JS-kielen toteutuksien ohjelmoijille?
No jos vastauksesta oli sinulle iloa, niin sitten on hyvä olo. Tuskin näin juurikaan enempää vaivaa kuin itsekään tuohon kysymykseen. Ja asian perinpohjaisesta selvittämisestä oli itsellekin iloa ihan näin "akateemisesta mielenkiinnosta".
Hymiöni jätit pois. :) Vastauksesta on toki oma ilonsa, jota tuo sanapari akateeminen mielenkiinto kuvaakin hyvin.
Lisäksi minusta nyt vain on yleisesti hauskaa pilkata kielten outoja ominaisuuksia, jos kuitenkin käytän näitä kieliä itse. (Toisten suosimien kielten haukkuminen on vähän turhan epäkohteliasta.)
Esimerkiksi Python on joskus perin kummallinen. Tarkempana esimerkkinä vaikkapa se, miten voi sekä saada virheilmoituksen laittomasta operaatiosta, että samalla aikaa suorittaa se onnistuneesti.
pari = (1, [2]) # pari, jonka toinen elementti on lista pari[1] += [3] # yhdistetään listan loppuun toinen lista
Tämän kun ajaa Python-tulkissa, niin tuloksena on sekä virhe ("TypeError: 'tuple' object does not support item assignment"), että parille uusi arvo (1, [2, 3]).
Lisäksi voi olla hyödyllistä tietää, miten asiat toimivat, vaikka kaikki toiminnallisuus ei tunnukaan tarpeelliselta yleisessä käytössä. Pareja kun ei ole Pythonissa tarkoitettu muokattaviksi. No, tähänkin pikku erikoisuuteen on omat syynsä, ja kun niistä on jotakin käsitystä, ehkä tietää myös jotakin hyödyllistä Pythonista. Kaikenlaisesta tiedosta voi olla hyötyä yllättävissä tilanteissa.
En odota, että kukaan pystyisi suunnittelmaan täydellisen kielen. Mutta on se kumma, jos nykyisiä parempaan ei vielä joskus pystytä. Siksi on ihan kohtuullista vaatia aina vaan parempaa ja valittaa armotta kaikesta tyhmyydestä.
No, koska Putka ei ole oikea paikka perusteelliselle valitukselle, voinemme ehkä lopettaa Grezin viimeisen puheenvuoron jälkeen :)
Pekka Karjalainen kirjoitti:
Aika usea kieli antaa virheitä puheen aiheena olevan kaltaisesta operaatiosta (joskus käännösaikana), tai nostaa poikkeuksen.
Kyseisissä kielissä varmaankin vastaavantyylinen operaatio on virheellinen. Jos nyt luit mitä kirjoitin, niin kaiva joku kieli joka antaa poikkeuksen sinänsä validista operaatiosta, joka todennäköisesti kuitenkin on hölmö.
throw new StupidProgrammerException("Did you really want to concatenate array's toString() to object's toString()? Idiot!")
Joissakin kielissä on toki olemassa warningit...
Jos olisi tuollainen hölmöjen operaatioiden tunnistin, niin missä vaiheessa sen pitäisi huutaa? Pitäisikö tällaisesta tulla Exception?
[17,18]+{toString:function() { return "Jipii" }}
Tulos: "17,18Jipii"
Lohko on parsimajärjestyksessä ennen lausekkeita. Siksi, jos koodi voidaan parsia lohkoksi ja olio-literaaliksi, se on lohko. Rivin täytyy alkaa {-merkillä, jotta siitä tulee lohko, sillä {}-merkkejä ei koskaan tulkita lohkoksi (vaan olio-literaaliksi) keskellä lauseketta. Siksi tämä näyttää vähän epäselvältä, vaikka onkin parsereiden ja standardin mielestä täysin selvä tapaus.
Esimerkki koodin muodossa:
// One-linerina {} + [] // Paremmalla formaatilla { } +[]
mutta
// One-linerina + {} + [] // Paremmalla formaatilla + {} + []
jukkah kirjoitti:
jos koodi voidaan parsia lohkoksi ja olio-literaaliksi, se on lohko.
Parseri ei backtrackaa, eli toisin sanoen statement-tasolla { aloittaa aina blokin ja sitä ei edes yritetä tulkita objektiliteraalina. Esimerkiksi {"foo":1}
statement-tasolla on syntaksivirhe. Tämän takia JSON:ia parsitaan joskus sanomalla eval('('+str+')')
— koska sulkujen sisällä ei voi olla statementtia, parseri tulkitsee {
-merkin objektiliteraalin aluksi.
> {"foo":1} SyntaxError: Unexpected token : > ({"foo":1}) Object foo: 1 > {foo:1} // parsitaan blokkina, foo on labeli 1
Node.js:n repliä ei kannata käyttää näiden kokeilemiseen, koska jostain syystä se ymmärtää {}
:n objektina. Chromen console toimii odotetusti.
jlaire kirjoitti:
Parseri ei backtrackaa
Testaus jäi puolitiehen... ;) Tuota en kyllä muista lukeneeni standardista, vaikka siellä se kuitenkin on jossakin kahdella sanalla kerrottuna.
Löysin maininnan sivulta 89, mutta kukapa noita BNF-listauksia jaksaisi näin tarkkaan lukea...
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf
ECMAScript 5.1, p. 89 kirjoitti:
12.4 Expression Statement
Syntax
ExpressionStatement:
[lookahead \not\in {{
,function
}] Expression ;NOTE An ExpressionStatement cannot start with an opening curly brace because that might make it ambiguous with a Block. Also, an ExpressionStatement cannot start with the
function
keyword because that might make it ambiguous with a FunctionDeclaration.
Eli suomeksi sanottuna ExpressionStatement voi olla mikä tahansa Expression joka ei ala tokenilla {
tai function
.
jlaire kirjoitti:
Löysin maininnan sivulta 89
Ai juu, niinpäs näkyy... Höhlä olisi pitänyt muistaa itsekin. :( Tajusin heti ensimmäisellä silmäyksellä tekstiäsi lukiessani, missä kohdassa standardia ne "kaksi sanaa" ovat.
jlaire kirjoitti:
mutta kukapa noita BNF-listauksia jaksaisi näin tarkkaan lukea...
Luin vuosi sitten 5. laitoksen niin tarkkaan läpi, että sain tehtyä siitä implementaation Javalla (joka ei toiminut läheskään). :)
Aihe on jo aika vanha, joten et voi enää vastata siihen.