Skip to content
This repository has been archived by the owner on Jan 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #12 from snatvb/develop
Browse files Browse the repository at this point in the history
New monad - IO [#2]
  • Loading branch information
snatvb authored Jun 1, 2019
2 parents 622cfee + 294db05 commit c828caf
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 3 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] :bomb:

## :bulb: [0.4.0] - 2019-06-01

### :gift: Added
- Module `interfaces`
- `IO` monad
- `Functor` interface
- `Applicative` interface

### :surfer: Changed
- `Maybe` implements `Functor` and `Applicative` interfaces
- `Either` implements `Functor` interface

## :bulb: [0.3.0] - 2019-05-24

### :gift: Added
Expand Down
2 changes: 2 additions & 0 deletions doc-theme/src/default/assets/css/setup/_typography.sass
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ code, pre
background-color: $COLOR_CODE_BACKGROUND
code
padding: 3px 8px
border-radius: 3px
pre
padding: 10px
border-radius: 5px

code
padding: 0
Expand Down
7 changes: 6 additions & 1 deletion smoke-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ const writeFile = promisify(fs.writeFile);
const assert = require('assert');

const code = `
const { Maybe, Either } = require('monad-maniac')
const { Maybe, Either, IO } = require('monad-maniac')
const double = (x) => x * 2
const just = Maybe.of(10)
const nothing = Maybe.of(null)
const io = IO.of('hi')
console.log(Either)
if (just.map(double).toString() !== 'Just(20)') {
Expand All @@ -32,6 +33,10 @@ if (Either.left('Some error').toString() !== 'Left(Some error)') {
if (Either.right('Some value').toString() !== 'Right(Some value)') {
throw new Error('Not Right(Some value)')
}
if (io.run() !== 'hi') {
throw new Error('io.run do not returns need value')
}
`

Promise.resolve()
Expand Down
54 changes: 54 additions & 0 deletions src/doc/io.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# IO

The monad need to _hide_ some side effect and keep your code clean.

You can use it for working with `WebSocket`, `fetch`, `console.log`, `DOM` and etc.
What could doing some side effect.

```ts
import { Either, IO } from 'monad-maniac'

type SideEffectDataType = { [id: number]: string }
let SideEffectData: SideEffectDataType = {
1: 'Jake',
2: 'Bob',
3: 'Alice',
}

const logError = (...args: any[]) => console.error('Got error:', ...args)

const readName = (id: number) => (): Either.Shape<string, string> => {
const name = SideEffectData[id]
return name
? Either.right(name)
: Either.left('Name not found')
}

const writeName = (id: number) => (name: Either.Shape<string, string>) => {
return name.caseOf({
Left: (error) => {
logError(error)
return error
},
Right: (name) => SideEffectData[id] = name,
})
}

const addFired = (name: string) => `${name} was fired!`

// Will get `Jake`
// Changed string to `Jake was fired!`
// Write the string to SideEffectData
// In result will be `Jake was fired!`
const result = IO
.from(readName(1))
.map((name) => name.map(addFired))
.chain(writeName(1))

// Name not found
// Also will show error in console
const resultFailure = IO
.from(readName(10))
.map((name) => name.map(addFired))
.chain(writeName(10))
```
3 changes: 2 additions & 1 deletion src/either.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as helpers from './helpers'
import * as Maybe from './maybe'
import { Nullable } from './types'
import { Functor } from './interfaces'

/** Mather type for caseOf */
export type CaseOf<L, R, U> = {
Expand All @@ -11,7 +12,7 @@ export type CaseOf<L, R, U> = {
/** This is alias for normal display from context (`Either.Either` => `Either.Shape`) */
export type Shape<L, R> = Either<L, R>

export interface Either<L, R> {
export interface Either<L, R> extends Functor<R> {
/**
* Apply some function to value in container. `map` for `Right`
* will call the function with value, for `Left` return `Left`.
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
This is just here to re-exporter. It doesn't do anything else.
*/
/** For fix tsdoc */
import * as interfaces from './interfaces'
import * as Maybe from './maybe'
import * as Either from './either'
import * as IO from './io'

export {
interfaces,
Maybe,
Either,
IO,
}
9 changes: 9 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type ApplicativeResult<T, U extends ((value: T) => any)> = Applicative<ReturnType<U>>

export interface Functor<T> {
map<U>(fn: (value: T) => U): Functor<U>
}

export interface Applicative<T> {
apply<U extends ((value: T) => any)>(functor: Functor<U>): ApplicativeResult<T, U>
}
100 changes: 100 additions & 0 deletions src/io.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/** [[include:doc/io.md]] */

/** Fix tsdoc */
import { Functor } from './interfaces'
import * as helpers from './helpers'

export class IO<T extends (...args: any[]) => any> implements Functor<T> {
private effect: T

constructor(effect: T) {
this.effect = effect
}

/**
* This method running function from `IO` and result will be referred to
* `fn` function. Result from `fn` function will be wrapped up into `IO`.
*/
map<U>(fn: (value: ReturnType<T>) => U): IO<() => U> {
return new IO(() => fn(this.effect()))
}

/**
* Same like `map`, but...
* Result from `fn` function will **not** be wrapped up into `IO`.
*/
chain<U>(fn: (value: ReturnType<T>) => U): U {
return fn(this.effect())
}

/** To run function from `IO` */
run(): ReturnType<T> {
return this.effect()
}

/** Just returns `IO` as string */
toString(): string {
return 'IO'
}

}

/** This need just use with context `IO.Shape<T>` instead of `IO.IO<T>` */
export interface Shape<T extends (...args: any[]) => any> extends IO<T> {}

/**
* This function are getting some value and contains it
* in function where the function will returns the value (`() => value` )
* */
export function of<T>(value: T) {
return new IO(() => value)
}

/** Making `IO` monad from function */
export function from(fn: (...args: any[]) => any) {
return new IO(fn)
}

/** Calling method `run` from `IO` instance */
export function run<T extends (...args: any[]) => any>(io: IO<T>): ReturnType<T> {
return io.run()
}

/** Calling method `toString` from `IO` instance */
export function toString<T extends (...args: any[]) => any>(io: IO<T>): string {
return io.toString()
}

/**
* Method like [`IO.map`](../interfaces/_io_.io.html#map)
* but to get `IO` and call method `map` with a function.
*
* */
export function map<T extends (...args: any[]) => any, U>(fn: (value: ReturnType<T>) => U, io: IO<T>): IO<() => U>
/**
* Just curried `map`.
*
* _(a -> b) -> IO(a) -> IO(b)_
*/
export function map<T extends (...args: any[]) => any, U>(fn: (value: ReturnType<T>) => U): (io: IO<T>) => IO<() => U>
export function map<T extends (...args: any[]) => any, U>(fn: (value: ReturnType<T>) => U, io?: IO<T>): IO<() => U> | ((io: IO<T>) => IO<() => U>) {
const op = (functor: IO<T>) => functor.map(fn)
return helpers.curry1(op, io)
}

/**
* Method like [`IO.chain`](../interfaces/_io_.io.html#chain)
* but to get `IO` and call method `chain` with a function.
*
* */
export function chain<T extends (...args: any[]) => any, U>(fn: (value: ReturnType<T>) => U, io: IO<T>): U
/**
* Just curried `chain`.
*
* _(a -> b) -> IO(a) -> b_
*/
export function chain<T extends (...args: any[]) => any, U>(fn: (value: ReturnType<T>) => U): (io: IO<T>) => U
export function chain<T extends (...args: any[]) => any, U>(fn: (value: ReturnType<T>) => U, io?: IO<T>): U | ((io: IO<T>) => U) {
const op = (functor: IO<T>) => functor.chain(fn)
return helpers.curry1(op, io)
}
3 changes: 2 additions & 1 deletion src/maybe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as helpers from './helpers'
import * as Either from './either'
import { Nullable } from './types'
import { Functor, Applicative } from './interfaces'

type ApplicativeResult<T, U extends ((value: T) => any)> = Maybe<ReturnType<U>>

Expand All @@ -18,7 +19,7 @@ export type CaseOf<T, U> = {
Nothing: () => U,
}

export interface Maybe<T> {
export interface Maybe<T> extends Functor<T>, Applicative<T> {
/**
* Apply some function to value in container. `map` for Just
* will call the function with value, for `Nothing` return Nothing.
Expand Down
117 changes: 117 additions & 0 deletions tests/io.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Either, IO } from '../src'

describe('Pure functions', () => {
it('of', () => {
expect(IO.of(222).run()).toBe(222)
expect(IO.of(4).map((x) => x * x).run()).toBe(16)
})

it('from', () => {
expect(IO.from(() => 222).run()).toBe(222)
expect(IO.from(() => 4).map((x) => x * x).run()).toBe(16)
})

it('run', () => {
const io = IO.from(() => 222)
expect(IO.run(io)).toBe(222)
})

it('toString', () => {
const io = IO.from(() => 222)
expect(IO.toString(io)).toBe('IO')
})
})

describe('Docs', () => {
it('map and chain', () => {
type SideEffectDataType = { [id: number]: string }
let SideEffectData: SideEffectDataType = {
1: 'Jake',
2: 'Bob',
3: 'Alice',
}

const logError = (...args: any[]) => args

const readName = (id: number) => (): Either.Shape<string, string> => {
const name = SideEffectData[id]
return name
? Either.right(name)
: Either.left('Name not found')
}

const writeName = (id: number) => (name: Either.Shape<string, string>) => {
return name.caseOf({
Left: (error) => {
logError(error)
return error
},
Right: (name) => SideEffectData[id] = name,
})
}

const addFired = (name: string) => `${name} was fired!`

const result = IO
.from(readName(1))
.map((name) => name.map(addFired))
.chain(writeName(1))

const resultFailure = IO
.from(readName(10))
.map((name) => name.map(addFired))
.chain(writeName(10))

expect(result).toBe('Jake was fired!')
expect(resultFailure).toBe('Name not found')
expect(SideEffectData).toEqual({
1: 'Jake was fired!',
2: 'Bob',
3: 'Alice',
})
})
})

describe('Pure functions', () => {
type SideEffectDataType = { [id: number]: string }
let SideEffectData: SideEffectDataType = {
1: 'Jake',
2: 'Bob',
3: 'Alice',
}

const logError = (...args: any[]) => args

const readName = (id: number) => (): Either.Shape<string, string> => {
const name = SideEffectData[id]
return name
? Either.right(name)
: Either.left('Name not found')
}

const writeName = (id: number) => (name: Either.Shape<string, string>) => {
return name.caseOf({
Left: (error) => {
logError(error)
return error
},
Right: (name) => SideEffectData[id] = name,
})
}
it('map and chain', () => {
const addFired = (name: string) => `${name} was fired!`

const readedIO = IO.from(readName(1))

const resultAddedFired = IO.map((name) => name.map(addFired), readedIO)
const result = IO.chain(writeName(1), resultAddedFired)

expect(readedIO.toString()).toBe('IO')
expect(result).toBe('Jake was fired!')
expect(SideEffectData).toEqual({
1: 'Jake was fired!',
2: 'Bob',
3: 'Alice',
})
})
})

0 comments on commit c828caf

Please sign in to comment.