detencode -- mi a gond vele?

Ugye az alapgondolat zseniális: írjuk bele a fájl elején kommentbe a fájl kódolását, pl:


/* HelloVilag.java */
/* encoding: ISO-8859-2 */

Aztán valahogy így fordítsunk:


$ javac -encoding $(detencode HelloVilag.java) HelloVilag.java
# javac -encoding ISO-8859-2 HelloVilag.java -- ez lesz belőle

Ez így csudállatiasan jó.

Lenne.

Ha most kezdődne a világ, és mindenki szépen alkalmazkodna ehhez a szokványhoz. És persze a fájl örökké úgy maradna, ahogy volt, az esetleges módosítások során pedig tiszteletben tartanák az eredeti beállítást.

A valóság azonban az, hogy a világban épp egy átmenet tart különféle egybájtos kódolásokból UTF8 irányba.

Ez az átmenet az én életemben valószínűleg még nem fog befejeződni, de mindenesetre azzal jár, hogy alkalmanként a forrásprogramok UTF8-ra konvertálódnak (optimális esetben a 'from' meg fog egyezni azzal, ami eredetileg volt; ha nem, akkor egy két kelet-európai betű elhullhat a harcban), de a fájl elején gondosan elhelyezett komment nagy biztonsággal a régi értéken marad, ettől kezdve a segítő szándékú 'detencode' programunk nem használ, hanem árt.

Most, hogy eddig eljutottunk, akár megpróbálhatunk megoldást is javasolni; íme a 2.0 verzió:


/* HelloVilag.java */
/* encoding: UTF-8/ISO-8859-2 */
/* sample: árvíztűrő tükörfúrógép */

Mit tesz a derék szoftver? Látja, hogy az encoding-ban bizonytalanság van, tehát folytatja a második módszerrel: olvas a fájlból, amíg az alábbiak valamelyike nem teljesül:
- olvasott egy utf8-ban érvénytelen szekvenciát
- olvasott hat darab érvényes utf8 szekvenciát
- olvasott 64KB-t
Ezek után az alábbi döntést hozza:
- olvastam érvénytelen szekvenciát? Ha igen, akkor az encoding két fele közül "a másik" kell (az, amelyik nem az UTF-8)
- Ha nem, akkor UTF-8
- Ha az encoding-ban hülyeség van (pl CP852/ISO-8859-2 vagy WINDOWS-1250/WINDOWS-1252 vagy UTF-8/UTF-8), akkor undefined behaviour lesz belőle. Például úgy vesszünk, mintha csak az első lenne ott. Vagy outputra adjuk az egész stringet.

Szerk: az utóbbit helyesbíteném: ez nem hülyeség, csak sajnos a software nem képes mágiára, és nem tudja megkülönböztetni a különféle egybájtos kódolásokat.

Note to self: ha a felhasználó odaírja, hogy -default=UTF8, de a fájlon látszik, hogy nem az, azt fel kellene ismerni, és hibaüzenettel nyugtázni

Szerk: mindjárt az következik, hogy két default értéket is megadhasson: -default=UTF-8,ISO-8859-2 ... ami pont ugyanazzal az eredménnyel jár majd, mint a -default=detect,ISO-8859-2

Hozzászólások

Elkezdtem egy programozási nyelvet tervezni. Több napig gondolkodtam rajta, hogy hogyan kell jól kezelni a stringeket. A végén arra jutottam, hogy csakis UTF-32-ben szabad a stringeket tárolni, minden más eretnekség.

Az UTF-32 az csak egy kódolása az Unicode-nak. A programozási nyelv alapvetően egy absztrakt dolog, azt mondhatod, hogy a nyelvedben a karakterláncok elemei Unicode karakterek.
Meg leírhatod, hogy az implementációban UTF-32 kódolással kell 1-1 karaktert kezelni.
Miért nem jó az UTF-8 kódolás? Miért eretnekség?

Miért kéne a belső reprezentációnak (ami implementációs részlet) a programnyelv részének lennie (ami egy absztrakt specifikáció)?
Az, hogy egy programnyelv adott implementációja hogyan tárolja belül a Stringeket, az érdekes kérdés. Például olyan beágyazott rendszerben, ahol a memória nagyobb korlát, mint a CPU, ott az UTF-8 a legjobb megoldás, az UTF-32 pazarlás.
Szerintem rossz dolog, ha egy programozási nyelv tervezésekor nem absztrakt fogalmakban (pl. Unicode karakterek), hanem konkrétumokban (pl. UTF-32 kódolás) gondolkodik az OP.

Persze, én sem a belső tárolást értem alatta. Az alapkérdés az, hogy mi a "String" objektum API-ja. Például a Java String objektum char (16 bit) típusú tömbként kezeli lényegében a stringet. Viszont vannak karakterek, amik nem férnek el a 16 bites char típusban. Ezért kellene a char típusnak 32 bitesnek lennie, ha el akarnánk kerülni a kivételek lekezelését a programokban.

Persze a legtöbb program soha nem találkozik olyan karakterrel, ami nincs benne a 16 bitben, de elvileg még léteznek. És éppen ezért tuti, hogy ezekre a karakterekre a Java programok jelentős része dobna egy hátast. Ugye én még tesztesetet is nehezen raknék össze.

Például mostanában túrtam a lanterna nevű Java könyvtárat, és találkoztam ilyen széles karakterek kezelése miatti extra függvényekkel a kódban. És bele akartam volna nyúlni a működésbe, de ezeket egyrészt nem is értettem, másrészt pedig nem tudtam még kipróbálni sem, hogy jól írtam-e át. A "megoldás" az lett, hogy kiszedtem a széles karakterkezelést mondván, hogy nekem nem kell, és így egyszerűen beletettem a libbe amit akartam. Viszont így a végeredményt nem tudom visszaosztani az eredeti projektnek, mert a kínaiak regressziót kapnának a nyakukba.

De a belső tárolás sem teljesen mindegy. Ha UTF-8-ban tárolod benn, de helyesen értelmezed az unicode karaktereket, akkor a legegyszerűbb string műveleteidnek is végig kell nyálaznia a teljes stringet. Például egy mystring.substring(1000, 1002) futási ideje az 1000-rel fog skálázódni a 2 helyett. ( http://www.joelonsoftware.com/articles/LeakyAbstractions.html ) Ha viszont UTF-32-ben tárolod, akkor a substring skálázódása ideális lesz. Cserébe 4X annyi memóriát használsz egy string tárolásakor.

Plusz, ha megnézel egy unicode konverziós kódot, akkor láthatod, hogy rohadt bonyolult, nem az a fajta cucc, amit egy nyelv core-jában látni szeretnél. Odabenn rendnek kell lenni, ilyen bonyolult, nehezen validálható vacakoknak nincs helye :-). De ez már filozófiai kérdés.

Eleve garbage kollektoros nyelvet terveztem, nem a mikrokontrollerek világa volt a cél. Mikrokontrolleren UTF-8-as stringeket kezelni... Mondjuk úgy, hogy nem vágyom rá. (Bár a magyar ékezetes karaktereket már nyomtattam mikrokontrollerről. De csak azt a 2*8-at valósítottam meg egy nagy switch-case-ben, ami nekem kellett.)

"Például a Java String objektum char (16 bit) típusú tömbként kezeli lényegében a stringet."
Nem igazán, a String az egy CharSequence. Ami char-ok rendezett, indexelhető (charAt) sorozata. Tárolhatod te ezt láncolt listával is, nem muszáj tömbként tárolni.
Ezért mondom, ez egy absztrakt struktúra, nem konkrét implementáció.

" Viszont vannak karakterek, amik nem férnek el a 16 bites char típusban. Ezért kellene a char típusnak 32 bitesnek lennie"
Nem biztos, hogy a char típusnak X vagy Y bitesnek kell lennie, ez egy implementációs kérdés. A char egy absztrakt dolog, egy Unicode karaktert reprezentál. AZ, hogy ezt fixen 16 biten teszi (így nem lehet minden Unicode karaktert charként tárolni), esetleg nem fixen (pl. UTF-8 kódolású tömbként), vagy fixen 32 biten, az implementációs kérdés.

Például lehet az a char típus definíciója, hogy 1 Unicode karaktert tárol, nem specifikálva a bithosszt. Ennyi. Implementációs kérdés a bithossz. Persze ekkor a char típus nem primitív típus már.

"Ha viszont UTF-32-ben tárolod, akkor a substring skálázódása ideális lesz. Cserébe 4X annyi memóriát használsz egy string tárolásakor."
Épp ezért ez implementációs részlet, nem része a programnyelv definíciójának. Az egyik implementáció így implementálhat, a másik amúgy, amíg az API-t betartja mindkét implementáció, semmi gond. A programnyelved definíciója nem szabad, hogy tartalmazzon implementációs részleteket.
Mondok egy példát, C++ STL. Régebben a GNU libstdc++ eléggé nem hatékony módon implementálta a vector kezelését, például a hossz nem volt cache-lve, hanem mindig végigszámolta azt. Nem hatékony - CPU szempontjából. Hatékony memória szempontjából. Implementációs részlet.

"Plusz, ha megnézel egy unicode konverziós kódot, akkor láthatod, hogy rohadt bonyolult, nem az a fajta cucc, amit egy nyelv core-jában látni szeretnél. "
Igen, rohadt bonyolult, mert emberi dologról (írásról) szól. Az emberi dolgok bonyolultak. Szokj hozzá.
Megcsinálhatod azt is, hogy a nyelvedben nincs beépített String adattípus, meg char adattípus, lehet ilyet is csinálni. Semmi gond.
Fejlesztenek majd hozzá ASCIIString libet, meg UnicodeString libet, no para.

"Eleve garbage kollektoros nyelvet terveztem"
Ez is implementation detail. MOndhatod azt, hogy a memória automatikusan kezelt. Hogy ezt hogyan implementálja egy-egy nyelvi implementáció (garbage collectorral, ARC-vel, stb.), az már implementációs kérdés.

A Java esetén így emlékeznek meg erről a JLS-ben:
"It includes automatic storage management, typically using a garbage collector"
Nem írják elő, hogy garbage collector kell. Csak azt, hogy a memóriát automatikusan kezeli a runtime.

Direkt kötekedsz?

"Nem igazán, a String az egy CharSequence. Ami char-ok rendezett, indexelhető (charAt) sorozata." char típusú objektumok indexelhető sorozata az lényegében egy tömb. Pláne, hogy read only, semmilyen módosító művelet nincs specifikálva rajta (Java Stringről beszélünk ugye). Persze hogy implementálhatja valaki láncolt listával, de attól még az API szerint read only tömbként viselkedik.

"Nem biztos, hogy a char típusnak X vagy Y bitesnek kell lennie, ez egy implementációs kérdés." Jaja, csak a skatulya-elv szerint 16 bitbe semmiképp nem fér bele az összes Unicode karakter. Tehát ha az APi specifikációja szerint Unicode karakterek tömbje a String, akkor nem lehet 16 bites az ábrázolás semmilyen implementációban sem.

Eleve az API-ról beszéltem. Egy dolog, hogy az API jól specifikált-e, és egy másik, hogy kellően egyszerű-e. És még egy, hogy egyszerűsíti-e a benne írt programokat, vagy bonyolítja-e?

Például a C++ nem adott standard string reprezentációt. Az lett a vége, hogy ahány lib, annyi féle stringet használ. Ez kisebb káoszhoz vezet, ha véletlenül többféle libet kell egyszerre használnod. A Java egész jó Stringet ad, és a kivételes igényektől eltekintve minden API azt használja.

A bonyolult dolgokkal meg nem az a bajom, hogy léteznek, hanem hogy ha igazából plusz RAM-mal ki lehet váltani őket, akkor megfontolandó ez a lépés. Nem azt mondtam, hogy kötelező, hanem hogy ésszerűnek tűnik.

Szerintem a Java String még így is egyértelműen jó példa, viszont egy rakás rejtett hibát okoz szerintem a 16 bites ábrázolás, amit persze 99%-ban sosem tapasztalunk magyarként. Ha egyszer az összes code point belefér a 32 bitbe, akkor minek szivatjuk magunkat 16 bittel? Ennyi volt az alap felvetésem. Az UTF-8-as cikk érdekes, vannak benne új dolgok számomra, úgyhogy azt bele fogom építeni a világképembe, ha lesz időm, és lehet, hogy mást fogok utána mondani a kérdéskörről.

Az algoritmusok skálázodásának ismerete is alapvető fontosságú, egy program tervezésekor ismertnek kell lenni ezeknek a kérdéseknek. Egyáltalán nem implementációs részlet. Például a hossz lekérdezés indokolatlanul való skálázódása pl O(n), miatt a terméket simán visszadobják a programozónak hogy lassú. Erre ő hiába mondja, hogy a másik libbel fejlesztette, és erre azt gondolta, hogy implementációs részlet, a végén mégiscsak probléma van, amit meg kell oldani.

A garbage collection, illetve referencia számlálás sem implementation detail, mert a referencia számlálás a körkörös rendszereket nem tudja kidobni. Azt még hajlandó vagyok elfogadni, hogy ha azt mondom, hogy precíz memory management van (azaz a nem referált és finalizált objektumok azonnal felszabadíthatóvá válnak), akkor az már elegendő. De még itt is kérdéses, hogy milyen CPU és latency karakterisztikával működik, ami szintén lehet a nyelv specifikációjának a része. (Azért mert a mostaniaknak általában nem az, attól még nem eretnekség, ha egy jövőbelinek az lesz. Ugyanis mérnökileg indokolt ezeket a dolgokat tervezéskor előre tudni.)

Épp azért hivatkoztam a leaky abstraction cikkre, mert az mondja meg nagyon jól: egyáltalán nem mindegy a programozó számára az alatta lévő implementáció. Az első problémás esetnél kiderül, hogy valójában ismerni kell.

Eleve az a hiba, hogy Java filet olyan kódolással tárolod, ami nem tud minden Unicode karaktert leírni, ugyanis a Java forrásprogramokban minden Unicode karakter szerepelhet, még az azonosítók nevében is.
Szóval minél előbb térj át valamilyen UTF kódolásra.

A CommonLisp például milyen kódolású forrásprogramot fogad el? Csak ASCII karakterek? Vagy Unicode is lehet?
Nem terelés ez egyáltalán.
Csak éppen annak a tökéletes bizonyítéka, hogy szarunk a szabványokra, előírásokra, mert úgyis okos a compiler, majd megeszi a szart is.

Ad2:
http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.1

"Programs are written using the Unicode character set."
Kétlem, hogy a CP852, vagy az ISO-8859-2 az Unicode karakterkészletet kódolná bytesorozattá. Az csak egy kedvező mellékhatás, hogy az Unicode-nak bizonyos kódolásai (pl. az UTF-8) megegyeznek az ISO-8859-2 kódolások egy részével. Emiatt lehet, hogy egy Java forrásprogram lehet egybájtos kódolásban, de attól még maga a Java forráskód az Unicode. Totál értelmetlenség olyan kódolású file-ban tárolni Java forráskódot, ami nem fedi le a Unicode-ot.
Persze tudom, mivel az IT-ben mindenki túl okos akar lenni mindenkinél, ezért a should meg a could könnyen összekeveredik.

> Kétlem, hogy a CP852, vagy az ISO-8859-2 az Unicode karakterkészletet kódolná bytesorozattá.

Pedig de, mindkettő unikódok egy-egy halmazát reprezentálja 8-8 biten. Olyan szabály nincsen, hogy egy adott Java source-nek az összes unikódot tartalmaznia kellene, vagy hogy olyan kódolásban kell lennie, ami az összes unikódot tartalmazza.

Egyébként nem érdektelen a dolgozat, kiderül belőle (ha esetleg nem tudtuk), hogy a Java azért használ UTF16-ot, mert még akkor állt rá az Unikódra, amikor még azt hittük, hogy "16 bit mindenre elegendő", ugyanígy járt pl. a MS Windows, az IBM AIX, az Oracle RDBMS.

"Pedig de, mindkettő unikódok egy-egy halmazát reprezentálja 8-8 biten."

Nem, nem az Unicode karakterkészletet kódolják 8 bitre.
Az Unicode karakterek kódolására vannak kódolási szabályok, és a CP852 és az ISO-8859-2 nincs köztük.

Az csak egy side effect, hogy az Unicode tartalmazza azokat a karaktereket, amelyeket a fent említett kódolások kódolni tudnak 8 bitre, épp ezért a bytesorozatból értelmezhetünk Unicode karaktereket is.
De side effectet feature-nek beállítani veszélyes terep, az IT-ben lévő sok gányolás ebből ered: ami side effect (esetleg bug), abból lesz feature, amit "okosan" kihasználhatunk.

És innen ered az, hogy ISO-8859-2-ben van tárolva Java forráskód. Persze a te problémád sem létezne, ha mérnökök programoznának, és nem pistikék.
Attól, hogy valami működik, még nem jelenti azt, hogy jól van az úgy.

Valahogy úgy érzem, hogy körbe-körbe járunk...

Érdekességképp megemlítem, hogy pl. Siemens BS2000-ben, EBCDIC kódolásban (pontosabban EBCDIC.DF.04-2 -ben) is lehet Java source-t előállítani, azt javac-cal lefordítani, MS Windowsban vagy IBM AIX-ben futtatni a keletkezett class fájlt, és akkor is ott is lesznek a gyönyörű ékezetes magyar betűk.

lehet != kell, még mindig nem sikerül megérteni.
Az egész detencode egy non-problem lenne és nem is lenne rá szükség, hogy foglalkozz vele, és betartanánk a józan észt, miszerint Unicode karaktereket nem kódolunk csak Unicode-subsetet tudó kódolással. Egyszerű ez, de szeretjük bonyolítani a dolgokat.
Te is, ahelyett, hogy megoldanád a problémát (minden forrás legyen UTF-8 - ezt egyszer kell megcsinálni, és totál mindegy, hogy emiatt nyúlsz hozzá egy file-hoz, vagy azért, hogy beírd az encodingot, amit a detencode megeszik), workaroundot keresel rá, totál feleslegesen pazarolva az idődet.

Szóval az, hogy "az alapgondolat zseniális" az hatalmas nagy baromság. Ez egy csúnya, undorító szar hack, ami csak amiatt van, mert sokan elhiszik, hogy amit side-effectek miatt lehet, azt kell is.

Namost ha a Jocó HelloWorld.java programjairól lenne szó, akkor valóban nem kellene sokat gondolkozni a migráció gyors vagy lassú, szükséges vagy szükségtelen voltáról.

Abban az esetben viszont, ha néhány millió programsorunk van, különféle programnyelveken, különféle fejlesztőktől, évtizedekre visszamenőleg, akkor már érdemes kissé elgondolkozni azon, hogy hogyan lehetne a működőképességet megőrizve alkotni.

Na erre mondtam, hogy elsőre a fájlba kommentként beleírt kódolást véltem jó ötletnek (vö python pep 263), írtam is erről egy kis blog-bejegyzést pár napja.

Most viszont felködlött bennem, hogy ez így nem annyira jó, mint amennyire rossz; nevezetesen a forrásprogramot egy derék ember/program puszta jószándékból előbb-vagy-utóbb át fogja konvertálni UTF8-ra, de úgy, hogy a kommentet nem változtatja meg. (Még örülhetünk, ha nem tesz elé BOM-ot).

Tehát valami olyan megoldáson törpölök, hogy az egybájtos kódolású fájlban meg lehessen adni kommentként a kódolást, de arra is esélyt hagyva, hogy mire a fordításra kerül a sor, esetleg mégsem az...

"Tehát valami olyan megoldáson törpölök, hogy az egybájtos kódolású fájlban meg lehessen adni kommentként a kódolást"
Ahelyett, hogy azt a megoldást választanád, hogy ahelyett, hogy kommentet ír az ember a fileba, konvertálja inkűbb UTF-8-ra. A filehoz ígyis-úgyis hozzá kell nyúlni a kommentírás esetén is, hiszen ha jól értem, akkor ezek a kommentek még nem léteznek. És akkor meg már miért nem oldanád meg rendesen a problémát? Minek a workaround?

Igen, a charset detektálás megoldhatatlan probléma, és ebbe mots belefutottál te is, és megpróbálsz okosabbnak lenni, mint az uchardet.

Még mindig: az uchardetben benne van, az, ami a best effort létezik. De te megpróbálod újra feltalálni a melegvizet. Ez egy olyan busy working, ami nem visz előre, cserébe az eredménye ugyanúgy kérdőjeles.

Nem, olyan zsákutcába soha se mennék bele, hogy statisztikák alapján megsaccolni, hogy ez most CP852 vagy CWI2. Ez rémálom lenne.

Egyetlen 'intelligens' funkciót tettem a programba: valid-e a source utf8-ként, vagy sem. Ez is elég problémás, ennél mélyebbre nem akarom ásni magamat.

Erre meg ott a moreutilsból az isutf8.
http://joeyh.name/code/moreutils/

De most tényleg nem értem, mire és hogyan akarod ezt használni. Elvárod minden forráskódtól, hogy legyen benne encoding comment? És ha nincs? Egyáltalán, milyen encodinggal legyen benne az encoding comment? Egy EBCDIC kódolású fileban teljesen máshogy néz ki bytesorozatként az encoding komment, mint egy CP852 kódolású bytesorozatban. Ugyanígy, egy Pascal kódban másként kell eleve kommentelni, mint egy SGML fileban.

Még mindig ugyanazt, amit az előző blogbejegyzésben írtam: a felhasználó megadhat egy -default opciót, az alábbiak szerint:


    -default <value>
	character-set name or special value 'detect' or 'getenv'
	'detect' means checking utf-8 validity (not very reliable)
	'getenv' means trying LC_ALL, LC_CTYPE, LANG (in this order)
	it can be a list, too, eg:
    -default ISO-8859-2
    -default detect,getenv,ISO-8859-1

ezek közül a legutolsó a defaultja a default-nak, vagyis detect, ha az nem megy, akkor getenv, ha az sem megy, akkor ISO-8859-1

A szövegfájlt sajnos úgy találták fel, hogy nem tartalmazza metaadatként azt, hogy őt hogyan kell értelmezni. (Ha belegondolsz, ez minden fájlra igaz. Valójában egyetlen fájl sem létezhetne ilyen metaadat nélkül, ha jobban belegondolunk.) Ezért nem lehet jól megoldani ezt a kérdést.

A fordításkori találgatás szerintem nagyon nem jó ötlet, mert bevisz egy plusz véletlenszerű faktort a buildbe, ahol eleve azt szeretnénk ha kiszámíthatóan reprodukálható végeredményt adna.

Az egyetlen viszonylag jó megoldás az, hogy a projekt metaadata az, hogy milyen enkódolást használ, amit az összes használt toolnál meg kell adni: pl: .gitencoding, pom.xml, Eclipse .project file, stb. Ha valamelyik projektbeli fájl ettől eltér, akkor az hiba. Vagy ha elkerülhetetlen (amit nem tudok elképzelni), akkor egyesével be kell állítgatni a projekt leírókban, hogy az olyan.

+1: includált fájlban (header) meg lehetőleg egyáltalán ne legyen ékezetes betű, legfeljebb kommentként, de inkább úgy sem...

+2: persze ha az adott kontextus engedi, akkor legjobb, ha a 'rendes' forrásfájlokban sincs ékezetes betű, hanem gettext-et vagy hasonlót használunk