6.2.1 Studies and Steps
On this page:
study?
make-study
run-study
current-participant-id
6.2.1.1 Study variables
defvar
defvar*
defvar/  instance
defvar*/  instance
undefined?
if-undefined
call-with-study-transaction
with-study-transaction
6.2.1.2 Steps
step?
make-step
make-step/  study
next
next?
done
done?
6.2.1.3 Step Widgets
button
form
skip
when-bot
6.2.1.4 Study loops
for/  study

6.2.1 Studies and Steps🔗

 (require congame/components/study) package: congame-core

procedure

(study? v)  boolean?

  v : any/c
Returns #t when v is a study.

procedure

(make-study name    
  steps    
  [#:requires requires    
  #:provides provides    
  #:transitions transitions    
  #:view-handler view-handler    
  #:failure-handler failure-handler])  study?
  name : string?
  steps : (listof step?)
  requires : (listof symbol?) = null
  provides : (listof symbol?) = null
  transitions : (or/c #f (hash/c symbol? any/c)) = #f
  view-handler : (or/c #f (-> request? response?)) = #f
  failure-handler : (or/c #f (-> step? any/c step-id/c)) = #f
Creates a study that can be run by run-study. The name argument is used in debugging and in the admin interface. Any step that is part of the study has to be listed in steps.

The #:transitions argument has to be a transition-graph that provides all the transitions between the steps. See transition-graph for details.

procedure

(run-study s [req #:bindings bindings])  any/c

  s : study?
  req : request? = (current-request)
  bindings : (hash/c symbol? any/c) = (hasheq)
Runs the study s under req with bindings.

Returns the database ID of the current participant in the current study instance.

6.2.1.1 Study variables🔗

A study variable is a variable whose value is recorded in the study server database.

  • A variable with participant scope will store/reference a separate value for each participant in each separate instance.

  • A variable with instance scope will store/reference a separate value for each study instance, but the value is shared by all participants in a given study instance.

syntax

(defvar id)

syntax

(defvar* id global-id)

Defines a study variable with participant scope bound to id. The study variable can be accessed inside the study steps using id and updated with (set! id expr).

The value of the study variable will be stored in the Congame server database under the current study → instance → participant.

Study variables created with defvar* will additionally be visible to any child studies (see defstep/study).

Important: when using defvar*, you must provide a second identifier global-id and manually ensure it is distinct from any identifiers that may be used in child studies. This prevents child studies that may be using the same identifier names from accidentally overwriting your parent study’s variable.

syntax

(defvar/instance ...)

syntax

(defvar*/instance ...)

Like defvar and defvar*, but for creating study variables with instance scope that is, the stored value is shared by all participants in the study instance.

procedure

(undefined? v)  boolean?

  v : any/c
Returns #t if v is a study variable which has been created but not yet given any value, #f otherwise.

syntax

(if-undefined study-var alt)

Returns the current value of study-var if it has been given a value, or alt if study-var is undefined?.

procedure

(call-with-study-transaction proc)  any/c

  proc : (-> any/c)

syntax

(with-study-transaction expr ...)

Calls proc (or evaluates expr ... in such a way as to prevent any study variables from being updated by other participants until completed. The result of proc (or expr ...) becomes the result of the expression.

Important: You should use one of these forms when you’re doing multiple operations that depend on each other and which involve instance-scoped variables (that is, variables with instance scope, created with defvar/instance or defvar*/instance). Variables with instance scope can be updated by other participants at any time, so it is possible that the variable could change between the time when its value is read and when it is updated.

Example:

(defvar/instance group-score)
 
;; ...
 
(define (bad-update)
  (when (> score 100)
    ;; Bad: another participant could change the score before the next line runs!
    (set! score (+ score 50))))
 
(define (good-update)
  (with-study-transaction ; Prevents changes by anyone else during this code
    (when (> score 100)
      (set! score (+ score 50)))))

Note that each highlighted expression above is a separate reference to the score variable; without the use of a transaction, other study participants could change its value at any point between those expressions.

You don’t need with-study-transaction for single operations; a single read, or a single write of a literal value, is safe. But the moment you have a sequence of operations where later steps depend on earlier ones, and those steps touch instance variables, use with-study-transaction to keep everything atomic and consistent.

In concrete technical terms, call-with-study-transaction enters a database transaction with an isolation level of 'serializable.

6.2.1.2 Steps🔗

procedure

(step? v)  boolean?

  v : any/c
Returns #t when v is a step.

procedure

(make-step id    
  handler    
  [transition    
  #:view-handler view-handler    
  #:for-bot bot-handler])  step?
  id : symbol?
  handler : (-> xexpr?)
  transition : transition/c = (lambda () next)
  view-handler : (or/c #f (-> request? response?)) = #f
  bot-handler : (or/c #f procedure?) = #f

procedure

(make-step/study id    
  s    
  [transition    
  #:require-bindings require-bindings    
  #:provide-bindings provide-bindings])  step?
  id : symbol?
  s : study?
  transition : transition/c = (lambda () next)
  require-bindings : (listof binding/c) = null
  provide-bindings : (listof binding/c) = null
Creates a step that executes s when reached.

The #:require-bindings argument maps identifiers required by s to identifiers available in the current study context if the name is different – otherwise it assumes that required identifers share names and attempts to set them accordingly.

The #:provide-bindings argument maps identifiers in the current study that should be mapped to some subset of the identifiers provided by s upon completion. When #:provide-bindings is null?, no values are assigned.

For example:

(make-step/study
  'required-tasks
  task-study
  #:require-bindings '([n task-treatment])
  #:provide-bindings '([root-success? success?]))

Here, n in task-study will take on the value of task-treatment, and after running, root-success? will be assigned the value of success? in the parent.

canary

next : next?

procedure

(next? v)  boolean?

  v : any/c
A special value that can be used as a transition result to cause a study to transition to the next step, whatever step that may be.

The predicate next? returns #t if v is identical to next, #f otherwise.

canary

done : done?

procedure

(done? v)  boolean?

  v : any/c
A special value that can be used as a transition result to cause a transition to the end of the study.

The predicate done? returns #t if v is identical to done, #f otherwise.

6.2.1.3 Step Widgets🔗

procedure

(button action    
  label    
  [#:id id    
  #:to-step-id to-step-id])  xexpr?
  action : (-> void?)
  label : xexpr?
  id : string? = ""
  to-step-id : (or/c #f symbol?) = #f
Renders a button with the given label that executes action when pressed. After the action is executed, moves the participant to the step named by to-step-id or the next step if to-step-id is #f.

The #:id argument is useful for identifying the button within bot handlers.

procedure

(form f    
  action    
  render    
  [#:id id    
  #:enctype enctype    
  #:combine combine-proc    
  #:defaults defaults])  xexpr?
  f : form?
  action : (-> void?)
  render : (-> (widget-renderer/c) xexpr?)
  id : string? = ""
  enctype : string? = "multipart/form-data"
  combine-proc : (-> any/c any/c any/c any/c) = (λ (k v1 v2) v2)
  defaults : hash? = (hash)
Renders the form represented by f using render and executes action on successful submission, then continues to the next step in the study.

The #:id argument is useful for identifying the button within bot handlers.

procedure

(skip [to-step-id])  void?

  to-step-id : symbol? = #f
Skips to the step named by to-step-id or the next step in the study if to-step-id is #f.

syntax

(when-bot expr)

When the value of current-user-bot? is not #f, returns the result of expr converted to a string (in display mode), otherwise returns "".

6.2.1.4 Study loops🔗

 (require congame/components/for-study)
  package: congame-core

syntax

(for/study [#:substudies]
           [#:requires requires]
           [#:provides provides]
           (for-clause ...)
           body-or-break ... body)
 
  requires : (listof symbol?)
  provides : (listof symbol?)
Iterates like for but each result of the last body accumulated into a list of steps, which are passed to make-study to produce a study.

Use for/study for quickly building studies with many steps that differ in only a few places.

(for/study ([phase (in-list '("Setup" "Activation" "Tear-down"))])
  (page
    (haml
      (:h1 phase " Phase")
      (:p "...")
      (button void "Next"))))