Mario Román

Search

Search IconIcon to open search

Protocols run on trials

Last updated Apr 15, 2025

Researchers are not usually able to state formally the design of a clinical trial. We will soon see clinical trials possibly written by large language models, of drugs possibly designed by unexplainable artificial intelligence: developing clinical trials is a business that moves large quantities of money and it is difficult to safeguard. It becomes critical to be able to impose a structure to these trials, a structure that helps us align them with the public interest.

This repository is an experiment on structuring a clinical trial as an element of a free monad: it tries to convert the problem of designing clinical trials into a software engineering problem. Not because we will run a trial as if it were a computer program, but because we want a structured logical description of the trial that is more resilient to reality than any software that implements it. We need a rich expressive language and a way of handling this structure; hiding or ignoring structure has, so far, only led to poor software.

Errors are modularly handled by monad morphisms. An interpretation into the IO monad is provided, but it is not the final goal of this description.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Use newtype instead of data" #-}
{-# HLINT ignore "Use if" #-}
module ProtocolsRunOnTrials where

import Control.Monad.Free
import Control.Monad
import Control.Monad.Loops
import Data.List (isInfixOf)
import Data.Maybe (maybeToList)

-- Forms as a free monad.
type Form a = Free Question a
data Question y
  = Consent { consent :: Maybe Bool -> y }
  | DrugsTaken { nothing :: y , drug :: [Drug] -> y }
  | AdverseEffectClass { aeMinor :: y, aeMajor :: y, aeNo :: y }
  | AdverseEffectName { aeName :: AdverseEffect -> y }
  | AdverseEffectNarrative { aeNarrative :: String -> y }
  deriving (Functor)

data Drug = Loratadine | Montelukast | Cetirizine deriving Show
data AdverseEffectClass = AEMinor | AEMajor | AENone deriving Show
data AdverseEffect = Headache | Depression | Unclassified deriving Show

drugsForm :: Form [Drug]
drugsForm = ask $ DrugsTaken
  { drug = \d -> do
        rest <- drugsForm
        return (d ++ rest)
  , nothing = return [] }

inclusionForm :: Form (Bool, [Drug])
inclusionForm = do
  c <- askConsent
  case c of
    (Just True) -> do { d <- drugsForm ; return (True, d) }
    (Just False) -> do { return (False,[]) }
    Nothing -> do { return (False,[]) }

askAdverseEffectClass :: Form AdverseEffectClass
askAdverseEffectClass = liftF $ AdverseEffectClass AEMinor AEMajor AENone
askAdverseEffectName :: Form AdverseEffect
askAdverseEffectName = liftF $ AdverseEffectName id
askAdverseEffectNarrative :: Form String
askAdverseEffectNarrative = liftF $ AdverseEffectNarrative id

type Narrative = String
adverseEffectForm :: Form (AdverseEffectClass, Maybe (AdverseEffect, Narrative))
adverseEffectForm = do
  aeclass <- askAdverseEffectClass
  case aeclass of
    AEMajor -> pure (AEMajor, Nothing)
    AEMinor -> do
      aename <- askAdverseEffectName
      aenarrative <- askAdverseEffectNarrative
      return (aeclass, Just (aename, aenarrative))
    AENone -> pure (AENone, Nothing)


-- Clinical trial as a free monad
data Clinical y = InvalidBool (Bool -> y) | InvalidDrug ([Drug] -> y) | InvalidAdverseEffect (AdverseEffect -> y) | Concern String y | PutString String y | GetString (String -> y) deriving (Functor)
type ClinicalTrial a = Free Clinical a


-- Handling errors from the form during design of the clinical trial.
questionYesNo :: String -> ClinicalTrial (Maybe Bool)
questionYesNo questionString = do
  putString questionString
  s <- getString
  case s of
    "Yes" -> return (Just True)
    "No"  -> return (Just False)
    _ -> return Nothing

runsForm :: Form a -> ClinicalTrial a
runsForm (Pure a) = pure a

runsForm (Free (Consent cmaybebool)) = do
  b <- questionYesNo "Do you consent to participate in this study?"
  runsForm (cmaybebool b)

runsForm (Free (DrugsTaken dtno dtls)) = do
  putString "Are you taking any other drugs? If so, give the name."
  drug <- getString
  case drug of
    "No" -> runsForm dtno
    "Loratadine" -> runsForm (dtls [Loratadine])
    "Montelukast" -> runsForm (dtls [Montelukast])
    "Cetirizine" -> runsForm (dtls [Cetirizine])
    _ -> do
      d <- invalidDrug
      runsForm (dtls d)

runsForm (Free (AdverseEffectClass aeminor aemajor aeno)) = do
  b <- questionYesNo "Are you reporting an adverse event?"
  case b of
    Just True -> do
      b <- iterateWhile (== Nothing) $ questionYesNo "Are you reporting a MAJOR/unexpected adverse event?"
      case b of
        Just True -> runsForm aemajor
        Just False -> runsForm aeminor
        Nothing -> do
          concern "Possible adverse event was not reported. Question was not answered."
          runsForm aeno
    Just False -> runsForm aeno
    Nothing -> runsForm aeno

runsForm (Free (AdverseEffectName aename)) = do
  putString "What is the classification of the adverse event?"
  classification <- getString
  case classification of
    "Headache" -> runsForm (aename Headache)
    "Depression" -> runsForm (aename Depression)
    _ -> runsForm (aename Unclassified)

runsForm (Free (AdverseEffectNarrative aenarrative)) = do
  putString "Please describe the adverse event."
  narrative <- getString
  runsForm (aenarrative narrative)

exampleClinicalTrial :: ClinicalTrial (Bool, [Drug], [(AdverseEffect, String)])
exampleClinicalTrial = do
  (consent, concomitantMedication) <- runsForm inclusionForm
  if consent then do
    (aeclass, maybeAdverseEffectAndNarrative) <- runsForm adverseEffectForm
    aeList <- maybeToList <$> clinicalAdverseEventCollection
    return (consent, concomitantMedication, aeList)
  else
    return (False, [], [])

clinicalAdverseEventCollection :: ClinicalTrial (Maybe (AdverseEffect, String))
clinicalAdverseEventCollection = do
  (aeclass, maybeAdverseEffectAndNarrative) <- runsForm adverseEffectForm
  case (aeclass, maybeAdverseEffectAndNarrative) of
      (AEMajor, _) -> do
        concern "Major adverse effect has been raised"
        return $ Just (Unclassified, "")
      (AEMinor, Just (Unclassified, aenarrative)) ->
        if "Headache" `isInfixOf` aenarrative || "headache" `isInfixOf` aenarrative
          then return $ Just (Headache, aenarrative)
          else return $ Just (Unclassified, aenarrative)
      (AEMinor, Just (aename, aenarrative)) -> do
        return $ Just (aename, aenarrative)
      (AEMinor, Nothing) -> do
        return $ Just (Unclassified, "No description provided.")
      (AENone, _) -> return Nothing

-- Handling errors from the form on execution.
interpretTrial :: ClinicalTrial a -> IO a
interpretTrial (Free (PutString s y)) = do
  putStrLn s
  interpretTrial y
interpretTrial (Free (GetString f)) = do
  s <- getLine
  interpretTrial (f s)
interpretTrial (Free (InvalidBool g)) = do
  raise "missing boolean, set to False."
  interpretTrial (g True)
interpretTrial (Free (InvalidDrug d)) = do
  raise "missing drug, not registered in the list. Please ask the participant again."
  interpretTrial (d [])
interpretTrial (Free (InvalidAdverseEffect a)) = do
  raise "missing adverse effect. Recorded as unclassified. Please ask nurses for report."
  interpretTrial (a Unclassified)
interpretTrial (Free (Concern s y)) = do
  putStrLn $ "Clinical concern raised: " ++ s
  interpretTrial y
interpretTrial (Pure a) = pure a


--Boilerplate
putString :: String -> ClinicalTrial ()
putString xs = liftF $ PutString xs ()
getString :: ClinicalTrial String
getString = liftF $ GetString id
invalidBool :: ClinicalTrial Bool
invalidBool = liftF $ InvalidBool id
invalidAdverseEffect :: ClinicalTrial AdverseEffect
invalidAdverseEffect = liftF $ InvalidAdverseEffect id
invalidDrug :: ClinicalTrial [Drug]
invalidDrug = liftF $ InvalidDrug id
askConsent :: Form (Maybe Bool)
askConsent = liftF $ Consent id
concern :: String -> ClinicalTrial ()
concern s = liftF $ Concern s ()
ask :: f (Free f y) -> Free f y
ask = Free
end :: Free f ()
end = Pure ()


raise :: String -> IO ()
raise s = putStrLn $ "#WARNING RAISED: " ++ s

Tags: clinical trial, Haskell, free monad.