|
1 | 1 | "use strict";
|
2 | 2 |
|
3 |
| -exports.pureE = function (a) { |
4 |
| - return function () { |
5 |
| - return a; |
| 3 | + |
| 4 | +/* |
| 5 | +A computation of type `Effect a` in runtime is represented by a function which when |
| 6 | +invoked performs some effect and results some value of type `a`. |
| 7 | +
|
| 8 | +With trivial implementation of `Effect` we have an issue with stack usage, as on each `bind` |
| 9 | +you create new function which increases size of stack needed to execute whole computation. |
| 10 | +For example if you write `forever` recursively like this, stack will overflow: |
| 11 | +
|
| 12 | +``` purs |
| 13 | +forever :: forall a b. Effect a -> Effect b |
| 14 | +forever f = f *> forever f |
| 15 | +``` |
| 16 | +
|
| 17 | +Solution to the stack issue is to change runtime representation of Effect from function |
| 18 | +to some "free like structure" (Defunctionalization), for example if we were to write new |
| 19 | +Effect structure which is stack safe we could do something like this: |
| 20 | +
|
| 21 | +``` purs |
| 22 | +data EffectSafe a |
| 23 | + = Effect (Effect a) |
| 24 | + | Pure a |
| 25 | + | exists b. Map (b -> a) (EffectSafe b) |
| 26 | + | exists b. Apply (EffectSafe b) (EffectSafe (b -> a)) |
| 27 | + | exists b. Bind (b -> EffectSafe a) (EffectSafe b) |
| 28 | +``` |
| 29 | +implementing Functor Applicative and Monad instances would be trivial, and instead of |
| 30 | +them constructing new function they create new node of EffectSafe tree structure |
| 31 | +which then needs to be interpreted. |
| 32 | +
|
| 33 | +
|
| 34 | +We could implement `EffectSafe` in PS but then to get safety benefits everyone should |
| 35 | +start using it and doing FFI on such type will not be as easy as with `Effect` implemented |
| 36 | +with just a function. If we just change runtime representation of the `Effect` that it would |
| 37 | +brake all FFI related code, which we don't want to do. |
| 38 | +
|
| 39 | +So we need some way to achieve stack safety such that runtime representation is still a function. |
| 40 | +
|
| 41 | +hmmm... |
| 42 | +
|
| 43 | +In JS, function is an object, so we can set arbitrary properties on it. i.e. we can use function |
| 44 | +as object, like look up some properties without invoking it. It means we can use function as |
| 45 | +representation of `Effect`, as it was before, but set some properties on it, to be able get |
| 46 | +benefits of the free-ish representation. |
| 47 | +
|
| 48 | +So we would assume an `Effect a` to be normal effectful function as before, |
| 49 | +but it could also have `tag` property which could be 'PURE', 'MAP', 'APPLY' or 'BIND', |
| 50 | +depending on the tag, we would expect certain properties to contain certain type of values: |
| 51 | +
|
| 52 | +``` js |
| 53 | +Effect a |
| 54 | + = { Unit -> a } |
| 55 | + | { Unit -> a, tag: "PURE", _0 :: a } |
| 56 | + | { Unit -> a, tag: "MAP", _0 :: b -> a, _1 :: Effect b } |
| 57 | + | { Unit -> a, tag: "APPLY", _0 :: Effect b, _1 :: Effect (b -> a) } |
| 58 | + | { Unit -> a, tag: "BIND", _0 :: b -> Effect a, _1 :: Effect b } |
| 59 | +``` |
| 60 | +
|
| 61 | +Now hardest thing is to interpret this in stack safe way. but at first let's see |
| 62 | +how `pureE` `mapE` `applyE` `bindE` `runPure` are defined: |
| 63 | +*/ |
| 64 | + |
| 65 | +var PURE = "PURE"; |
| 66 | +var MAP = "MAP"; |
| 67 | +var APPLY = "APPLY"; |
| 68 | +var BIND = "BIND"; |
| 69 | +var APPLY_FUNC = "APPLY_FUNC"; |
| 70 | + |
| 71 | +exports.pureE = function (x) { |
| 72 | + return mkEff(PURE, x); |
| 73 | +}; |
| 74 | + |
| 75 | +exports.mapE = function (f) { |
| 76 | + return function (effect) { |
| 77 | + return mkEff(MAP, f, effect); |
6 | 78 | };
|
7 | 79 | };
|
8 | 80 |
|
9 |
| -exports.bindE = function (a) { |
| 81 | +exports.applyE = function (effF) { |
| 82 | + return function (effect) { |
| 83 | + return mkEff(APPLY, effect, effF); |
| 84 | + }; |
| 85 | +}; |
| 86 | + |
| 87 | +exports.bindE = function (effect) { |
10 | 88 | return function (f) {
|
11 |
| - return function () { |
12 |
| - return f(a())(); |
13 |
| - }; |
| 89 | + return mkEff(BIND, f, effect); |
14 | 90 | };
|
15 | 91 | };
|
16 | 92 |
|
| 93 | +/* |
| 94 | +
|
| 95 | +As you can see this function takes the `tag` and up to 2 values depending on the `tag`. |
| 96 | +in here we create new named function which invokes runEff with itself |
| 97 | +(we give it name so it's easy to identify such functions during debugging) |
| 98 | +then we set `tag`, `_0` and `_1` properties on the function we just constructed |
| 99 | +and return it so the result is basically an object which can also be invoked |
| 100 | +and it then executes `runEff` with itself which tries to evaluate it without |
| 101 | +increasing stack usage. |
| 102 | +
|
| 103 | +*/ |
| 104 | +function mkEff(tag, _0, _1) { |
| 105 | + var effect = function $effect() { return runEff($effect); }; |
| 106 | + effect.tag = tag; |
| 107 | + effect._0 = _0; |
| 108 | + effect._1 = _1; |
| 109 | + return effect; |
| 110 | +} |
| 111 | + |
| 112 | +/* |
| 113 | +
|
| 114 | +So when this function is called it will take effect which must have the `tag` property on it. |
| 115 | +
|
| 116 | +we would set up some variables which are needed for safe evaluation: |
| 117 | +
|
| 118 | +* operations - this will be a type aligned sequence of `Operations` which looks like this: |
| 119 | + ``` purs |
| 120 | + Operation a b |
| 121 | + = { tag: "MAP", _0 :: a -> b } |
| 122 | + | { tag: "APPLY", _0 :: Effect a } |
| 123 | + | { tag: "APPLY_FUNC", _0 :: a -> b } |
| 124 | + | { tag: "BIND", _0 :: a -> Effect b } |
| 125 | + ``` |
| 126 | +* effect - initially it's `inputEff` (argument of the `runEff`), it's basically tip of the tree, |
| 127 | + it will be then updated with other nodes while we are interpreting the structure. |
| 128 | +* res - it will store results of invocations of effects which return results |
| 129 | +* op - it will store current `Operation` which is popped from `operations` |
| 130 | +
|
| 131 | +if you look closely at Operation and Effect you would see that they have similar shape. |
| 132 | +this nodes from `Effect` have same representation as `Operation`: |
| 133 | +
|
| 134 | +``` |
| 135 | +| { Unit -> a, tag: "MAP", _0 :: b -> a, _1 :: Effect b } |
| 136 | +| { Unit -> a, tag: "APPLY", _0 :: Effect b, _1 :: Effect (b -> a) } |
| 137 | +| { Unit -> a, tag: "BIND", _0 :: b -> Effect a, _1 :: Effect b } |
| 138 | +``` |
| 139 | +*/ |
| 140 | + |
| 141 | +function runEff(inputEff) { |
| 142 | + var operations = []; |
| 143 | + var effect = inputEff; |
| 144 | + var res; |
| 145 | + var op; |
| 146 | + effLoop: for (;;) { |
| 147 | + if (effect.tag !== undefined) { |
| 148 | + if (effect.tag === MAP || effect.tag === BIND || effect.tag === APPLY) { |
| 149 | + operations.push(effect); |
| 150 | + effect = effect._1 ; |
| 151 | + continue; |
| 152 | + } |
| 153 | + // here `tag === PURE` |
| 154 | + res = effect._0; |
| 155 | + } else { |
| 156 | + res = effect(); |
| 157 | + } |
| 158 | + |
| 159 | + while ((op = operations.pop())) { |
| 160 | + if (op.tag === MAP) { |
| 161 | + res = op._0(res); |
| 162 | + } else if (op.tag === APPLY_FUNC) { |
| 163 | + res = op._0(res); |
| 164 | + } else if (op.tag === APPLY) { |
| 165 | + effect = op._0; |
| 166 | + operations.push({ tag: APPLY_FUNC, _0: res }); |
| 167 | + continue effLoop; |
| 168 | + } else { // op.tag === BIND |
| 169 | + effect = op._0(res); |
| 170 | + continue effLoop; |
| 171 | + } |
| 172 | + } |
| 173 | + return res; |
| 174 | + } |
| 175 | +} |
| 176 | + |
17 | 177 | exports.untilE = function (f) {
|
18 | 178 | return function () {
|
19 | 179 | while (!f());
|
|
0 commit comments