Proč sociologové programují v Lispu?
Článek připravil náš Backend Developer Radek Smola ve spolupráci s Liborem Mořkovským, CTO Behavia.
Pokud chcete dělat kvalitní výzkum, který bude bavit i respondenty, nesmíte se ptát na otázky, které se jich netýkají. Když váš respondent uvedl, že abstinuje, neměli byste se ho v dalším kroku ptát na oblíbenou značku piva. Podobné přešlapy obvykle vedou k tomu, že uživatel dotazník předčasně ukončí, nebo ho začne bezmyšlenkovitě proklikávat.
Proto v Behaviu používáme dva jednoduché jazyky, kterými dokážeme zobrazovat jen ty části dotazníku, které jsou pro uživatele relevantní. Abstinent tak otázku na značku piva nikdy neuvidí, i přesto, že je součástí dotazníku. S těmito podmínkovacími jazyky si poradí i sociologové. Dotazníky tak jsou schopni udělat efektivní a mnohem kratší.
Prvnímu z nich říkáme conDSL. Sociologové jím popisují, jak se budou respondentům zobrazovat otázky na základě odpovědí z aktuálního nebo i předchozích dotazníků. Tento jazyk se výborně hodí i na vybírání respondentů a návaznou analýzu dat. Práce s ním je tak snadná, že sociologové nepotřebují mít po ruce specialistu a zvládnou ho používat sami. Tím se odlišujeme od mnoha výzkumných agentur.
Kromě podmínek, které jsou vázané na data, občas potřebujeme zobrazovat otázky jenom podle náhody. Náš druhý jazyk – ranDSL – nám umožňuje strukturovaně randomizovat. Ve zkratce to znamená, že umíme náhodně zobrazit nejen jednu otázku z několika možností, ale třeba celý blok otázek, a uvnitř tohoto bloku ještě náhodné pořadí.
Pokud vás zaujala podobnost ve jménech obou mini-jazyků, DSL je zkratka pro domain specific language, v překladu tedy jazyk určený pro nějaký speciální úkol.
Ukažte mi nějaký kód!
Modelově si představme třeba již zmíněné abstinenty. Na začátku dotazníku se zeptáme na to, co všechno respondent pije. V případě, že nezmíní alkohol a vybere limonády, tak se mu budou zobrazovat otázky už jen na ně.
(is self.drinks limo)
Zmíní-li, že je fanouškem Kofoly, můžeme značku jednoduše přidat do podmínky.
(all
(is self.drinks limo)
(has self.limo kofola))
Respondent pak vidí otázky spojené jen s Kofolou.
Při analýze dat používáme stejný podmínkovací jazyk. Budeme-li tedy chtít vidět odpovědi lidí, kteří nepijí alkohol a mají rádi značku Kofola, použijeme stejnou podmínku.
(all
(is self.drinks limo)
(has self.limo kofola))
Zároveň však můžeme zabrousit i do větších detailů, třeba zohlednit věk. Na ten se nemusíme dotazovat opakovaně, můžeme na něj odkázat do již uložených informací o respondentech.
(all
(is self.drinks limo)
(has self.limo kofola)
(lt user.age 40))
Při dostatečně velkém vzorku můžeme jít ještě hlouběji. Například, kolik z nich nakupuje potraviny online, což jsme zjišťovali třeba v jiném dotazníku. V zápisu se tak nově objeví ID dotazníku. Self v těchto případech zastupuje aktuálně vyplňovaný dotazník.
(all
(is self.drinks limo)
(has self.limo kofola)
(lt user.age 40)
(has shopping.food online))
A teď si to ukážeme v praxi. Proklikejte si náš krátký dotazník. Podle toho, co si na začátku vyberte, tak se vám zobrazí další otázka. Když si vyberete víc tipů drinků, zobrazí se vám více otázek.
Jak je to implementované?
Jazyky conDSL a ranDSL vycházejí z Lispu a výrazy v nich jsou jednotlivé s-expressions, které nemají žádnou control flow. To znamená, že běžný for cyklus si v něm nenapíšete. Jazyk jsme zjednodušili tak, že nerozlišuje mezi symboly a řetězci, takže nemusíme identifikátory otázek dávat do uvozovek.
Vyhodnocovat výrazy v těchto jazycích potřebujeme v prostředí JavaScriptu a Pythonu:
- Rozesílka dotazníků vyhodnocuje koho oslovit na serveru v Pythonu.
- Jednotlivé otázky se zobrazují v aplikaci napsané v JavaScriptu na webovém frontendu.
- Pro analýzu sebraných dat zase používáme Python na backendu.
Z toho plyne, že potřebujeme implementaci pro Python a JavaScript.
Důležité je, aby zápis fungoval úplně stejně, bez ohledu na které platformě to pouštíme.
K tomu nám pomáhají dvě věci:
- parser generovaný pomocí gramatiky
- testy
Pro zápis gramatiky jsme použili PEG, protože je moderní a cool. Samotný parser generujeme pomocí Canopy parser compiler.
Na úrovni PEG gramatiky řešíme pouze parsování zjednodušených s-expressions, které jsou mezi oběma našima jazyky sdílené. Samotná funkcionalita už je ručně napsaná v „hostovském“ jazyce. Zde je stejnost funkcionality kontrolována testy.
Co je to Lisp? A proč Lisp?
Základní úvaha byla jednoduchá – když někdo neumí programovat vůbec, je trochu jedno, který jazyk se bude učit. Lisp se pro účel našeho malinkého jazyka nabízí hned z několika důvodů:
- je velmi jednoduché ho naprogramovat
- velmi uniformní syntaxe (co_dělám a (s čím)) se dobře vysvětluje
- obecnou filozofií Lispu je, že se „v něm naprogramuje jazyk, který řeší můj problém“
conDSL detailněji
Kromě samotného výrazu jsou vstupem pro vyhodnocení ještě aktuální data – kterou možnost zrovna respondent vybral.
Abychom oddělili implementaci jazyka od konkrétního uložení dat, které se liší podle toho jestli výraz spouštíme při cílení, vyplňování nebo analýze dat, dostává vyhodnocovací funkce jako argument callback, se signaturou zhruba „dej_mi_data_pro_proměnnou(jméno_proměnné)“.
Volání v Pythonu potom vypadá zjednodušeně takto:
storage = {"self.drinks": ["limo"]}def data_callback(name):
return storage[name]condsl.eval(expression, data_callback)
ranDSL detailněji
Kromě samotného randomizačního programu je vstupem seznam unikátních identifikátorů – například seznam jmen všech otázek v dotazníku, nebo seznam všech možných odpovědí v jedné otázce.
Abychom mohli v randomizačním programu zmínit jen otázky, které chceme randomizovat, používá náš randomizátor „heuristiku“ (snaha co nejlépe tipnout, co bylo záměrem) postavenou nad sloty. Pokud ve vstupním seznamu mám 10 otázek a 3 z nich jsou v randomizačním předpisu, výstup vytvořím jako kopii vstupního seznamu, kde jsou tři volná místa, do kterých „naleji“ výsledek randomizace.
Stabilní, časem i rychlejší
Rádi stavíme naše technologie na osvědčených řešeních, ale v době, kdy jsme potřebovali konzistentní podmínkovací jazyk nic dostupné nebylo. Tak jsme použili alespoň lety osvědčenou technologii pro stavění jazyků. Pár let poté sice Google přišel s mini jazykem CEL (common expression language), jehož uvedením zvalidoval náš přístup, conDSL ale nahrazovat nebudeme. Je už velmi stabilní a jsme na něj všichni zvyklí. S-expressions jsme si navíc neoblíbili jenom my – třeba GitHub na nich založil své řešení pro code tagging.
conDSL pouze výjimečně rozšíříme o novou funkci. Prostor na vylepšení máme v implementaci. V nejnáročnějším nasazení – při výpočtu 1000 vlastností pro desítky tisíc respondentů – už je znát nižší rychlost interpretovaného jazyka. Až budeme hledat technologickou výzvu na hackathon, tohle vidíme jako jasného kandidáta.