diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..d584654 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "parserOptions": { + "ecmaVersion": 5 + }, + "extends": "eslint:recommended", + "env": { + "commonjs": true + }, + "rules": { + "strict": [2, "global"], + "block-scoped-var": 2, + "consistent-return": 2, + "eqeqeq": [2, "smart"], + "guard-for-in": 2, + "no-caller": 2, + "no-extend-native": 2, + "no-loop-func": 2, + "no-new": 2, + "no-param-reassign": 2, + "no-return-assign": 2, + "no-unused-expressions": 2, + "no-use-before-define": 2, + "radix": [2, "always"], + "indent": [2, 2, { "SwitchCase": 1 }], + "quotes": [2, "double"], + "semi": [2, "always"] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7050558 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.* +!/.gitignore +!/.eslintrc.json +!/.travis.yml +/bower_components/ +/node_modules/ +/output/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..316e760 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: node_js +dist: trusty +sudo: required +node_js: stable +env: + - PATH=$HOME/purescript:$PATH +install: + - TAG=$(wget -q -O - https://github.com/purescript/purescript/releases/latest --server-response --max-redirect 0 2>&1 | sed -n -e 's/.*Location:.*tag\///p') + - wget -O $HOME/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$TAG/linux64.tar.gz + - tar -xvf $HOME/purescript.tar.gz -C $HOME/ + - chmod a+x $HOME/purescript + - npm install -g bower + - npm install + - bower install --prod +script: + - npm run -s build + - bower install + - npm run -s test +after_success: +- >- + test $TRAVIS_TAG && + echo $GITHUB_TOKEN | pulp login && + echo y | pulp publish --no-push diff --git a/README.md b/README.md new file mode 100644 index 0000000..957b33d --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# purescript-ef EXPERIMENTAL + +[![Latest release](http://img.shields.io/github/release/safareli/purescript-ef.svg)](https://github.com/safareli/purescript-ef/releases) +[![Build status](https://travis-ci.org/safareli/purescript-ef.svg?branch=master)](https://travis-ci.org/safareli/purescript-ef) + +Faster and safer implementation of the Effect monad. + +## Ef vs Eff + +`Ef` is faster then `Eff`, plus it's stacksafe. Also it's faster to type :p + +## Installation + +``` +bower install purescript-ef +``` + +## Documentation + +Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-ef). diff --git a/bench/Bench/Main.js b/bench/Bench/Main.js new file mode 100644 index 0000000..0001a5b --- /dev/null +++ b/bench/Bench/Main.js @@ -0,0 +1,20 @@ +"use strict"; + +exports.mkArr = function(){ + return { count: 0 }; +}; + +exports.pushToArr = function(xs) { + return function(x) { + return function() { + xs.count += 1 + return xs; + }; + }; +}; + +exports.log = function(x) { + return function(){ + console.log(x) + } +}; \ No newline at end of file diff --git a/bench/Bench/Main.purs b/bench/Bench/Main.purs new file mode 100644 index 0000000..7b6b489 --- /dev/null +++ b/bench/Bench/Main.purs @@ -0,0 +1,140 @@ +module Bench.Main where + +import Prelude + +import Control.Monad.Ef (Ef) +import Control.Monad.Ef.Class (liftEf) +import Control.Monad.Eff (Eff) +import Control.Monad.Aff (Aff, launchAff_) +import Control.Monad.Eff.Class (class MonadEff, liftEff) +import Control.Monad.Eff.Console (CONSOLE) +import Control.Monad.Eff.Unsafe (unsafePerformEff) +import Data.Traversable (for_, intercalate) +import Performance.Minibench (BenchResult, benchWith', withUnits) + + +type BenchEff = (console :: CONSOLE) + +testApply :: forall m. MonadEff BenchEff m => Int -> m Unit +testApply n' = do + arr <- liftEff mkArr + applyLoop (void <<< liftEff <<< pushToArr arr) n' + where + applyLoop :: Monad m => (Int -> m Unit) -> Int -> m Unit + applyLoop eff max = go (pure unit) 0 + where + go acc n | n == max = acc + go acc n = go (acc <* eff n) (n + 1) + + +testBindRight :: forall m. MonadEff BenchEff m => Int -> m Unit +testBindRight n' = do + arr <- liftEff mkArr + bindRightLoop (void <<< liftEff <<< pushToArr arr) n' + where + bindRightLoop :: Monad m => (Int -> m Unit) -> Int -> m Unit + bindRightLoop eff max = go (pure unit) 0 + where + go acc n | n == max = acc + go acc n = go (eff (max - n - 1) >>= const acc) (n + 1) + + +testBindLeft :: forall m. MonadEff BenchEff m => Int -> m Unit +testBindLeft n' = do + arr <- liftEff mkArr + bindLeftLoop (void <<< liftEff <<< pushToArr arr) n' + where + bindLeftLoop :: Monad m => (Int -> m Unit) -> Int -> m Unit + bindLeftLoop eff max = go (pure unit) 0 + where + go acc n | n == max = acc + go acc n = go (acc >>= const (eff n)) (n + 1) + + +testMap :: forall m. MonadEff BenchEff m => Int -> m Unit +testMap n = do + arr <- liftEff mkArr + res <- mapLoop n (liftEff $ pushToArr arr 0) + pure unit + where + mapLoop :: Monad m => Int -> m Int -> m Int + mapLoop max i = + if max == 0 + then i + else mapLoop (max - 1) (map (_ + 1) i) + + +main :: Eff BenchEff Unit +main = do + log header + bench3 ">>=R" testBindRight testBindRight testBindRight [100, 500, 1000, 2000, 4000, 8000, 10000] + bench3 ">>=L" testBindLeft testBindLeft testBindLeft [100, 500, 1000, 2000, 4000, 8000] + bench3 "map" testMap testMap testMap [100, 500, 1000, 2000, 4000, 5000] + bench3 "apply" testApply testApply testApply [100, 500, 1000, 2000, 4000, 5000] + +extended :: Eff BenchEff Unit +extended = do + log header + bench2 ">>=R" testBindRight testBindRight [20000, 50000, 100000, 1000000] + bench2 ">>=L" testBindLeft testBindLeft [20000, 50000, 100000, 1000000] + bench2 "map" testMap testMap [10000, 20000, 50000, 100000, 1000000, 350000, 700000] + bench2 "apply" testApply testApply [10000, 20000, 50000, 100000, 1000000] + +header :: String +header = + "| bench | type | n | mean | stddev | min | max |\n" <> + "| ----- | ---- | - | ---- | ------ | --- | --- |" + +bench3 + :: String + -> (Int -> Eff BenchEff Unit) + -> (Int -> Ef BenchEff Unit) + -> (Int -> Aff BenchEff Unit) + -> Array Int + -> Eff BenchEff Unit +bench3 name buildEff buildEf buildAff vals = for_ vals \val -> do + logBench [name <> " build", "Eff", show val] $ benchWith' 1000 \_ -> buildEff val + logBench [name <> " build", "Aff", show val] $ benchWith' 1000 \_ -> buildAff val + logBench' [name <> " build", "Ef", show val] $ benchWith' 1000 \_ -> buildEf val + let eff = liftEff $ buildEff val + logBench [name <> " run", "Eff", show val] $ benchWith' 1000 \_ -> unsafePerformEff eff + let aff = launchAff_ $ buildAff val + logBench [name <> " run", "Aff", show val] $ benchWith' 1000 \_ -> unsafePerformEff aff + let ef = liftEf $ buildEf val + logBench' [name <> " run", "Ef", show val] $ benchWith' 1000 \_ -> unsafePerformEff ef + +bench2 + :: String + -> (Int -> Ef BenchEff Unit) + -> (Int -> Aff BenchEff Unit) + -> Array Int + -> Eff BenchEff Unit +bench2 name buildEf buildAff vals = for_ vals \val -> do + logBench [name <> " build", "Aff", show val] $ benchWith' 4 \_ -> buildAff val + logBench' [name <> " build", "Ef", show val] $ benchWith' 4 \_ -> buildEf val + let aff = launchAff_ $ buildAff val + logBench [name <> " run", "Aff", show val] $ benchWith' 4 \_ -> unsafePerformEff aff + let ef = liftEf $ buildEf val + logBench' [name <> " run", "Ef", show val] $ benchWith' 4 \_ -> unsafePerformEff ef + +logBench'' :: (String -> String) -> Array String -> Eff BenchEff BenchResult -> Eff BenchEff Unit +logBench'' f msg benchEff = do + res <- benchEff + let + logStr = intercalate " | " + $ append msg + $ map (f <<< withUnits) [res.mean, res.stdDev, res.min, res.max] + log $ "| " <> logStr <> " |" + +logBench :: Array String -> Eff BenchEff BenchResult -> Eff BenchEff Unit +logBench = logBench'' id + +logBench' :: Array String -> Eff BenchEff BenchResult -> Eff BenchEff Unit +logBench' = logBench'' \s -> "**" <> s <> "**" + +foreign import data Arr :: Type -> Type + +foreign import mkArr :: forall e a. Eff e (Arr a) +foreign import pushToArr :: forall e a. Arr a -> a -> Eff e a +foreign import log :: forall e a. a -> Eff e Unit + diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..1dfcde0 --- /dev/null +++ b/bower.json @@ -0,0 +1,29 @@ +{ + "name": "purescript-ef", + "homepage": "https://github.com/safareli/purescript-ef", + "description": "Faster and safer implementation of the Effect monad", + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/safareli/purescript-ef.git" + }, + "ignore": [ + "**/.*", + "bower_components", + "node_modules", + "output", + "test", + "bower.json", + "package.json" + ], + "dependencies": { + "purescript-prelude": "^3.0.0", + "purescript-eff": "^3.1.0", + "purescript-unsafe-coerce": "^3.0.0" + }, + "devDependencies": { + "purescript-foldable-traversable": "^3.6.1", + "purescript-minibench": "safareli/purescript-minibench#un-log", + "purescript-aff": "^4.0.1" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0df4bb7 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "scripts": { + "clean": "rimraf output && rimraf .pulp-cache", + "build": "eslint src && pulp build -- --censor-lib --strict", + "test": "pulp test", + "bench:build": "purs compile 'bench/**/*.purs' 'src/**/*.purs' 'bower_components/*/src/**/*.purs'", + "bench:run": "node --expose-gc -e 'require(\"./output/Bench.Main/index.js\").main()'", + "bench:run:extended": "node --expose-gc -e 'require(\"./output/Bench.Main/index.js\").extended()'", + "bench:all": "npm run bench:build && npm run bench:run && npm run bench:run:extended", + "bench": "npm run bench:build && npm run bench:run" + }, + "devDependencies": { + "eslint": "^3.17.1", + "pulp": "^11.0.0", + "purescript-psa": "^0.5.1", + "rimraf": "^2.6.1" + } +} diff --git a/src/Control/Monad/Ff.js b/src/Control/Monad/Ff.js new file mode 100644 index 0000000..c7bd92d --- /dev/null +++ b/src/Control/Monad/Ff.js @@ -0,0 +1,92 @@ +"use strict"; + + +// Ef a +// = () -> a +// | { Ef a, tag: "PURE", _0 :: a, _1 :: Void } +// | { Ef a, tag: "MAP", _0 :: b -> a, _1 :: Ef b } +// | { Ef a, tag: "APPLY", _0 :: Ef b, _1 :: Ef (b -> a) } +// | { Ef a, tag: "BIND", _0 :: b -> Ef a, _1 :: Ef b } + +// Operation a b +// = { tag: "MAP", _0 :: a -> b } +// | { tag: "APPLY", _0 :: Ef a } +// | { tag: "APPLY_FUNC", _0 :: a -> b } +// | { tag: "BIND", _0 :: a -> Ef b } + + +var PURE = "PURE"; +var MAP = "MAP"; +var APPLY = "APPLY"; +var BIND = "BIND"; +var APPLY_FUNC = "APPLY_FUNC"; + + +var toEff = function (inputEff) { + var operations = []; + var eff = inputEff; + var res; + var op; + var tag; + effLoop: for (;;) { + tag = eff.tag; + if (tag !== undefined) { + if (tag === MAP || tag === BIND || tag === APPLY) { + operations.push(eff); + eff = eff._1; + continue; + } + // here `tag === PURE` + res = eff._0; + } else { + // here `typeof eff == "function"` + res = eff(); + } + + while ((op = operations.pop())) { + if (op.tag === MAP) { + res = op._0(res); + } else if (op.tag === APPLY_FUNC) { + res = op._0(res); + } else if (op.tag === APPLY) { + eff = op._0; + operations.push({ tag: APPLY_FUNC, _0: res }); + continue effLoop; + } else { // op.tag === BIND + eff = op._0(res); + continue effLoop; + } + } + return res; + } +}; + +var mkEf = function (tag, _0, _1) { + var eff = function eff_() { return toEff(eff_); }; + eff.tag = tag; + eff._0 = _0; + eff._1 = _1; + return eff; +}; + +exports.pureE = function (x) { + return mkEf(PURE, x); +}; + +exports.mapE = function (f) { + return function (eff) { + return mkEf(MAP, f, eff); + }; +}; + +exports.applyE = function (effF) { + return function (eff) { + return mkEf(APPLY, eff, effF); + }; +}; + +exports.bindE = function (eff) { + return function (f) { + return mkEf(BIND, f, eff); + }; +}; diff --git a/src/Control/Monad/Ff.purs b/src/Control/Monad/Ff.purs new file mode 100644 index 0000000..c35c30e --- /dev/null +++ b/src/Control/Monad/Ff.purs @@ -0,0 +1,39 @@ +module Control.Monad.Ef (Ef) where + +import Control.Applicative (class Applicative) +-- import Control.Applicative (class Applicative, liftA1) +import Control.Apply (class Apply) +import Control.Bind (class Bind) +import Control.Monad (class Monad) +-- import Control.Monad (class Monad, ap) +import Control.Monad.Eff (kind Effect) +import Data.Functor (class Functor) +import Control.Monad.Eff.Class (class MonadEff) +import Unsafe.Coerce (unsafeCoerce) + + +foreign import data Ef :: # Effect -> Type -> Type + +instance functorEf :: Functor (Ef e) where + map = mapE + -- map = liftA1 + +instance applyEf :: Apply (Ef e) where + apply = applyE + -- apply = ap + +instance applicativeEf :: Applicative (Ef e) where + pure = pureE + +instance bindEf :: Bind (Ef e) where + bind = bindE + +instance monadEf :: Monad (Ef e) + +instance monadEEFff :: MonadEff eff (Ef eff) where + liftEff = unsafeCoerce + +foreign import mapE :: forall e a b. (a -> b) -> Ef e a -> Ef e b +foreign import applyE :: forall e a b. Ef e (a -> b) -> Ef e a-> Ef e b +foreign import pureE :: forall e a. a -> Ef e a +foreign import bindE :: forall e a b. Ef e a -> (a -> Ef e b) -> Ef e b \ No newline at end of file diff --git a/src/Control/Monad/Ff/Class.purs b/src/Control/Monad/Ff/Class.purs new file mode 100644 index 0000000..2a398a1 --- /dev/null +++ b/src/Control/Monad/Ff/Class.purs @@ -0,0 +1,19 @@ +module Control.Monad.Ef.Class + ( class MonadEf + , liftEf + ) where + +import Control.Category (id) +import Control.Monad (class Monad) +import Control.Monad.Ef (Ef) +import Control.Monad.Eff (Eff) +import Unsafe.Coerce (unsafeCoerce) + +class Monad m <= MonadEf eff m | m -> eff where + liftEf :: forall a. Ef eff a -> m a + +instance monadEfEf :: MonadEf eff (Ef eff) where + liftEf = id + +instance monadEfEff :: MonadEf eff (Eff eff) where + liftEf = unsafeCoerce \ No newline at end of file diff --git a/test/Test/Main.js b/test/Test/Main.js new file mode 100644 index 0000000..3feffb8 --- /dev/null +++ b/test/Test/Main.js @@ -0,0 +1,70 @@ +"use strict"; + +exports.mkArr = function(){ + return []; +}; + +exports.unArr = function(xs){ + return xs.slice(0); +}; + +exports.pushToArr = function(xs) { + return function(x) { + return function() { + xs.push(x); + return x; + }; + }; +}; + +exports.assert = function(isOk) { + return function(msg) { + return function() { + if (isOk == false) { + throw new Error("assertion failed: " + msg); + }; + }; + }; +}; + +exports.naturals = function(n) { + var res = []; + for (var index = 0; index < n; index++) { + res[index] = index; + } + return res; +}; + +exports.log = function(x) { + return function(){ + console.log(x) + } +}; + + +exports.time = function(x) { + return function(){ + console.time(x) + } +}; + + +exports.timeEnd = function(x) { + return function(){ + console.timeEnd(x) + } +}; + +exports.plus_ = function(a) { + return function(b) { + return a + b + } +} + +exports.stackSize = function computeCallStack() { + try { + return 1 + computeCallStack() + } catch (_) { + return 1 + } +}() \ No newline at end of file diff --git a/test/Test/Main.purs b/test/Test/Main.purs new file mode 100644 index 0000000..0f6dd9a --- /dev/null +++ b/test/Test/Main.purs @@ -0,0 +1,139 @@ +module Test.Main where + +import Prelude + +import Control.Monad.Eff (Eff) +import Control.Apply (lift2) +import Control.Monad.Eff.Class (class MonadEff, liftEff) +import Control.Monad.Ef (Ef) +import Control.Monad.Ef.Class (liftEf) +import Control.Monad.Eff.Console (CONSOLE) + + +type TestEff = (console :: CONSOLE) + +main :: Eff TestEff Unit +main = liftEf mainF + + +testLift2 :: Ef TestEff Unit +testLift2 = do + arr <- mkArr' + res <- liftEff (pushToArr arr 1) `lift2 plus_` liftEff (pushToArr arr 2) + res' <- liftEff (pure 1) `lift2 plus_` liftEff (pure 2) + assert' ([1, 2] == unArr arr) "lift2 1/3" + assert' (3 == res') "lift2 2/3" + assert' (3 == res) "lift2 3/3" + + +testApply :: forall m. MonadEff TestEff m => Int -> m Unit +testApply n' = do + arr <- liftEff mkArr + applyLoop (void <<< liftEff <<< pushToArr arr) n' + liftEff $ assert (naturals n' == unArr arr) $ "apply " <> show n' + where + applyLoop :: Monad m => (Int -> m Unit) -> Int -> m Unit + applyLoop eff max = go (pure unit) 0 + where + go acc n | n == max = acc + go acc n = go (acc <* eff n) (n + 1) + + + +testBindRight :: forall m. MonadEff TestEff m => Int -> m Unit +testBindRight n' = do + arr <- liftEff mkArr + bindRightLoop (void <<< liftEff <<< pushToArr arr) n' + liftEff $ assert (naturals n' == unArr arr) $ "bind right " <> show n' + where + bindRightLoop :: Monad m => (Int -> m Unit) -> Int -> m Unit + bindRightLoop eff max = go (pure unit) 0 + where + go acc n | n == max = acc + go acc n = go (eff (max - n - 1) >>= const acc) (n + 1) + + + + +testBindLeft :: forall m. MonadEff TestEff m => Int -> m Unit +testBindLeft n' = do + arr <- liftEff mkArr + bindLeftLoop (void <<< liftEff <<< pushToArr arr) n' + liftEff $ assert (naturals n' == unArr arr) $ "bind left " <> show n' + where + bindLeftLoop :: Monad m => (Int -> m Unit) -> Int -> m Unit + bindLeftLoop eff max = go (pure unit) 0 + where + go acc n | n == max = acc + go acc n = go (acc >>= const (eff n)) (n + 1) + + +testMap :: forall m. MonadEff TestEff m => Int -> m Unit +testMap n = do + res <- mapLoop n (pure 0) + liftEff $ assert (res == n) $ "map " <> show n + where + mapLoop :: Monad m => Int -> m Int -> m Int + mapLoop max i = + if max == 0 + then i + else mapLoop (max - 1) (map (_ + 1) i) + + +mainF :: Ef TestEff Unit +mainF = do + testLift2 + log' "\n" + + test "testBindRight" testBindRight testBindRight + test "testBindLeft" testMap testMap + test "testMap" testMap testMap + test "testApply" testApply testApply + + where + test + :: String + -> (Int -> Eff TestEff Unit) + -> (Int -> Ef TestEff Unit) + -> Ef TestEff Unit + test name eff ef = do + log' name + log' "eff" + liftEff $ eff 100 + log' "Ef" + liftEf $ ef 100 + + +foreign import data Arr :: Type -> Type + + +foreign import mkArr :: forall e a. Eff e (Arr a) +mkArr' :: forall e a. Ef e (Arr a) +mkArr' = liftEff mkArr + +foreign import pushToArr :: forall e a. Arr a -> a -> Eff e a +pushToArr' :: forall e a. Arr a -> a -> Ef e a +pushToArr' xs x = liftEff $ pushToArr xs x + +foreign import assert :: forall e. Boolean -> String -> Eff e Unit +assert' :: forall e. Boolean -> String -> Ef e Unit +assert' b msg = liftEff $ assert b msg + +foreign import log :: forall e a. a -> Eff e Unit +log' :: forall e a. a -> Ef e Unit +log' x = liftEff $ log x + +timed :: forall m. MonadEff TestEff m => String -> m Unit -> m Unit +timed msg eff = do + liftEff $ log $ msg <> " ... started" + liftEff (time msg) *> eff <* liftEff (timeEnd msg) + liftEff $ log "\n" + +foreign import time :: forall e. String -> Eff e Unit +foreign import timeEnd :: forall e. String -> Eff e Unit + + +foreign import unArr :: forall a. Arr a -> Array a +foreign import naturals :: Int -> Array Int +foreign import plus_ :: Int -> Int -> Int +foreign import stackSize :: Int \ No newline at end of file