-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathREADME.lhs
421 lines (311 loc) · 11.5 KB
/
README.lhs
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# Scientist
[![Hackage](https://img.shields.io/hackage/v/scientist.svg?style=flat)](https://hackage.haskell.org/package/scientist)
[![Stackage Nightly](http://stackage.org/package/scientist/badge/nightly)](http://stackage.org/nightly/package/scientist)
[![Stackage LTS](http://stackage.org/package/scientist/badge/lts)](http://stackage.org/lts/package/scientist)
[![CI](https://github.com/freckle/scientist-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/freckle/scientist-hs/actions/workflows/ci.yml)
Haskell port of
[`github/scientist`](https://github.com/github/scientist#readme).
## Usage
The following extensions are recommended:
```haskell
{-# LANGUAGE OverloadedStrings #-}
```
<!--
```haskell
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
module Main (module Main) where
import Prelude
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Data.Foldable (for_)
import Data.Text (Text)
import UnliftIO.Exception (SomeException, throwIO, displayException)
```
-->
Most usage will only require the top-level library:
```haskell
import Scientist
```
<!--
```haskell
import Text.Markdown.Unlit ()
theOriginalCode :: m a
theOriginalCode = undefined
theExperimentalCode :: m a
theExperimentalCode = undefined
```
-->
1. Define a new `Experiment m c a b` with,
```haskell
ex0 :: Functor m => Experiment m c a b
ex0 = newExperiment "some name" theOriginalCode
```
1. The type variables capture the following details:
- `m`: Some Monad to operate in, e.g. `IO`.
- `c`: Any context value you attach with `setExperimentContext`. It will then
be available in the `Result c a b` you publish.
- `a`: The result type of your original (aka "control") code, this is what is
always returned and so is the return type of `experimentRun`.
- `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 between `a` and `b`).
1. Configure the Experiment as desired
```haskell
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
```
1. Run the experiment
```haskell
run0 :: (MonadUnliftIO m, Eq a) => m a
run0 =
experimentRun
$ setExperimentPublish publish0
$ setExperimentCompare experimentCompareEq
$ setExperimentTry theExperimentalCode
$ newExperiment "some name" theOriginalCode
```
1. 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.
## [How do I science?](https://github.com/github/scientist#how-do-i-science)
<!--
```haskell
data Model = Model
data User = User
userLogin :: User -> Text
userLogin = undefined
userCanRead :: User -> Model -> m Bool
userCanRead = undefined
modelCheckUserValid :: Model -> User -> m Bool
modelCheckUserValid = undefined
```
-->
```haskell
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 way
```
## [Making science useful](https://github.com/github/scientist#making-science-useful)
```haskell
run1 :: 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 = undefined
```
### [Controlling comparison](https://github.com/github/scientist#controlling-comparison)
<!--
```haskell
userServiceFetch :: m [User]
userServiceFetch = undefined
fetchAllUsers :: m [User]
fetchAllUsers = undefined
```
--->
```haskell
run2 :: MonadUnliftIO m => m [User]
run2 =
experimentRun
$ setExperimentCompare (experimentCompareOn $ map userLogin)
$ setExperimentTry userServiceFetch
$ newExperiment "users" fetchAllUsers
```
When using `experimentCompareOn`, `By`, or `Eq`, if a candidate branch raises an
exception, that will never compare equally.
### [Adding context](https://github.com/github/scientist#adding-context)
See `setExperimentContext`.
### [Expensive setup](https://github.com/github/scientist#expensive-setup)
Just do it ahead of time.
<!--
```haskell
data X = X
expensiveSetup :: m X
expensiveSetup = undefined
theOriginalCodeWith :: X -> m a
theOriginalCodeWith = undefined
theExperimentalCodeWith :: X -> m a
theExperimentalCodeWith = undefined
```
-->
```haskell
run3 :: MonadUnliftIO m => m a
run3 = do
x <- expensiveSetup
experimentRun
$ setExperimentTry (theExperimentalCodeWith x)
$ newExperiment "expensive" (theOriginalCodeWith x)
```
### [Keeping it clean](https://github.com/github/scientist#keeping-it-clean)
Not supported at this time. Format the value(s) as necessary when publishing.
### [Ignoring mismatches](https://github.com/github/scientist#ignoring-mismatches)
See `setExperimentIgnore`.
### [Enabling/disabling experiments](https://github.com/github/scientist#enablingdisabling-experiments)
See `setExperimentRunIf`.
### [Ramping up experiments](https://github.com/github/scientist#ramping-up-experiments)
```haskell
run4 :: MonadUnliftIO m => m a
run4 =
experimentRun
$ setExperimentEnabled (experimentEnabledPercent 30)
$ setExperimentTry theExperimentalCode
$ newExperiment "some name" theOriginalCode
```
### [Publishing results](https://github.com/github/scientist#publishing-results)
<!--
```haskell
data Value = Value
data Pair = Pair
toJSON :: a -> Value
toJSON = undefined
(.=) :: Text -> a -> Pair
(.=) = undefined
object :: [Pair] -> Value
object = undefined
data MyContext = MyContext
data MyPayload = MyPayload
{ name :: Text
, context :: Maybe MyContext
, control :: Value
, candidate :: Value
, execution_order :: [Text]
}
statsdTiming :: Text -> a -> m ()
statsdTiming = undefined
statsdIncrement :: Text -> m ()
statsdIncrement = undefined
redisLpush :: Text -> Value -> m ()
redisLpush = undefined
redisLtrim :: Text -> Int -> Int -> m ()
redisLtrim = undefined
```
-->
```haskell
run5 :: 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 = userLogin
```
See `Result`, `ResultDetails`, `ResultControl` and `ResultCandidate` for all the
available data you can publish.
### [Testing](https://github.com/github/scientist#testing)
**TODO**: `raise_on_mismatches`
#### [Custom mismatch errors](https://github.com/github/scientist#custom-mismatch-errors)
**TODO**: `raise_with`
### [Handling errors](https://github.com/github/scientist#handling-errors)
#### [In candidate code](https://github.com/github/scientist#in-candidate-code)
Candidate code is wrapped in `tryAny`, resulting in `Either SomeException`
values in the result candidates list. We use the [safer][blog]
`UnliftIO.Exception` module.
[blog]: https://www.fpcomplete.com/haskell/tutorial/exceptions/
#### [In a Scientist callback](https://github.com/github/scientist#in-a-scientist-callback)
See `setExperimentOnException`.
## [Breaking the rules](https://github.com/github/scientist#breaking-the-rules)
### [Ignoring results entirely](https://github.com/github/scientist#ignoring-results-entirely)
```haskell
nope0 :: Experiment m c a b -> Experiment m c a b
nope0 = setExperimentIgnore (\_ _ -> True)
```
Or, more efficiently:
```haskell
nope1 :: Experiment m c a b -> Experiment m c a b
nope1 = setExperimentCompare (\_ _ -> True)
```
### [Trying more than one thing](https://github.com/github/scientist#trying-more-than-one-thing)
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`.
### [No control, just candidates](https://github.com/github/scientist#no-control-just-candidates)
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.
<!--
```haskell
main :: IO ()
main = pure ()
```
-->
---
[LICENSE](./LICENSE) | [CHANGELOG](./CHANGELOG.md)