Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Initial Transformation Functions #48

Merged
merged 1 commit into from
Feb 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[![Build Status](https://travis-ci.org/evilsoft/crocks.svg?branch=master)](https://travis-ci.org/evilsoft/crocks) [![Coverage Status](https://coveralls.io/repos/github/evilsoft/crocks/badge.svg?branch=master)](https://coveralls.io/github/evilsoft/crocks?branch=master)
[![NPM version](https://badge.fury.io/js/crocks.svg)](https://www.npmjs.com/package/crocks)

# crocks.js
`crocks` is a collection of popular *Algebraic Data Types (ADTs)* that are all the rage in functional programming. You have heard of things like `Maybe` and `Either` and heck maybe even `IO`, that is what these are. The main goal of `crocks` is to curate and provide not only a common interface between each type (where possible of course), but also all of the helper functions needed to hit the ground running.
Expand Down Expand Up @@ -60,6 +61,7 @@ There are (6) classifications of "things" included in this library:

* Point-free Functions (`crocks/pointfree`): Wanna use these ADTs in a way that you never have to reference the actual data being worked on? Well here is where you will find all of these functions to do that. For every algebra available on both the `Crocks` and `Monoids` there is a function here.

* Transformation Functions (`crocks/transform`): All the functions found here are used to transform from one type to another, naturally. These come are handy in situations where you have functions that return one type (like an `Either`), but are working in a context of another (say `Maybe`). You would like to compose these, but in doing so will result in a nesting that you will need to account for for the rest of your flow.

### Crocks
The `Crocks` are the heart and soul of this library. This is where you will find all your favorite ADT's you have grown to :heart:. They include gems such as: `Maybe`, `Either` and `IO`, to name a few. The are usually just a simple constructor that takes either a function or value (depending on the type) and will return you a "container" that wraps whatever you passed it. Each container provides a variety of functions that act as the operations you can do on the contained value. There are many types that share the same function names, but what they do from type to type may vary. Every Crock provides type function on the Constructor and both inspect and type functions on their Instances.
Expand Down Expand Up @@ -316,3 +318,121 @@ These functions provide a very clean way to build out very simple functions and
| `tail` | `Array`, `List`, `String` |
| `traverse` | `Array`, `Either`, `Identity`, `List`, `Maybe` |
| `value` | `Arrow`, `Const`, `Either`, `Identity`, `List`, `Pair`, `Pred`, `Unit`, `Writer` |

### Transformation Functions
Transformation functions are mostly used to reduce unwanted nesting of similar types. Take for example the following structure:

```javascript
const data =
Either.of(Maybe.of(3)) // Right Just 3

// mapping on the inner Maybe is tedious at best
data
.map(map(x => x + 1)) // Right Just 4
.map(map(x => x * 10)) // Right Just 40

// and extraction...super gross
data
.either(identity, identity) // Just 3
.option(0) // 3

// or
data
.either(option(0), option(0)) // 3
```

The transformation functions, that ship with `crocks`, provide a means for dealing with this. Using them effectively, can turn the above code into something more like this:

```javascript
const data =
Either.of(Maybe.of(3)) // Right Just 3
.chain(maybeToEither(0)) // Right 3

// mapping on a single Either, much better
data
.map(x => x + 1) // Right 4
.map(x => x * 10) // Right 40

// no need to default the Left case anymore
data
.either(identity, identity) // 3

// effects of the inner type are applied immediately
const nested =
Either.of(Maybe.Nothing) // Right Nothing

const unnested =
nested
.chain(maybeToEither(0)) // Left 0

// Always maps, although the inner Maybe skips
nested
.map(map(x => x + 1)) // Right Nothing (runs mapping)
.map(map(x => x * 10)) // Right Nothing (runs mapping)
.either(identity, identity) // Nothing
.option(0) // 0

// Never maps on a Left, just skips it
unnested
.map(x => x + 1) // Left 0 (skips mapping)
.map(x => x * 10) // Left 0 (skips mapping)
.either(identity, identity) // 0
```

Not all types can be transformed to and from each other. Some of them are lazy and/or asynchronous, or are just too far removed. Also, some transformations will result in a loss of information. Moving from an `Either` to a `Maybe`, for instance, would lose the `Left` value of `Either` as a `Maybe`'s first parameter (`Nothing`) is fixed at `Unit`. Conversely, if you move the other way around, from a `Maybe` to an `Either` you must provide a default `Left` value. Which means, if the inner `Maybe` results in a `Nothing`, it will map to `Left` of your provided value. As such, not all of these functions are guaranteed isomorphic. With some types you just cannot go back and forth and expect to retain information.

Each function provides two signatures, one for if a Function is used for the second argument and another if the source ADT is passed instead. Although it may seem strange, this provides some flexibility on how to apply the transformation. The ADT version is great for squishing an already nested type or to perform the transformation in a composition. While the Function version can be used to extend an existing function without having to explicitly compose it. Both versions can be seen here:

```javascript
// Avoid nesting
// inc : a -> Maybe Number
const inc =
safeLift(isNumber, x => x + 1)

// using Function signature
// asyncInc : a -> Async Number Number
const asyncInc =
maybeToAsync(0, inc)

// using ADT signature to compose (extending functions)
// asyncInc : a -> Async Number Number
const anotherInc =
compose(maybeToAsync(0), inc)

// resolveValue : a -> Async _ a
const resolveValue =
Async.of

resolveValue(3) // Resolved 3
.chain(asyncInc) // Resolved 4
.chain(anotherInc) // Resolved 5
.chain(compose(maybeToAsync(20), inc)) // Resolved 6

resolveValue('oops') // Resolved 'oops'
.chain(asyncInc) // Rejected 0
.chain(anotherInc) // Rejected 0
.chain(compose(maybeToAsync(20), inc)) // Rejected 0

// Squash existing nesting
// Just Right 'nice'
const good =
Maybe.of(Either.Right('nice'))

// Just Left 'not so nice'
const bad =
Maybe.of(Either.Left('not so nice'))

good
.chain(eitherToMaybe) // Just 'nice'

bad
.chain(eitherToMaybe) // Nothing
```

#### Transformation Signatures
| Transform | ADT signature | Function Signature |
|---|---|---|
| `eitherToAsync` | `Either e a -> Async e a` | `(a -> Either e b) -> a -> Async e b` |
| `eitherToMaybe` | `Either b a -> Maybe a` | `(a -> Either c b) -> a -> Maybe b` |
| `maybeToAsync` | `e -> Maybe a -> Async e a` | `e -> (a -> Maybe b) -> a -> Async e b` |
| `maybeToEither` | `c -> Maybe b a -> Maybe a` | `c -> (a -> Maybe b) -> a -> Either c b` |
12 changes: 10 additions & 2 deletions crocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ const predicates = {
isNumber: require('./predicates/isNumber'),
isObject: require('./predicates/isObject'),
isSemigroup: require('./predicates/isSemigroup'),
isString: require('./predicates/isString'),
isString: require('./predicates/isString')
}

const transforms = {
eitherToAsync: require('./transforms/eitherToAsync'),
eitherToMaybe: require('./transforms/eitherToMaybe'),
maybeToAsync: require('./transforms/maybeToAsync'),
maybeToEither: require('./transforms/maybeToEither')
}

module.exports = Object.assign(
Expand All @@ -125,5 +132,6 @@ module.exports = Object.assign(
helpers,
monoids,
pointFree,
predicates
predicates,
transforms
)
10 changes: 10 additions & 0 deletions crocks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ const isObject = require('./predicates/isObject')
const isSemigroup = require('./predicates/isSemigroup')
const isString = require('./predicates/isString')

const eitherToAsync = require('./transforms/eitherToAsync')
const eitherToMaybe = require('./transforms/eitherToMaybe')
const maybeToAsync = require('./transforms/maybeToAsync')
const maybeToEither = require('./transforms/maybeToEither')

test('entry', t => {
t.equal(crocks.toString(), '[object Object]', 'is an object')

Expand Down Expand Up @@ -217,5 +222,10 @@ test('entry', t => {
t.equal(crocks.isSemigroup, isSemigroup, 'provides the isSemigroup function')
t.equal(crocks.isString, isString, 'provides the isString function')

t.equal(crocks.eitherToAsync, eitherToAsync, 'provides the eitherToAsync function')
t.equal(crocks.eitherToMaybe, eitherToMaybe, 'provides the eitherToMaybe function')
t.equal(crocks.maybeToAsync, maybeToAsync, 'provides the maybeToAsync function')
t.equal(crocks.maybeToEither, maybeToEither, 'provides the maybeToEither function')

t.end()
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build": "npm test && webpack && uglifyjs dist/crocks.js -c \"warnings=false\" -m -o dist/crocks.min.js",
"lint": "jshint .",
"lint:dev": "jshint --reporter=node_modules/jshint-stylish .",
"spec": "tape combinators/*.spec.js crocks/*.spec.js helpers/*.spec.js internal/*.spec.js monoids/*.spec.js pointfree/*.spec.js ./predicates/*.spec.js ./*.spec.js",
"spec": "tape combinators/*.spec.js crocks/*.spec.js helpers/*.spec.js internal/*.spec.js monoids/*.spec.js pointfree/*.spec.js ./predicates/*.spec.js ./transforms/*.spec.js ./*.spec.js",
"spec:dev": "nodemon -q -e js -x 'npm run spec -s | tap-spec'",
"test": "npm run lint && nyc npm run spec",
"coverage": "nyc report --reporter=text-lcov | coveralls"
Expand Down
36 changes: 36 additions & 0 deletions transforms/eitherToAsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/** @license ISC License (c) copyright 2017 original and current authors */
/** @author Ian Hofmann-Hicks (evil) */

const curry = require('../helpers/curry')

const isFunction = require('../predicates/isFunction')
const isType = require('../internal/isType')

const Either = require('../crocks/Either')
const Async = require('../crocks/Async')

const applyTransform = either =>
either.either(Async.rejected, Async.of)

// eitherToAsync : Either e a -> Async e a
// eitherToAsync : (a -> Either e b) -> a -> Async e b
function eitherToAsync(either) {
if(isType(Either.type(), either)) {
return applyTransform(either)
}
else if(isFunction(either)) {
return function(x) {
const m = either(x)

if(!isType(Either.type(), m)) {
throw new TypeError('eitherToAsync: Either returing function required')
}

return applyTransform(m)
}
}

throw new TypeError('eitherToAsync: Either or Either returing function required')
}

module.exports = curry(eitherToAsync)
97 changes: 97 additions & 0 deletions transforms/eitherToAsync.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const test = require('tape')
const sinon = require('sinon')
const helpers = require('../test/helpers')

const bindFunc = helpers.bindFunc
const noop = helpers.noop

const identity = require('../combinators/identity')

const isFunction = require('../predicates/isFunction')
const isType = require('../internal/isType')

const Either = require('../crocks/Either')
const Async = require('../crocks/Async')

const eitherToAsync = require('./eitherToAsync')

test('eitherToAsync transform', t => {
const f = bindFunc(eitherToAsync)

t.ok(isFunction(eitherToAsync), 'is a function')

t.throws(f(undefined), TypeError, 'throws if arg is undefined')
t.throws(f(null), TypeError, 'throws if arg is null')
t.throws(f(0), TypeError, 'throws if arg is a falsey number')
t.throws(f(1), TypeError, 'throws if arg is a truthy number')
t.throws(f(''), TypeError, 'throws if arg is a falsey string')
t.throws(f('string'), TypeError, 'throws if arg is a truthy string')
t.throws(f(false), TypeError, 'throws if arg is false')
t.throws(f(true), TypeError, 'throws if arg is true')
t.throws(f([]), TypeError, 'throws if arg is an array')
t.throws(f({}), TypeError, 'throws if arg is an object')

t.end()
})

test('eitherToAsync with Either', t => {
const some = 'something'
const none = 'nothing'

const good = eitherToAsync(Either.Right(some))
const bad = eitherToAsync(Either.Left(none))

t.ok(isType(Async.type(), good), 'returns an Async when Right')
t.ok(isType(Async.type(), bad), 'returns an Async when Left')

const res = sinon.spy(noop)
const rej = sinon.spy(noop)

good.fork(rej, res)
bad.fork(rej, res)

t.ok(res.calledWith(some), 'Right maps to a Resolved')
t.ok(rej.calledWith(none), 'Left maps to a Rejected')

t.end()
})

test('eitherToAsync with Either returning function', t => {
const some = 'something'
const none = 'nothing'

t.ok(isFunction(eitherToAsync(Either.of)), 'returns a function')

const f = bindFunc(eitherToAsync(identity))

t.throws(f(undefined), TypeError, 'throws if function returns undefined')
t.throws(f(null), TypeError, 'throws if function returns null')
t.throws(f(0), TypeError, 'throws if function returns a falsey number')
t.throws(f(1), TypeError, 'throws if function returns a truthy number')
t.throws(f(''), TypeError, 'throws if function returns a falsey string')
t.throws(f('string'), TypeError, 'throws if function returns a truthy string')
t.throws(f(false), TypeError, 'throws if function returns false')
t.throws(f(true), TypeError, 'throws if function returns true')
t.throws(f([]), TypeError, 'throws if function returns an array')
t.throws(f({}), TypeError, 'throws if function returns an object')

const lift =
x => x !== undefined ? Either.Right(x) : Either.Left(none)

const good = eitherToAsync(lift, some)
const bad = eitherToAsync(lift, undefined)

t.ok(isType(Async.type(), good), 'returns an Async when Right')
t.ok(isType(Async.type(), bad), 'returns an Async when Left')

const res = sinon.spy(noop)
const rej = sinon.spy(noop)

good.fork(rej, res)
bad.fork(rej, res)

t.ok(res.calledWith(some), 'Right maps to a Resolved')
t.ok(rej.calledWith(none), 'Left maps to a Rejected with option')

t.end()
})
36 changes: 36 additions & 0 deletions transforms/eitherToMaybe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/** @license ISC License (c) copyright 2017 original and current authors */
/** @author Ian Hofmann-Hicks (evil) */

const curry = require('../helpers/curry')

const isFunction = require('../predicates/isFunction')
const isType = require('../internal/isType')

const Either = require('../crocks/Either')
const Maybe = require('../crocks/Maybe')

const applyTransform = either =>
either.either(Maybe.Nothing, Maybe.Just)

// eitherToMaybe : Either b a -> Maybe a
// eitherToMaybe : (a -> Either c b) -> a -> Maybe b
function eitherToMaybe(either) {
if(isType(Either.type(), either)) {
return applyTransform(either)
}
else if(isFunction(either)) {
return function(x) {
const m = either(x)

if(!isType(Either.type(), m)) {
throw new TypeError('eitherToMaybe: Either returing function required')
}

return applyTransform(m)
}
}

throw new TypeError('eitherToMaybe: Either or Either returing function required')
}

module.exports = curry(eitherToMaybe)
Loading