Na Common Lispu jsem měl vždy problém pochopit jednu podstatnou věc. Jedná se rozsáhlý, standardizovaný, velmi rychlý, průmyslově orientovaný, stabilní, dobře promyšlený, extrémně mocný, moderní a rozšířitelný jazyk. Proč se, sakra, nepoužívá tolik, jak by si zasloužil?
Viděl jsem na toto téma hodně diskusí, ale nemohu říct, že by mi jejich závěry přišly opravdu relevantní. Většinou to skončí u toho, že svádí programátora k překomplikovaně obecnému řešení místo toho, aby se soustředili prostě na vyřešení daného problému. Argumenty se závorkovým peklem není moc věrohodný, když algolovské jazyky jich mají obvykle stejné množství s výjimkou věcí, co řeší speciálními operátory.
Takže, prosím, zkuste odpovědět. Proč nepoužíváte Common Lisp? Já jej osobně nepoužívám, protože místo něj používám Smalltalk, ale vy ostatní omluvu nemáte ;-)
Common Lisp je jeden z mnoha programovacích jazyků, takže "správná" otázka by mohla znít, proč bychom měli používat zrovna tenhle. No ale OK, moje zkušenosti:
Když jsem se snažil si s CL hrát, byl prvotní problém právě v té mnohosti implementací, kterou zmiňuje autor článku. Mám používat CMUCL, nebo SBCL? Případně jiný?
Další problém byl ekosystém - knihovny a jak s nimi pracovat. Byly nějaké nástroje pro řešení závislostí, ale nedalo se to srovnat s "moderními" systémy typu npm a cargo a myslím, že ani s cabal/hackage.
Další problém byl v tom, že CL je jazyk, který umožňuje dělat všechno tisíci způsoby. Mně tohle nevyhovuje, z podobných důvodů mi nesedl Perl nebo C++. Prostě dávám přednost jazykům, které jdou cestou "ortogonality" a "idiomatičnosti", tzn. najít pokud možno nejvhodnější postup a držet se ho, pokud není zásadní důvod to udělat jinak.
Závorky jsou problém (pro mě), jelikož u jazyků, které mají (), [] i {}, se já osobně podstatně lépe vyznám v kódu. Možná to někdo má jinak, o tom se nemá smysl přít.
Takže, abych to shrnul - preferuju jazyky s "návodnou syntaxí", které mají kvalitní ekosystém, mně vyhovující komunitu, "one preferred way" pro konkrétní problém a jednu referenční implementaci, na kterou se komunita soustředí a rozvíjí ji. Common Lisp nemá ani jedno z toho.
Jen zareaguju na ekosystém + knihovny. V Common Lispu se totiž správa knihoven neprovádí na příkazovém řádku (typu pip install ...), ale přímo z REPLu - což ovšem znamená, že se mění spuštěná image Lispu. Tedy ještě jinak - můžu mít 100 běžících REPLů/image, každý s jinou sadou knihoven (takže se neřeší virtuání prostředí a další narovnáváky na ohýbáky).
Navíc je repositář knihoven v CL hodně podobný repositářím Linuxových dister - prostě cokoli se nainstaluje z daného obrazu, bude spolu kompatibilní. Ten repositář se buildí každou chvíli. A zase je to něco, co souvisí se stabilitou CL a jeho ekosystému.
To bych asi potřeboval nějak přiblížit, těžko se mi to představuje. Dělám větší aplikaci, potřebuju tam dát závislosti na knihovnách, knihovny se vyvíjejí. To znamená, že chci/potřebuju mít (já nevím) knihovnu pro AES ve verzi 3.x, knihovnu pro generátory pseudonáhodných čísel verze 2.x atd.
Uvolním to jako FOSS program, zveřejním na GitHubu, co má uživatel udělat, aby se mu to celé nainstalovalo a spustilo? Bude muset řešit nějaké REPL image a distribuce? V Rustu udělám cargo install MySuperApp a mám to tam. Případně udělám checkout z repozitáře, pustím instalaci z toho checkoutu a všechno by mělo jet. Je to podobné i v tom Common Lispu?
14. 4. 2022, 10:49 editováno autorem komentáře
Teď to hodně zjednoduším (a dá se to ohnout, ale popravdě to asi nikdo nedělá).
1. knihovny se až tak nevyvíjejí, že by něco rozbíjely, a když už, tak se mění i jejich závislosti a tranzitivní závoslosti (tedy celý strom)
2. v quicklispu se to (zjednodušuji) řeší tak, že se vydávají snapshoty knihoven, které se buildí spolu *
3. dá se vracet v čase dozadu na libovolný snapshot (http://blog.quicklisp.org/2011/08/going-back-in-dist-time.html), ale prostě CL ekosystém je trošku jiný - věci jsou stabilní (v řádu let, i desítek let), takže to (IMHO) zase tak moc potřeba není.
* To není nic nového, má to prakticky každý jazyk, který má "battery included". Například v Pythonu 3.8 se asi dá nějak vrátit k implementaci řekněme Deque z verze 3.6, ale asi se dost věcí rozbije. Nemluvě o snaze vrátit nějakou knihovnu v Java stdlib například (zase, dá se znásilnit classloader, ale to si koleduje o problémy).
Díky za odpověď. Jakožto starý Pythonista mám tu zkušenost, že "batteries included" prostředí pro seriózní vývoj vždycky bylo poměrně nedostatečné. Některé věci se do distribuce nakonec dostanou ze zdrojů třetích stran, ale stejně člověk znovu a znovu potřebuje nějakou další knihovnu. Záleží na doméně, ve které se aplikace pohybuje, ale prostě to tak obecně je. A pak to začne být "zajímavé".
Mnohost implementací snad nemůže být zásadní problém. Po každý úspěšnější jazyk, který má jednu standardní implementaci, se brzy objeví další alternativní implementace (viz třeba Python). Kdo by preferoval vendor lock-in? Pro některá nasazení můžete chtít implementaci s komerční podporou.
Dělání věcí tisíci způsoby je spíše kulturní problém než problém jazyka. Třeba zmíněný Smalltalk je také velice tvárný metajazyk, ale o něco víc zaměřený na kulturu nástrojů, takže se programátoři většinou vyhýbají větším zběsilostem, protože by se o možnost používat tyto nástroje nad svým programem ochudili nebo si dokonce znefunkčnili vývojové prostředí.
Hm, tak konkrétně pro Python sice existuje dost alternativních implementací (IronPython, Jython, samozřejmě PyPy, RustPython, Stackless...), ale stejně se pořád bere, že CPython je "ten" Python. Vendor lock-in ve světě OSS snad nehrozí.
Jinak, co se týče "kultury jazyka", tak ano, dá se nastavit v rámci třeba firmy nebo týmu. Ale některé jazyky (a jejich komunity) tu "hravost" berou jako plus, mě to tak moc nebere (v praktické aplikaci).
CL používám jako první volbu tak kde něco řeším (třeba jednorázově) sám, ale jsou chvíle kdy si to rozmyslím, a umím si představit že někoho to otráví už při prvním setkání:
Do té doby než vznikl quicklisp byly knihovny a závislosti problém, s tím souhlas s předchozí reakcí. Teď je mnohem jednodušší knihovny získat, ale pořád - pokud chci něco specifického tak si to často musím dopsat sám (za poslední půlrok třeba alpn v ssl, nebo api na secret service). Pozitivní je že to většinou je na hranici triviality, ale některé věci bych asi psát nechtěl.
Doteď je opruz nasadit CL kód tam kde jsou jiné jazyky podporovány out of the box - Heroku se dalo, ale třeba Azure funkce bych se musel namáhat.
A taky se dvakrát rozmyslím než použiji CL řešení tam kde čekám že to bude za měsíc debuggovat někdo jiný (protože lidi).
A zatímco IDE v emacs (slynk, sly) jsou prvotřídní, tak si nejsem jistý jaký je komfort programování mimo emacs. Já jsem kvůli tomu na emacs před lety přešel, ale ne kažý chce.
Jinak díky autoru za článek, i když spousty oblastí se mohl jen dotknout. Snad jen možná není úplně vždy vidět co je vlastnost Common Lispu jako standardu, a co je sbcl specifické (třeba tvorba binárky - a mimochodem pod "překladem do nativního kódu" jsem čekal spíš výstup disassemble té faktorial funkce).
"Doteď je opruz nasadit CL kód tam kde jsou jiné jazyky podporovány out of the box"
Sice nedělám s Common Lispem, ale s jazyky, které se taky překládají do spustitelného kódu a tahají si s sebou celý runtime (Go, ale vlastně i Rust). A zrovna toto je naopak řešení, které se hodí do světa Dockeru a Kubernetes. Prostě vezmu nějakej minimální systém, ať již Alpine (to nepoužíváme) nebo ten RHtí minimal image a do toho se hodí další layer s binárkou aplikace. Nic dalšího, bo konfigurace jde přes env. var.
Neřešíme tedy, jaká verze Pythonu nebo kdovíčeho v tom imagi je, všechno aplikačně-specifické je v té jediné binárce.
Moje subjektivní hodnocení/volba - ano v IT se dost často skočí na "jednoduchém" vyřešení problému (třeba volbou v té době populárnějšího jazyka nebo řekněme operačního systému, který má pěkná vokýnka, ale jinak je to děs a hrůza), aby se nakonec řešily narovnáváky na ohýbáky, když se zjistí limity (nebo někdo jiný řekne, že jsou tam limity a donutí vás ke změně).
Simple != Easy
Jinak v profesní praxi jsme skončili u Go, protože mj. je pro daný obor dobře postaveno a navíc na to dokážu sehnat vývojáře. A když ne, tak už jsme přeučili pár Pythonistů, a to rychle. Být to v US, tak by možná i šel Common Lisp (tam má přece jen větší zastoupení na univerzitách), ale tady moc ne (mám lidi ze Španělska, Itálie, ČR/SR, UA, takže to není jen čístě ČR podivnost).
A jasně, je to jazyk s jedinou cestou, jak psát, žádné velké rozlety se nedají čekat.
Trošku OT: zrovna včera jsem se musel smát tomu, že někdo vymyslel další jazyk na zápis konfigurace (cue) s podtextem "Escape YAML hell". Takže jsme tady měli vývoj XML -> XML s ohýbáky typu podmínky a proměnné (Ant) -> JSON -> YAML -> YAML s ohýbáky typu podmínky a proměnné (v řetězcových hodnotách!) -> Cue. Kdo zůstal u s-výrazů (já používám EDN), tak toto fakt neřeší prakticky celej profesní život.
Má omluva je, že místo něj používám Typescript :-D
V tom vašem výčtu chybí dvě velmi důležité vlastnosti, které spolu volně souvisí:
1) Statické typy. Zrychlují vývoj tím, že odchytávají chyby už ve fázi kompilace, respektive s IDE už ve fázi psaní, umožňují napovídání v IDE (další zrychlení vývoje). Dělat větši refaktoring bez podpory typování musí být peklíčko. Atd.
2) Čitelnost. Pro projekty ve více lidech, případně (netriviální) takové, kdy se k programu vracíte po delší době opět kritická vlastnost. Samozřejmě jako člověk, který v LISPu nikdy pořádně nedělal mohu být zaujatý, ale ta výtka zaznívá hodně často, takže na ní něco bude. Jednak k tomu IMO moc nepřispívá prefixová notace. A pak ty závorky. Když si vezmu jednoduchý příklad z tutoriálu LISPu:
(defun fib (n)
"Return the nth Fibonacci number."
(if (< n 2)
n
(+ (fib (- n 1))
(fib (- n 2)))))
Třeba v Typescriptu by to bylo nějak takhle:
// Return the nth Fibonacci number
const fib = (n: number) => n < 2 ? n : fib(n - 1) + fib(n - 2)
TS verze obsahuje troje (jednoduché) závorky. LISP jich má celkem devatero a navic dosahuje až pětinásobného vnoření (sic!)
Plus s jazyky jako / přístupem LISPu mám tu zkušenost, že jejich zápis je poměrně elegantní, dokud člověk implementuje něco, co je "čisté matematické" řešení. Jenže reálná zadání mají "chlupy", různé specialitky, zvláštní případy, takže kód se komplikuje, není zdaleka tak elegantní a tam jsou pak tyhle jazyky naopak méně čitelné. "Klasické algolské" jazyky jsou v těchhle případech tak nějak přímočařejší a tedy ve výsledku čitelnější. (Ale tohle těžko to nějak objektivně dokazovat, je to můj subjektivní dojem.)
Common Lisp má také volitelné statické typování jako TypeScript.
Příklad s Fibbonacciho posloupností je jeden z těch, které používají hodně infixových operátorů, což pro běžný kód není typické (a tam, kde je to problém, se dá použít makro umožňující zápis infixovou notaci). Netvrdím, že Common Lisp je jazyk s nejčitelnějším zápisem (i když hodně záleží na zvyku), ale udělal zde ústupky, které mu umožňují věci jinde nemyslitelné. Dobře faktorizovaný kód bude čitelný vždy.
Standard CL definuje, jak se mají definovat nové typy a vkládat typové kontroly, nedefinuje však, jak s nimi má daná implementace zacházet. Ta je může ignorovat, používat pro optimalizace, provádět dynamické kontroly či využívat k plnohodnotné statické typové kontrole. Protože obecně, striktní statické typování bývá hodně neohrabané, tento přístup umožňuje programátorům vytvářet skutečně relevantní dynamické typové kontroly a otevírá prostor pro nástroje pro statickou analýzu pokrýt to, co je ještě teoreticky možné.
Jsou věci, které jinak než dynamicky neošetříte. Plácnu třeba: čtverec, jehož strany nepřekročí poměr 3:4; řetězec, který odpovídá určité množině formátů, datová struktura, kde některé položky omezují hodnoty jiných atd.
Nebo třeba jednoduchý číselný typ pro hodnoty 1-7 (třeba indexy dnů v týdnu). Zdaleka ne ve všech staticky typovaných jazycích můžete takový jednoduchý číselný typ vůbec vytvořit. A pokud ano, pak nad ním máte silně limitovanou množinu operací.
Ale nerad bych se tu pouštěl do diskusí o vlastnostech statické a dynamická typové kontroly. Snad jen jeden citát: I'm sure it crashed in the most type-safe way possible.
Je otázka, co má být ještě součást "typu". Je "typ YMD" (trojice čísel) zodpovědný za správné ošetření přestupného roku, tedy za řešení počtu dní v únoru? Asi ne. Na druhou stranu ale pokud jazyk dokáže vyjádřit jenom "vektor objektů" a není možno ani říct, že to je vektor celých čísel a musí se to kontrolovat dynamicky a popisovat v komentáři, je to slabé.
Určitě existují situace, které jdou řešit dynamicky lépe než staticky (mě teď z hlavy nic nenapadá, ale to nic nedokazuje). Každopádně uvedené příklady to zrovna nejsou.
Jiný problém jsou jazyky sice se statickým ale neohrabaným typováním. To je pak rozdíl mezi tím "nejde to z principu" versus "jazyk xy to nepodporuje".
V zásadě jde psaní “business” vrstvy v něčem příčetném, nabízí se Purescript, Haskell (transpiler do JS) nebo něco podobného. Vzhledem k tomu, že interop se zbytkem aplikace je v podstatě zadarmo, člověka nic nesvazuje v psaní bezpečného a stručného kódu. Výsledkem je, že se eliminuje většina jednotkových a integračních testů a všechny “runtime checks”, takže výrazně rychlejší je nejen vývoj, ale i běh kódu. Jediná potenciální nevýhoda je, že V8 nemá optimalizaci ocasní rekurze (standard ES ji předepisuje, ale implementuje ji jen Apple ve svém JS, ostatní na to kašlou), ovšem backend přehazující data v REST API na to nenarazí.
"mě teď z hlavy nic nenapadá"
Jako vazne? Nevim, jestli tu s Calculonem jen trolite nebo to myslite vazne. Obecne nejde staticky definovat ani ciselny rozah, neprazdny list, retezec rozsahu danych delek. Pokud chcete pouzivat bezne aritmeticke/retezcove/listove operace.
16. 4. 2022, 18:56 editováno autorem komentáře
> Obecne nejde staticky definovat ani ciselny rozah, neprazdny list, retezec rozsahu danych delek. Pokud chcete pouzivat bezne aritmeticke/retezcove/listove operace.
Nevím, co máš na mysli tím "obecně", ale běžně se to děje. Přece máš už teď ve všech možných jazycích 32-bitové celé číslo, máš typy s pevnou délkou (jako array v Rustu). Teď je otázka, jaké to má konsekvence:
1. Snažíme se vykonat operaci, kterou zachytí kompilátor/linter (indexování literálem mimo rozsah).
2. Bezpečná operace typu Try... - kód v runtime provede větev podle toho, zda se vejdeme do omezení.
3. Výjimka při překročení mezí.
4. Tiché překročení mezí (přetečení čísla).
Pořád je ale typ definovaný staticky a dynamické jsou "pouze" některé důsledky.
"už teď ve všech možných jazycích 32-bitové celé číslo"
ktery jazyk dokaze v case kompilace u netrivialniho programu zarucit, ze nedojde k preteceni?
"máš typy s pevnou délkou (jako array v Rustu)"
listovymi operacemi myslim operace nad poli s promennou delkou, jako filter, concatenate a podobne.
I u poli s pevnou delkou Rust casto kontroluje meze v case behu
https://stackoverflow.com/questions/47542438/does-rusts-array-bounds-checking-affect-performance
17. 4. 2022, 16:36 editováno autorem komentáře
Nás obviňuješ z trollení, a přitom sám píšeš, že
> ktery jazyk dokaze v case kompilace u netrivialniho programu zarucit, ze nedojde k preteceni?
To pak asi nemá smysl, že?
Zde jistá slečna pěkně a IMHO do mrtě rozebrala dva mýty a proč jsou to mýty.
https://lexi-lambda.github.io/blog/2020/01/19/no-dynamic-type-systems-are-not-inherently-more-open/
Jeste jednou, jak v Haskellu v case kompilace zarucis, ze vysledek aritmeticke operace nepresahne maxint?
kdyby to bylo tak jednoduche jak tvrdis, tak neexistuji veci jako https://hackage.haskell.org/package/safeint
Defines a variant of Haskell's Int type that is overflow-checked. If an overflow or arithmetic error occurs, a <b>run-time</b> exception is thrown.
Dal nebudu reagovat, nebavi me cekat dva dny na schvaleni meho prispevku.
18. 4. 2022, 01:36 editováno autorem komentáře
Já tedy nejsem odborník na typové systémy, ale je všeobecně známé, že:
https://simonmar.github.io/bib/papers/erltc.pdf
nebyl nejúspěšnější projekt.
I když v Erlangu už jsou poslední roky celkem úspěšné snahy to překonat a dotáhnout. I typové systémy samozřejmě zažívají vývoj. Jinak o Erlangu by chtělo někdy článek, nebo aspoň o https://lfe.io/, když už mám zůstat u tématu.
18. 4. 2022, 11:05 editováno autorem komentáře
Trochu to rozvedu: z pohledu standardu jsou typové deklarace slibem kompilátoru, že dané proměnné (funkce, mezivýsledky, ...) jsou daného typu, a pokud nejsou, jde o nedefinovanou situaci a může se stát cokoliv.
Ty deklarované typy mohou být od "cokoliv" přes "celé číslo" po třeba "buď NIL, nebo pole prvočísel délky 23 a libovolné šířky"; nevím co umožňuje za anotace Python takže neporovnám.
Jak se k tomu staví konkrétní implementace je na ní, a typicky také na hodnotě nastavení optimizací (což jsou taky deklarace). Konkrétně sbcl, pokud si pamatuji, postupuje podle kombinace nastavení deklarovaného safety a speed, a buď kontroluje vše, nebo zjednodušeně (např, deklaraci "celé číslo buď od -17 do -7, nebo od 7 do 17" zjednoduší na běhovou kontrolu "od -17 do 17").
Kromě toho sbcl při kompilaci sleduje typy proměnných v určité míře a varuje pokud něco "nesedí" vzhledem k deklarovaným typům a známým funkcím a operátorům. Ale všechno z té deklarace při kompilaci neověřuje.
Třeba v příkladu nahoře: nepozná jestli do pole ukládáme prvočíslo, ale pokud odvodí že přistupujeme k 24 sloupci pole, tak varuje. A příslušný kód rovnou zkompiluje jako vyvolání chyby INVALID-ARRAY-TYPE-ERROR.
Zkuste v Typescriptu s nativnim typem number (a nejakou efektivnejsi implementaci fib) spocitat fib(1000). V Common Lispu aspon dostanete spravny vysledek.
Hlavni featura (Common) Lispu jsou makra, prace s kodem jako s daty. Na takovem malem prikladu se toho moc nepredvede.
Pro zajimavost v Clojure jde (efektivne) fibonaciho posloupnost spocitat takto.
(def fibs (lazy-cat [0 1] (map +' fibs (rest fibs))))
(nth fibs 1000)
14. 4. 2022, 10:39 editováno autorem komentáře
Pretoze po nom nie je dopyt a vyvojovy ekosystem je maly. Ale je velka skoda, CLOS a defgeneric su velmi uzitocne koncepty. Nezaoberal som sa s nim dost dlho na to aby som dokazal povedat ci zatvorky su alebo nie su problem, nikdy mi nestihli vadit, ale trochu to vyvolavalo zvlastne pocity. Takisto z makier sa vie zatocit hlava, ale ako vravim, neviem ci sa to da skusenostami prekonat alebo ci vacsina programatorov je na dostatocnej urovni aby to zvladla. Podobne som to mal aj s Haskellom, na ten som nemal ani dostatocnu motivaciu.