Haskell port of
github/scientist.
The following extensions are recommended:
{-# LANGUAGE OverloadedStrings #-}Most usage will only require the top-level library:
import Scientist-
Define a new
Experiment m c a bwith,ex0 :: Functor m => Experiment m c a b ex0 = newExperiment "some name" theOriginalCode
-
The type variables capture the following details:
m: Some Monad to operate in, e.g.IO.c: Any context value you attach withsetExperimentContext. It will then be available in theResult c a byou publish.a: The result type of your original (aka "control") code, this is what is always returned and so is the return type ofexperimentRun.b: The result type of your experimental (aka "candidate") code. It probably won't differ (and must not to use a comparison like(==)), but it can (provided you implement a comparison betweenaandb).
-
Configure the Experiment as desired
ex1 :: (Functor m, Eq a) => Experiment m c a a ex1 = setExperimentPublish publish0 $ setExperimentCompare experimentCompareEq $ setExperimentTry theExperimentalCode $ newExperiment "some name" theOriginalCode -- Increment Statsd, Log, store in Redis, whatever publish0 :: Result c a b -> m () publish0 = undefined
-
Run the experiment
run0 :: (MonadUnliftIO m, Eq a) => m a run0 = experimentRun $ setExperimentPublish publish0 $ setExperimentCompare experimentCompareEq $ setExperimentTry theExperimentalCode $ newExperiment "some name" theOriginalCode
-
Explore things like
setExperimentIgnore,setExperimentEnabled, etc.
The rest of this README matches section-by-section to the ported project and shows only the differences in syntax for those features. Please follow the header links for additional details, motivation, etc.
myWidgetAllows :: MonadUnliftIO m => Model -> User -> m Bool
myWidgetAllows model user = do
experimentRun
$ setExperimentTry
(userCanRead user model) -- new way
$ newExperiment "widget-permissions"
(modelCheckUserValid model user) -- old wayrun1 :: MonadUnliftIO m => m a
run1 =
experimentRun
$ setExperimentEnabled (pure True)
$ setExperimentOnException onScientistException
$ setExperimentPublish (liftIO . putStrLn . formatResult)
$ setExperimentTry theExperimentalCode
$ newExperiment "some name" theOriginalCode
onScientistException :: MonadIO m => SomeException -> m ()
onScientistException ex = do
liftIO $ putStrLn "..."
-- To re-raise
throwIO ex
formatResult :: Result c a b -> String
formatResult = undefinedrun2 :: MonadUnliftIO m => m [User]
run2 =
experimentRun
$ setExperimentCompare (experimentCompareOn $ map userLogin)
$ setExperimentTry userServiceFetch
$ newExperiment "users" fetchAllUsersWhen using experimentCompareOn, By, or Eq, if a candidate branch raises an
exception, that will never compare equally.
See setExperimentContext.
Just do it ahead of time.
run3 :: MonadUnliftIO m => m a
run3 = do
x <- expensiveSetup
experimentRun
$ setExperimentTry (theExperimentalCodeWith x)
$ newExperiment "expensive" (theOriginalCodeWith x)Not supported at this time. Format the value(s) as necessary when publishing.
See setExperimentIgnore.
See setExperimentRunIf.
run4 :: MonadUnliftIO m => m a
run4 =
experimentRun
$ setExperimentEnabled (experimentEnabledPercent 30)
$ setExperimentTry theExperimentalCode
$ newExperiment "some name" theOriginalCoderun5 :: MonadUnliftIO m => m User
run5 =
experimentRun
$ setExperimentPublish publish1
$ setExperimentTry theExperimentalCode
$ newExperiment "some name" theOriginalCode
publish1 :: MonadIO m => Result MyContext User User -> m ()
publish1 result = do
-- Details are present unless it's a ResultSkipped, which we'll ignore
for_ (resultDetails result) $ \details -> do
let eName = resultDetailsExperimentName details
-- Store the timing for the control value,
statsdTiming ("science." <> eName <> ".control")
$ resultControlDuration
$ resultDetailsControl details
-- for the candidate (only the first, see "Breaking the rules" below,
statsdTiming ("science." <> eName <> ".candidate")
$ resultCandidateDuration
$ resultDetailsCandidate details
-- and counts for match/ignore/mismatch:
case result of
ResultSkipped{} -> pure ()
ResultMatched{} -> do
statsdIncrement $ "science." <> eName <> ".matched"
ResultIgnored{} -> do
statsdIncrement $ "science." <> eName <> ".ignored"
ResultMismatched{} -> do
statsdIncrement $ "science." <> eName <> ".mismatched"
-- Finally, store mismatches in redis so they can be retrieved and
-- examined later on, for debugging and research.
storeMismatchData details
storeMismatchData :: Monad m => ResultDetails MyContext User User -> m ()
storeMismatchData details = do
let
eName = resultDetailsExperimentName details
eContext = resultDetailsExperimentContext details
payload = MyPayload
{ name = eName
, context = eContext
, control = controlObservationPayload $ resultDetailsControl details
, candidate = candidateObservationPayload $ resultDetailsCandidate details
, execution_order = resultDetailsExecutionOrder details
}
key = "science." <> eName <> ".mismatch"
redisLpush key $ toJSON payload
redisLtrim key 0 1000
controlObservationPayload :: ResultControl User -> Value
controlObservationPayload rc =
object ["value" .= cleanValue (resultControlValue rc)]
candidateObservationPayload :: ResultCandidate User -> Value
candidateObservationPayload rc = case resultCandidateValue rc of
Left ex -> object ["exception" .= displayException ex]
Right user -> object ["value" .= cleanValue user]
-- See "Keeping it clean" above
cleanValue :: User -> Text
cleanValue = userLoginSee Result, ResultDetails, ResultControl and ResultCandidate for all the
available data you can publish.
TODO: raise_on_mismatches
TODO: raise_with
Candidate code is wrapped in tryAny, resulting in Either SomeException
values in the result candidates list. We use the safer
UnliftIO.Exception module.
See setExperimentOnException.
nope0 :: Experiment m c a b -> Experiment m c a b
nope0 = setExperimentIgnore (\_ _ -> True)Or, more efficiently:
nope1 :: Experiment m c a b -> Experiment m c a b
nope1 = setExperimentCompare (\_ _ -> True)If you call setExperimentTry more than once, it will append (not overwrite)
candidate branches. If any candidate is deemed ignored or a mismatch, the
overall result will be.
setExperimentTryNamed can be used to give branches explicit names (otherwise,
they are "control", "candidate", "candidate-{n}"). These names are visible in
ResultControl, ResultCandidate, and resultDetailsExecutionOrder.
Not supported.
Supporting the lack of a Control branch in the types would ultimately lead to a
runtime error if you attempt to run such an Experiment without having and
naming a Candidate to use instead, or severely complicate the types to account
for that safely. In our opinion, this feature is not worth either of those.