Praktijk-voorbeeld

Om u een idee te geven van wat het scripten van een plugin inhoudt, in vergelijking met de directe aanpak die u in voorgaande hoofdstukken heeft gezien, maken we opnieuw een volledige t-test plugin aan -- deze keer met alleen de R-functies van het rkwarddev pakket.

Tip

Het pakket voegt een nieuwe GUI-dialoog toe aan RKWard in het menu BestandExporterenRKWard plugin-script aanmaken. Uit de naam blijkt, dat u kale (skeleton) plugins kunt aanmaken, die nader kunnen worden uitgewerkt. Deze plugin werd op zijn beurt gegenereerd door een rkwarddev-script die u kunt vinden in de demo-directory van het geïnstalleerde pakket en pakketbronnen (sources), als een extra voorbeeld. U kunt het ook laten werken met de aanroep (calling) demo("skeleton_dialog")

Beschrijving van de GUI

U zult onmiddellijk merken dat de manier van werken heel anders is: In tegenstelling tot het direct schrijven van de XML-code, begint u niet met de <document>-definitie, maar met de plugin- elementen die u in de dialoog wilt hebben. U kunt elk interface- element toekennen -- of het nu keuzevakjes, neerklapmenu's, slots voor variabelen of wat anders zijn -- aan individuele R-objecten, en daarna deze objecten combineren met de actuele GUI. Het pakket heeft functies voor elke XML tag die kan worden gebruikt voor het definiëren van de plugin-GUI, en de meeste daarvan hebben dezelfde naam, maar dan met het voorvoegsel rk.XML.*. Bijvoorbeeld kan het definiëren van een <varselector> en twee <varslot>-elementen voor de variabelen "x" en "y" van het t-test voorbeeld worden gedaan met:

variables <- rk.XML.varselector(id.name="vars")
var.x <- rk.XML.varslot("compare", source=variables, types="number", required=TRUE, id.name="x")
var.y <- rk.XML.varslot("against", source=variables, types="number", required=TRUE, id.name="y")
                

Het interessantste hierbij is waarschijnlijk source=variables: een prominente eigenschap van het pakket is dat alle functies automatische ID's kunnen genereren, zodat u niet zelf id-waarden hoeft te bedenken, of eraan te denken dat ze naar een specifiek plugin-element moeten verwijzen. U kunt eenvoudig verwijzen naar de R-objecten, omdat alle functies die een ID nodig hebben van enig ander element die ook uit deze objecten kunnen lezen. rk.XML.varselector() is een beetje bijzonder, omdat die gewoonlijk geen inhoud (content) heeft waaruit een ID kan worden verkregen (het is mogelijk, maar alleen als u een naam opgeeft), dus moeten we zelf een ID-naam instellen. Maar rk.XML.varslot() zou hier geen id.name argumenten nodig hebben, en dus is dit voldoende:

variables <- rk.XML.varselector(id.name="vars")
var.x <- rk.XML.varslot("compare", source=variables, types="number", required=TRUE)
var.y <- rk.XML.varslot("against", source=variables, types="number", required=TRUE)
                

Om alle voorbeeldcode precies zo opnieuw aan te maken zou u alle ID-waarden handmatig moeten instellen. Maar omdat het pakket het ons gemakkelijker moet maken, maken we ons daarover geen zorgen meer.

Tip

rkwarddev kan veel automatiseren bij het compileren van uw plugins. Maar het kan de voorkeur hebben dit niet volledig te gebruiken. Als u als doel heeft niet alleen werkende code te produceren, maar ook dat die door een mens gemakkelijk kan worden gelezen en vergeleken met uw genererende script, moet u overwegen altijd zelfverklarende ID's (namen) in te stellen met id.name. Als u uw R-objecten dezelfde namen geeft helpt dit ook met het verkrijgen van script-code die gemakkelijk(er) te begrijpen is.

Indien u wilt zien hoe de XML-code van het gedefinieerde element eruit ziet als u het naar een bestand exporteert, moet u het object met zijn naam aanroepen (call by name). Dus, als u nu var.x aanroept in uw R-sessie, moet u iets zien dat er ongeveer zo uitziet:

<varslot id="vrsl_vergelijk" label="vergelijken" source="vars" types="number" required="true" />
                

Sommige tags zijn alleen nuttig in de context van andere. Daarom, bijvoorbeeld, vindt u geen functie voor de <option>-tag. In plaats daarvan worden beide radioknoppen en neerklapmenu's gedefinieerd met inbegrip van hun opties als een lijst met naam (named list) waarin de namen gelijk zijn aan de de in de dialoog getoonde namen, en hun waardes een vector zijn met naam (named vector) die twee ingangen kan hebben, val voor de waarde van een optie, en de boolean chk om op te geven of deze optie standaard aan is.

test.hypothesis <- rk.XML.radio("met test-hypothese",
        options=list(
                "Twee-zijdig"=c(val="two.sided"),
                "Eerste is groter"=c(val="greater"),
                "Tweede is groter"=c(val="less")
        )
)
                

Het resultaat ziet er als volgt uit:

<radio id="rad_usngtsth" label="met test-hypothese">
        <option label="Twee-zijdig" value="two.sided" />
        <option label="Eerste is groter" value="greater" />
        <option label="Tweede is groter" value="less" />
</radio>
                

Alles wat ontbreekt voor de elementen van het tabblad Basisinstellingen is het keuzevakje voor gepaarde voorbeelden, en het structureren van al deze elementen in rijen en kolommen:

check.paired <- rk.XML.cbox("Gepaard voorbeeld", value="1", un.value="0")
basic.settings <- rk.XML.row(variables, rk.XML.col(var.x, var.y, test.hypothesis, check.paired))
                

rk.XML.cbox() is een zeldzame uitzondering, waarin de functienaam niet de volledige tag-naam bevat, om wat typewerk uit te sparenvoor dit veelgebruikte element. De basic.settings (basisinstellingen) bevat nu:

<row id="row_vTFSPP10TF">
        <varselector id="vars" />
        <column id="clm_vrsTFSPP10">
                <varslot id="vrsl_compare" label="vergelijk" source="vars" types="number" required="true" />
                <varslot id="vrsl_against" label="met" i18n_context="vergelijken met" source="vars" types="number" required="true" />
                <radio id="rad_usngtsth" label="met test-hypothese">
                        <option label="Twee-zijdig" value="two.sided" />
                        <option label="Eerste is groter" value="greater" />
                        <option label="Tweede is groter" value="less" />
                </radio>
                <checkbox id="chc_Pardsmpl" label="Gepaard voorbeeld" value="1" value_unchecked="0" />
        </column>
</row>
                

Op een soortgelijke manier maken de volgende regels R-objecten aan voor de elementen van het tabblad Opties, voor functies voor spinvelden, frames en stretch:

check.eqvar <- rk.XML.cbox("gelijke varianties aannemen", value="1", un.value="0")
conf.level <- rk.XML.spinbox("vertrouwensniveau", min=0, max=1, initial=0.95)
check.conf <- rk.XML.cbox("druk vertrouwensniveau af", val="1", chk=TRUE)
conf.frame <- rk.XML.frame(conf.level, check.conf, rk.XML.stretch(), label="Vertrouwensinterval")
                

Hierna is alles wat we nog moeten doen de objecten samen in een tabbook zetten en dat in een dialoogsectie plaatsen:

full.dialog <- rk.XML.dialog(
        label="t-Test met twee variabelen",
        rk.XML.tabbook(tabs=list("Basisinstellingen"=basic.settings, "Opties"=list(check.eqvar, conf.frame)))
)
                

We kunnen ook nog de assistent-sectie aanmaken met zijn twee pagina's met dezelfde objecten, dus hun ID's worden gehaald voor de <copy>-tags:

full.wizard <- rk.XML.wizard(
                label="Twee-variabelen t-Test",
                rk.XML.page(
                        rk.XML.text("Ten eerste, selecteer de twee te vergelijken variabelen. En geef op, welke volgens uw theorie de grootste is. Selecteer twee-zijdig,
                                als uw theorie niet uitwijst, welke variabele de grootste is."),
                        rk.XML.copy(basic.settings)),
                rk.XML.page(
                        rk.XML.text("Hieronder zijn wat gevorderde opties. Het  is gewoonlijk veilig niet aan te nemen dat
                                de varianties van de variabelen gelijk zijn. Een toepasselijke correctie wordt in dat geval toegepast.
                                Maar het kiezen van  \"gelijke varianties aannemen\" kan de sterkte van de test gunstig beïnvloeden."),
                        rk.XML.copy(check.eqvar),
                        rk.XML.text("Soms is het verkrijgen nuttig van een schatting van het betrouwbaarheidsinterval van 
                                het verschil van de gemiddelde waarden. Hieronder kunt u opgeven of er een moet worden getoond, en
                                welk vertrouwensniveau moet worden toegepast (95% komt overeen met een 5%
                                significantie-niveau)."),
                        rk.XML.copy(conf.frame)))
                

Voor zover de GUI. Het gehele document wordt tenslotte gecombineerd met rk.plugin.skeleton().

JavaScript-code

Tot dusver heeft het gebruiken van het rkwarddev-pakket naar het lijkt niet zo veel nut gehad. Dit gaat nu echt veranderen.

Ten eerste, precies zoals we niet hoefden te zorgen voor ID's voor elementen bij het maken van de indeling van de GUI, hoeven we nu niet te zorgen voor de namen van variabelen in JavaScript in de volgende stap. Indien u wat meer controle wilt, kunt u gewone JavaScript-code schrijven, en dit plakken in het gegenereerde bestand. Maar het is waarschijnlijk efficiënter het op de rkwarddev- manier te doen.

U hoeft werkelijk geen enkele variabele zelf te definiëren, omdat de rk.plugin.skeleton() uw XML-code kan scannen en automatisch alle variabelen kan definiëren die u mogelijk nodig heeft -- bijvoorbeeld, u hoeft niet te zorgen voor een keuzevakje als u die later toch niet nodig heeft. Dus kunnen we direct beginnen met het schrijven van de actuele R-code voor het genereren van JS:

Tip

De functie rk.JS.scan() kan ook bestaande XML-bestanden scannen, op zoek naar variabelen.

Het pakket heeft ook een aantal functies voor JS code constructs die gewoonlijk worden gebruikt in RKWard-plugins, zoals de echo()-functie of if() {...} else {...} condities. Er zijn enkele verschillen tussen JS en R, bijv., bij paste() in R wordt de komma gebruikt voor het aan elkaar plakken van karakter-strings, waar bij echo() in JS daarvoor de + wordt gebruikt, en regels moeten worden afgesloten met een puntkomma. Door R-functies te gebruiken, kunt u deze verschillen bijna vergeten en R-code blijven schrijven.

Deze functies hebben verschillende classes van invoerobjecten nodig: of gewone tekst, R-objecten zoals hier boven met XML-code, of op hun beurt resultaten van enige andere JS-functies in het pakket. Uiteindelijk roept u altijd rk.paste.JS() aan, die zich net zo gedraagt als paste(), maar afhankelijk van de invoerobjecten vervangt het die door hun XML-ID, variabelenaam in JavaScript of zelfs complete JavaScript-code.

In het t-test voorbeeld hebben we twee JS-objecten nodig: een voor de berekening van de resultaten, en een om die af te drukken met de printout() functie:

JS.calc <- rk.paste.JS(
        echo("res <- t.test (x=", var.x, ", y=", var.y, ", hypothesis=\"", test.hypothesis, "\""),
        js(
                if(check.paired){
                        echo(", paired=TRUE")
                },
                if(!check.paired && check.eqvar){
                        echo(", var.equal=TRUE")
                },
                if(conf.level != "0.95"){
                        echo(", conf.level=", conf.level)
                },
                linebreaks=TRUE
        ),
        echo(")\n"),
        level=2
)

JS.print <- rk.paste.JS(echo("rk.print (res)\n"), level=2)
                

Zoals u ziet geeft rkwarddev ook een R- implementatie van de echo() functie. Die geeft precies één karakterstring terug met een geldige JS-versie van zichzelf. U merkt misschien ook op dat hierin alle R-objecten dezelfde zijn als die we eerder hebben aangemaakt. Die worden automatisch vervangen door hun variabelenamen, dus moet dit heel intuïtief zijn. Telkens wanneer u alleen deze vervanging wenst, kan de functie id() worden gebruikt, die ook precies één karakterstring teruggeeft voor alle erin ingevoerde objecten (u zou kunnen zeggen dat die zich net zo gedraagt als paste(), met een zeer specifieke object-substitutie.

De js()-functie is een wrapper (functie waarin een andere functie is "verpakt") waarmee u R's if(){...} else {...} condities op dezelfde manier kunt gebruiken zoals u gewend bent. Die wordt direct naar JS-code vertaald. Hierin worden ook enkele operators zoals <, >= of || behouden, zodat u uw R-objecten logisch met elkaar kunt vergelijken zonder de noodzaak ze steeds weer tussen aanhalingstekens te plaatsen. Bekijken we het resulterende JS.calc-object, dat nu een karakterstring met inhoudt bevat:

echo("res <- t.test (x=" + vrslCompare + ", y=" + vrslAgainst + ", hypothesis=\"" + radUsngtsth + "\"");
        if(chcPardsmpl) {
                echo(", paired=TRUE");
        } else {}
        if(!chcPardsmpl && chcAssmqlvr) {
                echo(", var.equal=TRUE");
        } else {}
        if(spnCnfdnclv != "0.95") {
                echo(", conf.level=" + spnCnfdnclv);
        } else {}
        echo(")\n");
                

Opmerking

U kunt ook voor if() condities, genest in js(), de functie ite() gebruiken, die zich net zo gedraagt als R's ifelse(). Echter, conditie-statements gemaakt met ite() zijn gewoonlijk moeilijk leesbaar, en zouden waar mogelijk door js() moeten worden vervangen.

Plugin map

Deze sectie is heel kort: we hoeven helemaal geen .pluginmap te schrijven, omdat die automatisch kan worden gegenereerd door rk.plugin.skeleton(). De menu-hiërarchie kan worden opgegeven via de pluginmap-optie:

[...]
        pluginmap=list(
                name="Twee-variabele t-Test",
                hierarchy=list("analyse", "gemiddelden", "t-Test"))
        [...]
                        

Help-pagina

Ook deze sectie is heel kort: rk.plugin.skeleton() kan geen volledige help-pagina aanmaken met de beschikbare informatie. Maar die kan wel het XML-document scannen naar elementen die mogelijk thuishoren in een help-pagina, en automatisch een sjabloon (template) maken voor de help-pagina van onze plugin. We moeten daarna voor elk daarvan wat regels tekst toevoegen.

Tip

De functie rk.rkh.scan() kan ook bestaande XML- bestanden scannen en daarna een kaal help-bestand aanmaken.

De plugin-bestanden genereren

Nu komt de laatste stap, waarin we alle gegenereerde objecten overdragen aan rk.plugin.skeleton():

plugin.dir <- rk.plugin.skeleton("t-Test",
        xml=list(
                dialog=full.dialog,
                wizard=full.wizard),
        js=list(
                results.header="Twee-variabele t-Test",
                calculate=JS.calc,
                printout=JS.print),
        pluginmap=list(
                name="Twee-variabele t-Test",
                hierarchy=list("analyse", "gemiddelden", "t-Test")),
        load=TRUE,
        edit=TRUE,
        show=TRUE)
                        

Standaard worden de bestanden aangemaakt in een tijdelijke directory. De laatste drie opties zijn niet nodig, maar wel erg handig: load=TRUE plaatst automatisch de nieuwe plugin in RKWards configuratie (omdat het in een tijdelijke directory is en dus ophoudt te bestaan als RKWard wordt afgesloten, wordt het automatisch weer verwijderd door RKWard bij de volgende start), edit=TRUE opent alle aangemaakte bestanden voor bewerking in RKWard's editor tabs, en show=TRUE probeert direct de plugin te starten, zodat u die kunt bekijken zonder een enkele klik. U zou kunnen overwegen overwrite=TRUE toe te voegen, indien u uw script herhaaldelijk wilt starten (bijv. na wijzigingen in de code), omdat standaard bestanden niet worden overschreven.

Het resulterende object plugin.dir bevat het pad naar de directory waarin de plugin is aangemaakt. Dit kan nuttig zijn, samen met de functie rk.build.package(), voor het aanmaken van een actueel R-pakket om uw plugin met anderen te delen -- bijv. door die te sturen naar het RKWard development team, zodat die kan worden toegevoegd aan uw plugin repository.

Het volledige script

Om al het bovenstaande samen te vatten, volgt hier het volledige script voor een werkend voorbeeld van de t-test. Behalve de reeds uitgelegde code, leest het ook het pakket in, indien nodig, en gebruikt het de local() omgeving, zodat niet alle aangemaakte objecten terecht komen in uw huidige workspace (behalve plugin.dir). Hierin is door de mij, vertaler, NIETS gewijzigd, zodat u dit ook als het origineel kunt beschouwen van de wel vertaalde delen:

require(rkwarddev)

local({
        variables <- rk.XML.varselector(id.name="vars")
        var.x <- rk.XML.varslot("compare", source=variables, types="number", required=TRUE)
        var.y <- rk.XML.varslot("against", source=variables, types="number", required=TRUE)
        test.hypothesis <- rk.XML.radio("using test hypothesis",
                options=list(
                        "Two-sided"=c(val="two.sided"),
                        "First is greater"=c(val="greater"),
                        "Second is greater"=c(val="less")
                )
        )
        check.paired <- rk.XML.cbox("Paired sample", value="1", un.value="0")
        basic.settings <- rk.XML.row(variables, rk.XML.col(var.x, var.y, test.hypothesis, check.paired))

        check.eqvar <- rk.XML.cbox("assume equal variances", value="1", un.value="0")
        conf.level <- rk.XML.spinbox("confidence level", min=0, max=1, initial=0.95)
        check.conf <- rk.XML.cbox("print confidence interval", val="1", chk=TRUE)
        conf.frame <- rk.XML.frame(conf.level, check.conf, rk.XML.stretch(), label="Confidence Interval")

        full.dialog <- rk.XML.dialog(
                label="Two Variable t-Test",
                rk.XML.tabbook(tabs=list("Basic settings"=basic.settings, "Options"=list(check.eqvar, conf.frame)))
        )

        full.wizard <- rk.XML.wizard(
                        label="Two Variable t-Test",
                        rk.XML.page(
                                rk.XML.text("As a first step, select the two variables you want to compare against
                                        each other. And specify, which one you theorize to be greater. Select two-sided,
                                        if your theory does not tell you, which variable is greater."),
                                rk.XML.copy(basic.settings)),
                        rk.XML.page(
                                rk.XML.text("Below are some advanced options. It's generally safe not to assume the
                                        variables have equal variances. An appropriate correction will be applied then.
                                        Choosing \"assume equal variances\" may increase test-strength, however."),
                                rk.XML.copy(check.eqvar),
                                rk.XML.text("Sometimes it's helpful to get an estimate of the confidence interval of
                                        the difference in means. Below you can specify whether one should be shown, and
                                        which confidence-level should be applied (95% corresponds to a 5% level of
                                        significance)."),
                                rk.XML.copy(conf.frame)))

        JS.calc <- rk.paste.JS(
                echo("res <- t.test (x=", var.x, ", y=", var.y, ", hypothesis=\"", test.hypothesis, "\""),
                js(
                        if(check.paired){
                        echo(", paired=TRUE")
                        },
                        if(!check.paired && check.eqvar){
                        echo(", var.equal=TRUE")
                        },
                        if(conf.level != "0.95"){
                        echo(", conf.level=", conf.level)
                        },
                        linebreaks=TRUE
                ),
                echo(")\n"), level=2)

        JS.print <- rk.paste.JS(echo("rk.print (res)\n"), level=2)

        plugin.dir <<- rk.plugin.skeleton("t-Test",
                xml=list(
                        dialog=full.dialog,
                        wizard=full.wizard),
                js=list(
                        results.header="Two Variable t-Test",
                        calculate=JS.calc,
                        printout=JS.print),
                pluginmap=list(
                        name="Two Variable t-Test",
                        hierarchy=list("analysis", "means", "t-Test")),
                load=TRUE,
                edit=TRUE,
                show=TRUE,
                overwrite=TRUE)
})