├── examples └── signal │ └── mario │ ├── .gitignore │ ├── resources │ ├── grass.png │ ├── bg-day.png │ ├── bg-night.png │ ├── ground.png │ ├── bg-evening.png │ ├── bg-morning.png │ ├── font │ │ ├── prstartk.eot │ │ ├── prstartk.ttf │ │ ├── prstartk.woff │ │ └── prstartk.woff2 │ ├── goomba │ │ ├── left.gif │ │ └── right.gif │ ├── bg-transparent.png │ ├── bg2-transparent.png │ ├── hud-score-coin.png │ ├── item-container.png │ └── mario │ │ ├── jump │ │ ├── left.gif │ │ └── right.gif │ │ ├── walk │ │ ├── left.gif │ │ └── right.gif │ │ └── stand │ │ ├── left.gif │ │ └── right.gif │ ├── babel.config.js │ ├── src │ ├── MarioDOM.ts │ ├── Main.ts │ └── Mario.ts │ ├── webpack.config.js │ ├── index.html │ └── lib │ └── Main.js ├── maybe.ts ├── docs ├── examples │ └── signal │ │ └── mario │ │ ├── .gitignore │ │ ├── resources │ │ ├── grass.png │ │ ├── bg-day.png │ │ ├── bg-night.png │ │ ├── ground.png │ │ ├── bg-evening.png │ │ ├── bg-morning.png │ │ ├── font │ │ │ ├── prstartk.eot │ │ │ ├── prstartk.ttf │ │ │ ├── prstartk.woff │ │ │ └── prstartk.woff2 │ │ ├── goomba │ │ │ ├── left.gif │ │ │ └── right.gif │ │ ├── bg-transparent.png │ │ ├── bg2-transparent.png │ │ ├── hud-score-coin.png │ │ ├── item-container.png │ │ └── mario │ │ │ ├── jump │ │ │ ├── left.gif │ │ │ └── right.gif │ │ │ ├── walk │ │ │ ├── left.gif │ │ │ └── right.gif │ │ │ └── stand │ │ │ ├── left.gif │ │ │ └── right.gif │ │ ├── babel.config.js │ │ ├── src │ │ ├── MarioDOM.ts │ │ ├── Main.ts │ │ └── Mario.ts │ │ ├── webpack.config.js │ │ ├── index.html │ │ └── lib │ │ └── Main.js ├── _config.yml ├── assets │ └── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png ├── globals.html ├── modules │ ├── _signal_signalchannel_.html │ ├── _reader_.html │ ├── _maybe_.html │ └── _writer_.html └── enums │ └── _either_.eithertype.html ├── either.ts ├── functional.ts ├── signal.ts ├── .gh-pages.yml ├── signal-dom.ts ├── .huskyrc ├── .editorconfig ├── index.ts ├── jest.config.js ├── src ├── signal │ ├── __tests__ │ │ ├── SignalChannel.test.ts │ │ └── Signal.test.ts │ ├── SignalChannel.ts │ ├── SignalDOM.ts │ └── Signal.ts ├── utils │ └── TestSetup.ts ├── __tests__ │ ├── Reader.test.ts │ ├── Functional.test.ts │ ├── State.test.ts │ ├── Either.test.ts │ ├── Writer.test.ts │ └── Maybe.test.ts ├── Reader.ts ├── Either.ts ├── Functional.ts ├── Writer.ts ├── State.ts └── Maybe.ts ├── tsconfig.json ├── .gitignore ├── LICENSE ├── package.json ├── tslint.json └── README.md /examples/signal/mario/.gitignore: -------------------------------------------------------------------------------- 1 | !lib 2 | -------------------------------------------------------------------------------- /maybe.ts: -------------------------------------------------------------------------------- 1 | export * from './src/Maybe' 2 | -------------------------------------------------------------------------------- /docs/examples/signal/mario/.gitignore: -------------------------------------------------------------------------------- 1 | !lib 2 | -------------------------------------------------------------------------------- /either.ts: -------------------------------------------------------------------------------- 1 | export * from './src/Either' 2 | 3 | -------------------------------------------------------------------------------- /functional.ts: -------------------------------------------------------------------------------- 1 | export * from './src/Functional' 2 | -------------------------------------------------------------------------------- /signal.ts: -------------------------------------------------------------------------------- 1 | export * from './src/signal/Signal' 2 | -------------------------------------------------------------------------------- /.gh-pages.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "_*_.html" 3 | - "_*_.*.html" 4 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "_*_.html" 3 | - "_*_.*.html" 4 | -------------------------------------------------------------------------------- /signal-dom.ts: -------------------------------------------------------------------------------- 1 | import * as SignalDOM from './src/signal/SignalDOM' 2 | export {SignalDOM} 3 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "pre-push": "yarn check.types && yarn test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/signal/mario/resources/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/grass.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/bg-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/bg-day.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/bg-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/bg-night.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/ground.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/grass.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/bg-evening.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/bg-evening.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/bg-morning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/bg-morning.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/bg-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/bg-day.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/bg-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/bg-night.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/ground.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/font/prstartk.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/font/prstartk.eot -------------------------------------------------------------------------------- /examples/signal/mario/resources/font/prstartk.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/font/prstartk.ttf -------------------------------------------------------------------------------- /examples/signal/mario/resources/goomba/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/goomba/left.gif -------------------------------------------------------------------------------- /examples/signal/mario/resources/goomba/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/goomba/right.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/bg-evening.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/bg-evening.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/bg-morning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/bg-morning.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/bg-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/bg-transparent.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/bg2-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/bg2-transparent.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/font/prstartk.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/font/prstartk.woff -------------------------------------------------------------------------------- /examples/signal/mario/resources/font/prstartk.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/font/prstartk.woff2 -------------------------------------------------------------------------------- /examples/signal/mario/resources/hud-score-coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/hud-score-coin.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/item-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/item-container.png -------------------------------------------------------------------------------- /examples/signal/mario/resources/mario/jump/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/mario/jump/left.gif -------------------------------------------------------------------------------- /examples/signal/mario/resources/mario/walk/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/mario/walk/left.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/font/prstartk.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/font/prstartk.eot -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/font/prstartk.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/font/prstartk.ttf -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/goomba/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/goomba/left.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/goomba/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/goomba/right.gif -------------------------------------------------------------------------------- /examples/signal/mario/resources/mario/jump/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/mario/jump/right.gif -------------------------------------------------------------------------------- /examples/signal/mario/resources/mario/stand/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/mario/stand/left.gif -------------------------------------------------------------------------------- /examples/signal/mario/resources/mario/stand/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/mario/stand/right.gif -------------------------------------------------------------------------------- /examples/signal/mario/resources/mario/walk/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/examples/signal/mario/resources/mario/walk/right.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/bg-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/bg-transparent.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/bg2-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/bg2-transparent.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/font/prstartk.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/font/prstartk.woff -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/font/prstartk.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/font/prstartk.woff2 -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/hud-score-coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/hud-score-coin.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/item-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/item-container.png -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/mario/jump/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/mario/jump/left.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/mario/walk/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/mario/walk/left.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/mario/jump/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/mario/jump/right.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/mario/stand/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/mario/stand/left.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/mario/stand/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/mario/stand/right.gif -------------------------------------------------------------------------------- /docs/examples/signal/mario/resources/mario/walk/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/communityfunded/monadism/HEAD/docs/examples/signal/mario/resources/mario/walk/right.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /examples/signal/mario/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/env', 5 | { 6 | 'targets': { 7 | 'ie': 11, 8 | }, 9 | 'useBuiltIns': 'entry', 10 | 'modules': 'commonjs', 11 | } 12 | ], 13 | '@babel/typescript', 14 | ], 15 | plugins: [ 16 | '@babel/proposal-class-properties', 17 | '@babel/proposal-object-rest-spread', 18 | '@babel/syntax-dynamic-import', 19 | '@babel/proposal-export-default-from' 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /docs/examples/signal/mario/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/env', 5 | { 6 | 'targets': { 7 | 'ie': 11, 8 | }, 9 | 'useBuiltIns': 'entry', 10 | 'modules': 'commonjs', 11 | } 12 | ], 13 | '@babel/typescript', 14 | ], 15 | plugins: [ 16 | '@babel/proposal-class-properties', 17 | '@babel/proposal-object-rest-spread', 18 | '@babel/syntax-dynamic-import', 19 | '@babel/proposal-export-default-from' 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import Maybe, {Just, Nothing, maybe} from './src/Maybe' 2 | import Either, {Left, Right, either} from './src/Either' 3 | import Signal, {constant, run, unwrap, every} from './src/signal/Signal' 4 | import Channel, {channel} from './src/signal/SignalChannel' 5 | import State, {get, gets, put, modify} from './src/State' 6 | 7 | export * from './src/Functional' 8 | 9 | export { 10 | Maybe, Just, Nothing, maybe, 11 | Either, Left, Right, either, 12 | Signal, constant, run, unwrap, every, 13 | Channel, channel, 14 | State, get, gets, put, modify, 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.tsx?$': 'ts-jest' 4 | }, 5 | testRegex: '(/(test|__tests__)/.*(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 6 | testURL: 'http://localhost', 7 | roots: [ 8 | '/src/' 9 | ], 10 | testPathIgnorePatterns: [ 11 | '/node_modules/', 12 | '/dist/', 13 | ], 14 | moduleFileExtensions: [ 15 | 'ts', 16 | 'tsx', 17 | 'js', 18 | 'jsx', 19 | 'json', 20 | 'node' 21 | ], 22 | collectCoverageFrom: ['src/**/*.{ts,tsx.js,jsx,mjs}'], 23 | setupFilesAfterEnv: ['/src/utils/TestSetup.ts'], 24 | globals: { 25 | 'ts-jest': { 26 | diagnostics: false, 27 | }, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/signal/__tests__/SignalChannel.test.ts: -------------------------------------------------------------------------------- 1 | import wait from 'waait' 2 | 3 | import {channel} from '../SignalChannel' 4 | import {tick} from './Signal.test' 5 | 6 | describe('Channel', () => { 7 | describe('subscribe()', () => { 8 | it('yields when we send to the Channel', async () => { 9 | const check = jest.fn() 10 | const chan = channel(1) 11 | const ticker = tick(1, 1, [2, 3, 4]).map(a => a.getOrThrow()) 12 | 13 | ticker.on(chan.send) 14 | 15 | chan.subscribe() 16 | // @ts-ignore - test function 17 | .subscribe(check) 18 | 19 | await wait(50) 20 | 21 | const result = check.mock.calls.map(call => call[0]) 22 | 23 | expect(result).toEqual([2, 3, 4]) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /examples/signal/mario/src/MarioDOM.ts: -------------------------------------------------------------------------------- 1 | import {Character, charSpriteDescriptor} from './Mario' 2 | 3 | const groundHeight = 40 // px 4 | 5 | export const updatePosition = (c: Character) => { 6 | c.node.style.left = `${c.x}px` 7 | c.node.style.bottom = `${c.y + groundHeight}px` 8 | 9 | return c 10 | } 11 | 12 | export const updateSprite = (c: Character) => { 13 | c.node.className = charSpriteDescriptor(c) 14 | 15 | return c 16 | } 17 | 18 | export const onDOMContentLoaded = (action: () => void) => { 19 | if (document.readyState === 'interactive') { 20 | action() 21 | } else { 22 | document.addEventListener('DOMContentLoaded', action) 23 | } 24 | } 25 | 26 | export const getMarioNode = () => document.getElementById('mario') || document.body 27 | -------------------------------------------------------------------------------- /docs/examples/signal/mario/src/MarioDOM.ts: -------------------------------------------------------------------------------- 1 | import {Character, charSpriteDescriptor} from './Mario' 2 | 3 | const groundHeight = 40 // px 4 | 5 | export const updatePosition = (c: Character) => { 6 | c.node.style.left = `${c.x}px` 7 | c.node.style.bottom = `${c.y + groundHeight}px` 8 | 9 | return c 10 | } 11 | 12 | export const updateSprite = (c: Character) => { 13 | c.node.className = charSpriteDescriptor(c) 14 | 15 | return c 16 | } 17 | 18 | export const onDOMContentLoaded = (action: () => void) => { 19 | if (document.readyState === 'interactive') { 20 | action() 21 | } else { 22 | document.addEventListener('DOMContentLoaded', action) 23 | } 24 | } 25 | 26 | export const getMarioNode = () => document.getElementById('mario') || document.body 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "downlevelIteration": true, 8 | "strictFunctionTypes": true, 9 | "strictNullChecks": true, 10 | "strictPropertyInitialization": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noErrorTruncation": true, 13 | "jsx": "react", 14 | "target": "es5", 15 | "module": "commonjs", 16 | "moduleResolution": "node", 17 | "allowSyntheticDefaultImports": true, 18 | "esModuleInterop": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "removeComments": false, 22 | "preserveConstEnums": true, 23 | "sourceMap": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "*": [ 27 | "*", 28 | "src/bindings/*" 29 | ] 30 | }, 31 | "typeRoots": ["./node_modules/@types"], 32 | "lib": ["dom", "esnext.asynciterable", "es2015", "es2016", "es2017.object"], 33 | }, 34 | "exclude": ["lib", "docs"] 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional eslint cache 39 | .eslintcache 40 | 41 | # @std/esm cache 42 | .esm-cache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | # dotenv environment variables file 54 | .env 55 | 56 | # Yeoman local state 57 | .yo-rc.json 58 | 59 | # Compiled files 60 | lib 61 | !docs/examples/signal/mario/lib 62 | 63 | # Dependencies 64 | node_modules 65 | -------------------------------------------------------------------------------- /src/signal/SignalChannel.ts: -------------------------------------------------------------------------------- 1 | import Signal, {constant} from './Signal' 2 | 3 | /** 4 | * A Channel allows you to feed arbitrary values into a [[Signal]]. This is the simplest way to get 5 | * started with Signals. 6 | * 7 | * ```ts 8 | * const chan = channel('Hello, Bodil!') 9 | * const hello = chan.subscribe() 10 | * 11 | * // For each value sent to the Channel, transform it to uppercase and then log it. 12 | * hello.map(value => value.toUpperCase()).on(console.log) 13 | * 14 | * chan.send('This is great!') 15 | * ``` 16 | */ 17 | export default class Channel { 18 | /** @ignore */ 19 | private readonly signal: Signal 20 | 21 | /** @ignore */ 22 | private constructor (val: A) { 23 | this.signal = constant(val) 24 | } 25 | 26 | /** 27 | * Creates a channel, which allows you to feed arbitrary values into a signal. 28 | */ 29 | static channel = (val: A) => new Channel(val) 30 | 31 | /** 32 | * Sends a value to a given channel. 33 | */ 34 | // @ts-ignore - exception to enable Channel functionality 35 | send = (val: A) => this.signal.set(val) 36 | 37 | /** 38 | * Returns the signal of the values sent to the channel. 39 | */ 40 | subscribe = () => this.signal 41 | } 42 | 43 | export const channel = Channel.channel 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Community Funded Enterprises, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /examples/signal/mario/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const TerserPlugin = require('terser-webpack-plugin') 3 | const path = require('path') 4 | 5 | const babelConfig = require('./babel.config') 6 | 7 | module.exports = { 8 | mode: 'production', 9 | devtool: 'inline-source-map', 10 | entry: __dirname + '/src/Main', 11 | target: 'web', 12 | output: { 13 | path: __dirname + '/lib', 14 | filename: 'Main.js', 15 | library: 'Mario', 16 | libraryExport: 'default', 17 | libraryTarget: 'umd', 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.(ts|tsx|js|jsx)$/, 22 | include: [__dirname + '/src', path.resolve(__dirname + '../../../../src')], 23 | use: [{ 24 | loader: require.resolve('babel-loader'), 25 | options: { 26 | ...babelConfig, 27 | cacheDirectory: true, 28 | }, 29 | }], 30 | }], 31 | }, 32 | resolve: { 33 | modules: [__dirname + '../../../../node_modules'], 34 | extensions: ['.ts', '.tsx', '.jsx', '.js', '.json'], 35 | }, 36 | plugins: [ 37 | new webpack.HashedModuleIdsPlugin(), 38 | new webpack.optimize.AggressiveMergingPlugin(), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | ], 41 | optimization: { 42 | minimizer: [new TerserPlugin()], 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /docs/examples/signal/mario/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const TerserPlugin = require('terser-webpack-plugin') 3 | const path = require('path') 4 | 5 | const babelConfig = require('./babel.config') 6 | 7 | module.exports = { 8 | mode: 'production', 9 | devtool: 'inline-source-map', 10 | entry: __dirname + '/src/Main', 11 | target: 'web', 12 | output: { 13 | path: __dirname + '/lib', 14 | filename: 'Main.js', 15 | library: 'Mario', 16 | libraryExport: 'default', 17 | libraryTarget: 'umd', 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.(ts|tsx|js|jsx)$/, 22 | include: [__dirname + '/src', path.resolve(__dirname + '../../../../src')], 23 | use: [{ 24 | loader: require.resolve('babel-loader'), 25 | options: { 26 | ...babelConfig, 27 | cacheDirectory: true, 28 | }, 29 | }], 30 | }], 31 | }, 32 | resolve: { 33 | modules: [__dirname + '../../../../node_modules'], 34 | extensions: ['.ts', '.tsx', '.jsx', '.js', '.json'], 35 | }, 36 | plugins: [ 37 | new webpack.HashedModuleIdsPlugin(), 38 | new webpack.optimize.AggressiveMergingPlugin(), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | ], 41 | optimization: { 42 | minimizer: [new TerserPlugin()], 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/TestSetup.ts: -------------------------------------------------------------------------------- 1 | // Set the test environment 2 | process.env.NODE_ENV = 'test' 3 | 4 | const RealDate = Date 5 | 6 | // Global date mock 7 | class MockDate extends RealDate { 8 | static frozenTime = '2000-01-01T00:00:00z' // In the yeeeeeeaaaaar two thousaaaaaaand! 9 | 10 | // tslint:disable-next-line:no-any This has to be `any` to make any sense 11 | constructor (...args: any[]) { 12 | // @ts-ignore - test mock 13 | super(...args) 14 | 15 | if (args.length) { 16 | // @ts-ignore - test mock 17 | return new RealDate(...args) 18 | } 19 | 20 | return new RealDate(MockDate.frozenTime) 21 | } 22 | 23 | static now () { 24 | return new RealDate(MockDate.frozenTime).getTime() 25 | } 26 | } 27 | 28 | interface MockFile { 29 | parts: (string | Blob | ArrayBuffer | ArrayBufferView)[] 30 | filename: string 31 | properties: FilePropertyBag 32 | } 33 | 34 | // Global file mock 35 | // tslint:disable-next-line:no-unnecessary-class 36 | class MockFile implements MockFile { 37 | constructor ( 38 | parts: MockFile['parts'], 39 | filename: MockFile['filename'], 40 | properties: MockFile['properties'] 41 | ) { 42 | this.filename = filename 43 | this.properties = properties 44 | this.parts = parts 45 | } 46 | } 47 | 48 | // @ts-ignore - test mock 49 | global.Date = MockDate 50 | 51 | // @ts-ignore - test mock 52 | global.File = MockFile 53 | 54 | beforeEach(() => { 55 | jest.resetAllMocks() 56 | }) 57 | -------------------------------------------------------------------------------- /examples/signal/mario/src/Main.ts: -------------------------------------------------------------------------------- 1 | import Signal from '../../../../src/signal/Signal' 2 | import {animationFrame, keyPressed} from '../../../../src/signal/SignalDOM' 3 | import {getMarioNode, onDOMContentLoaded, updatePosition, updateSprite} from './MarioDOM' 4 | import {Character, Direction, Inputs, marioLogic} from './Mario' 5 | 6 | interface State { 7 | mario: Character 8 | } 9 | 10 | const KeyCodes = { 11 | left: 37, 12 | right: 39, 13 | jump: 32, 14 | } 15 | 16 | const gameLogic = (inputs: Inputs) => (state: State): State => ({ 17 | ...state, 18 | mario: marioLogic(inputs)(state.mario), 19 | }) 20 | 21 | const render = (state: State) => () => updateSprite(updatePosition(state.mario)) 22 | 23 | const getInputs = (left: boolean) => (right: boolean) => (jump: boolean) => ({left, right, jump}) 24 | 25 | export const main = () => { 26 | onDOMContentLoaded(() => { 27 | const frames = animationFrame() 28 | 29 | const initialState: State = { 30 | mario: { 31 | node: getMarioNode(), 32 | x: -50, 33 | y: 0, 34 | dx: 3, 35 | dy: 6, 36 | dir: Direction.Right, 37 | }, 38 | } 39 | 40 | const leftInputs = keyPressed(KeyCodes.left) 41 | const rightInputs = keyPressed(KeyCodes.right) 42 | const jumpInputs = keyPressed(KeyCodes.jump) 43 | 44 | const inputs = jumpInputs.apply(rightInputs.apply(leftInputs.map(getInputs))) 45 | 46 | const game = frames.sampleOn(inputs).foldp(gameLogic, initialState).map(render) 47 | 48 | Signal.run(game) 49 | }) 50 | } 51 | 52 | if (require.main === module) { 53 | main() 54 | } 55 | -------------------------------------------------------------------------------- /docs/examples/signal/mario/src/Main.ts: -------------------------------------------------------------------------------- 1 | import Signal from '../../../../src/signal/Signal' 2 | import {animationFrame, keyPressed} from '../../../../src/signal/SignalDOM' 3 | import {getMarioNode, onDOMContentLoaded, updatePosition, updateSprite} from './MarioDOM' 4 | import {Character, Direction, Inputs, marioLogic} from './Mario' 5 | 6 | interface State { 7 | mario: Character 8 | } 9 | 10 | const KeyCodes = { 11 | left: 37, 12 | right: 39, 13 | jump: 32, 14 | } 15 | 16 | const gameLogic = (inputs: Inputs) => (state: State): State => ({ 17 | ...state, 18 | mario: marioLogic(inputs)(state.mario), 19 | }) 20 | 21 | const render = (state: State) => () => updateSprite(updatePosition(state.mario)) 22 | 23 | const getInputs = (left: boolean) => (right: boolean) => (jump: boolean) => ({left, right, jump}) 24 | 25 | export const main = () => { 26 | onDOMContentLoaded(() => { 27 | const frames = animationFrame() 28 | 29 | const initialState: State = { 30 | mario: { 31 | node: getMarioNode(), 32 | x: -50, 33 | y: 0, 34 | dx: 3, 35 | dy: 6, 36 | dir: Direction.Right, 37 | }, 38 | } 39 | 40 | const leftInputs = keyPressed(KeyCodes.left) 41 | const rightInputs = keyPressed(KeyCodes.right) 42 | const jumpInputs = keyPressed(KeyCodes.jump) 43 | 44 | const inputs = jumpInputs.apply(rightInputs.apply(leftInputs.map(getInputs))) 45 | 46 | const game = frames.sampleOn(inputs).foldp(gameLogic, initialState).map(render) 47 | 48 | Signal.run(game) 49 | }) 50 | } 51 | 52 | if (require.main === module) { 53 | main() 54 | } 55 | -------------------------------------------------------------------------------- /src/__tests__/Reader.test.ts: -------------------------------------------------------------------------------- 1 | import Reader, {ask, asks} from '../Reader' 2 | 3 | describe('Reader', () => { 4 | const double = (n: number): number => n * 2 5 | 6 | test('map()', () => { 7 | const x = asks<{}, number>(() => 1) 8 | 9 | expect(x.map(double).run({})).toEqual(2) 10 | }) 11 | 12 | test('of()', () => { 13 | expect(Reader.of(1).run({})).toEqual(1) 14 | }) 15 | 16 | test('apply()', () => { 17 | const f = Reader.of(double) 18 | const x = Reader.of(1) 19 | 20 | expect(x.apply(f).run({})).toEqual(2) 21 | }) 22 | 23 | test('fmap()', () => { 24 | const x = asks(() => 'foo') 25 | const f = (s: string): Reader => Reader.of(s.length) 26 | 27 | expect(x.fmap(f).run({})).toEqual(3) 28 | }) 29 | 30 | test('ask()', () => { 31 | const x = ask() 32 | 33 | expect(x.run(1)).toEqual(1) 34 | }) 35 | 36 | test('asks()', () => { 37 | const x = asks((s: string) => s.length) 38 | const y = ask().local((s: string) => `${s}!`) 39 | 40 | expect(x.run('foo')).toEqual(3) 41 | expect(y.run('foo')).toEqual('foo!') 42 | }) 43 | 44 | test('local()', () => { 45 | type E = string 46 | interface E2 { 47 | name: string 48 | } 49 | 50 | const x = asks((e: E) => e.length).local((e2: E2) => e2.name) 51 | 52 | expect(x.run({name: 'foo'})).toEqual(3) 53 | }) 54 | 55 | test('compose()', () => { 56 | const x = asks(s => s.length) 57 | const y = asks(n => n >= 2) 58 | const z = y.compose(x) 59 | 60 | expect(z.run('foo')).toBe(true) 61 | expect(z.run('a')).toBe(false) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/__tests__/Functional.test.ts: -------------------------------------------------------------------------------- 1 | import {eq} from '../Functional' 2 | 3 | describe('Functional', () => { 4 | describe('eq', () => { 5 | it('validates simple equality', () => { 6 | const obj = {test: 'value'} 7 | 8 | expect(eq(123, 123)).toBe(true) 9 | expect(eq('test-string', 'test-string')).toBe(true) 10 | expect(eq(obj, obj)).toBe(true) 11 | }) 12 | 13 | it('validates simple inequality', () => { 14 | const obj = {test: 'value'} 15 | const otherObj = {...obj} 16 | 17 | expect(eq('test-string', 'other-test-string')).toBe(false) 18 | expect(eq(obj, otherObj)).toBe(false) 19 | }) 20 | 21 | it('validates array equality', () => { 22 | const obj = {test: 'value'} 23 | 24 | expect(eq([123, 456], [123, 456])).toBe(true) 25 | expect(eq(['test', 'string'], ['test', 'string'])).toBe(true) 26 | expect(eq([obj, obj], [obj, obj])).toBe(true) 27 | }) 28 | 29 | it('validates array inequality', () => { 30 | const obj = {test: 'value'} 31 | const otherObj = {...obj} 32 | 33 | expect(eq(['test', 'string'], ['other', 'test-string'])).toBe(false) 34 | expect(eq(['test', 'string'], ['string', 'test'])).toBe(false) 35 | expect(eq(['test', 'string'], ['other', 'test', 'string'])).toBe(false) 36 | expect(eq([obj, obj], [obj, otherObj])).toBe(false) 37 | }) 38 | 39 | it('validates Eq equality', () => { 40 | const obj = {test: 'value', equals: jest.fn().mockReturnValue(true)} 41 | const otherObj = {...obj} 42 | 43 | expect(eq(obj, otherObj)).toBe(true) 44 | expect(obj.equals).toHaveBeenCalledWith(otherObj) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/__tests__/State.test.ts: -------------------------------------------------------------------------------- 1 | import State, {modify, get, gets, put} from '../State' 2 | 3 | describe('State', () => { 4 | const double = (n: number) => n * 2 5 | 6 | describe('get', () => { 7 | it('gets state', () => { 8 | // @ts-ignore - test 9 | expect(get().run(1)).toEqual([1, 1]) 10 | }) 11 | }) 12 | 13 | describe('put', () => { 14 | it('puts state', () => { 15 | // @ts-ignore - test 16 | expect(put(2).run(1)).toEqual([undefined, 2]) 17 | }) 18 | }) 19 | 20 | describe('modify', () => { 21 | it('applies a function to the current state', () => { 22 | // @ts-ignore - test 23 | expect(modify(double).run(1)).toEqual([undefined, 2]) 24 | }) 25 | }) 26 | 27 | describe('eval', () => { 28 | it('run a computation, discarding the final state', () => { 29 | expect(State.of('a').eval(0)).toEqual('a') 30 | }) 31 | }) 32 | 33 | describe('exec', () => { 34 | it('runs a computation, discarding the result', () => { 35 | expect(State.of('a').exec(0)).toEqual(0) 36 | }) 37 | }) 38 | 39 | describe('gets', () => { 40 | it('gets a value which depends on the current state', () => { 41 | expect(gets(double).run(1)).toEqual([2, 1]) 42 | }) 43 | }) 44 | 45 | describe('map', () => { 46 | it('changes the type of the result in an action', () => { 47 | // @ts-ignore - test 48 | const x: State = new State((s: number) => [s - 1, s + 1]) 49 | 50 | expect(x.map(double).run(0)).toEqual([-2, 1]) 51 | }) 52 | }) 53 | 54 | describe('apply', () => { 55 | it('apply a function in the result of one State to the result in this State', () => { 56 | const doubled = State.of(double) 57 | const initial = State.of(1) 58 | 59 | expect(initial.apply(doubled).run(0)).toEqual([2, 0]) 60 | }) 61 | }) 62 | 63 | describe('fmap', () => { 64 | it('bind a new computation to the State, transforming the result', () => { 65 | // @ts-ignore - test 66 | const f = (_n: number): State => new State((s: number) => [s - 1, s + 1]) 67 | 68 | // @ts-ignore - test 69 | const x: State = new State((s: number) => [s - 1, s + 1]) 70 | 71 | expect(x.fmap(f).run(0)).toEqual([0, 2]) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monadism", 3 | "version": "5.0.1", 4 | "main": "lib/index.js", 5 | "description": "A small set of monadic Types implemented in TypeScript", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:communityfunded/monadism.git" 9 | }, 10 | "files": [ 11 | "lib" 12 | ], 13 | "author": "Community Funded (http://communityfunded.com)", 14 | "license": "BSD-2-Clause", 15 | "scripts": { 16 | "check.types": "yarn build --noEmit", 17 | "build": "tsc --declaration --pretty --skipLibCheck", 18 | "build.docs": "yarn build.docs.typedoc; yarn build.docs.config; yarn build.docs.mario", 19 | "build.docs.typedoc": "typedoc --out docs src --exclude \"**/*+(TestSetup|.test).ts\" --name Monadism", 20 | "build.docs.config": "cp .gh-pages.yml docs/_config.yml", 21 | "build.docs.mario": "mkdir -p docs/examples/signal; webpack --config examples/signal/mario/webpack.config.js; cp -r examples/signal/mario docs/examples/signal", 22 | "build.watch": "yarn build --watch", 23 | "lint": "tslint --format stylish --project tsconfig.json", 24 | "test.watch": "NODE_ENV=test jest --watch", 25 | "test": "NODE_ENV=test jest", 26 | "prepublish": "yarn build" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.4.5", 30 | "@babel/plugin-proposal-class-properties": "^7.4.4", 31 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 32 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4", 33 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 34 | "@babel/preset-env": "^7.4.5", 35 | "@babel/preset-typescript": "^7.3.3", 36 | "@types/jest": "^24.0.15", 37 | "@types/node": "^12.0.10", 38 | "@types/raf": "^3.4.0", 39 | "babel-loader": "^8.0.6", 40 | "husky": "^2.4.1", 41 | "identity-obj-proxy": "^3.0.0", 42 | "jest": "^24.8.0", 43 | "lint-staged": "^8.2.1", 44 | "raf": "^3.4.1", 45 | "terser-webpack-plugin": "^1.3.0", 46 | "ts-essentials": "^2.0.11", 47 | "ts-jest": "^24.0.2", 48 | "tslint": "^5.18.0", 49 | "tslint-config-prettier": "^1.18.0", 50 | "tslint-config-standard": "^8.0.1", 51 | "tslint-microsoft-contrib": "^6.2.0", 52 | "tslint-react": "^4.0.0", 53 | "typedoc": "^0.14.2", 54 | "typescript": "3.5.2", 55 | "waait": "^1.0.5", 56 | "webpack": "^4.35.0", 57 | "webpack-cli": "^3.3.5" 58 | }, 59 | "lint-staged": { 60 | "linters": { 61 | "!(docs/examples|lib|__tests__)/**/*.{ts,tsx}": [ 62 | "yarn lint", 63 | "git add" 64 | ] 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/__tests__/Either.test.ts: -------------------------------------------------------------------------------- 1 | import Either, {Left, Right} from '../Either' 2 | import {Just, Nothing} from '../Maybe' 3 | 4 | describe('Either', () => { 5 | describe('map()', () => { 6 | it('lifts functions into the Either type', () => { 7 | const f = (s: string): number => s.length 8 | 9 | expect(Right('abc').map(f).equals(Right(3))).toBe(true) 10 | expect(Left('s').map(f).equals(Left('s'))).toBe(true) 11 | }) 12 | }) 13 | 14 | describe('apply()', () => { 15 | it('applies a function based on the left or right case', () => { 16 | const f = (s: string): number => s.length 17 | 18 | expect(Right('abc').apply(Right number>(f)) 19 | .equals(Right(3))).toBe(true) 20 | 21 | expect(Left('a').apply(Right number>(f)) 22 | .equals(Left('a'))).toBe(true) 23 | 24 | expect(Right('abc').apply(Left number>('a')) 25 | .equals(Left('a'))).toBe(true) 26 | 27 | expect(Left('b').apply(Left number>('a')) 28 | .equals(Left('a'))).toBe(true) 29 | }) 30 | }) 31 | 32 | describe('fmap()', () => { 33 | it('chains Eithers together in a sequence', () => { 34 | const f = (s: string) => Right(s.length) 35 | 36 | expect(Right('abc').fmap(f).equals(Right(3))).toBe(true) 37 | expect(Left('a').fmap(f).equals(Left('a'))).toBe(true) 38 | expect(Right('abc').fmap(f).equals(Right(3))).toBe(true) 39 | }) 40 | }) 41 | 42 | describe('getOr()', () => { 43 | it('returns a default Right value if the Either is not a Right', () => { 44 | expect(Right(12).getOr(17)).toEqual(12) 45 | expect(Left('a').getOr(17)).toEqual(17) 46 | }) 47 | }) 48 | 49 | describe('fromMaybe()', () => { 50 | it('converts a Maybe to an Either', () => { 51 | expect(Either.fromMaybe('default', Nothing()).equals(Left('default'))).toBe(true) 52 | expect(Either.fromMaybe('default', Just(1)).equals(Right(1))).toBe(true) 53 | }) 54 | }) 55 | 56 | describe('fromNullable()', () => { 57 | it('converts a nullable to an Either', () => { 58 | expect(Either.fromNullable('default', null).equals(Left('default'))).toBe(true) 59 | expect(Either.fromNullable('default', undefined).equals(Left('default'))).toBe(true) 60 | expect(Either.fromNullable('default', 1).equals(Right(1))).toBe(true) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/Reader.ts: -------------------------------------------------------------------------------- 1 | import {Apply, Functor, Monad, identity} from './Functional' 2 | 3 | /** 4 | * A Reader represents a computation which can read values from a shared environment. Computations 5 | * are implemented as functions that take the environment (E) and return a value (A). 6 | * 7 | * Adapted from [fp-ts/Reader](https://github.com/gcanti/fp-ts/blob/master/src/Reader.ts). 8 | * 9 | * @typeparam E - The type representing the shared environment. 10 | * @typeparam A - The return value of the computation. 11 | */ 12 | export default class Reader implements Apply, Functor, Monad { 13 | /** 14 | * The wrapped computation. 15 | */ 16 | readonly run: (env: E) => A 17 | 18 | /** @ignore */ 19 | private constructor (run: (env: E) => A) { 20 | this.run = run 21 | } 22 | 23 | /** 24 | * Create a new Reader that always returns the given value. 25 | */ 26 | static of = (a: A): Reader => new Reader(() => a) 27 | 28 | /** 29 | * Reads the current environment. 30 | */ 31 | static ask = (): Reader => new Reader(identity) 32 | 33 | /** 34 | * Projects a value from the global context in a Reader. 35 | */ 36 | static asks = (func: (env: E) => A): Reader => new Reader(func) 37 | 38 | /** 39 | * Take a function that maps one type to another and lift it to work with Reader. 40 | * 41 | * @typeparam B - The type of the transformed return value. 42 | */ 43 | map = (func: (a: A) => B): Reader => 44 | new Reader((env: E) => func(this.run(env))) 45 | 46 | /** 47 | * Unpacks a Reader with a return value that is a function from `A` to `B` into a function from 48 | * `Reader` to `Reader`. Allows functions contained within a return value to transform 49 | * a return value contained within a Reader. 50 | * 51 | * @typeparam B - The type of the transformed return value. 52 | */ 53 | apply = (m: Reader B>): Reader => 54 | new Reader((env: E) => m.run(env)(this.run(env))) 55 | 56 | /** 57 | * Bind a new computation to the Reader. Also known as `flatMap`. 58 | * 59 | * @typeparam B - The type of the transformed return value. 60 | */ 61 | fmap = (func: (a: A) => Reader): Reader => 62 | new Reader((env: E) => func(this.run(env)).run(env)) 63 | 64 | /** 65 | * Execute a computation in a modified environment. 66 | * 67 | * @typeparam B - The type of the transformed environment. 68 | */ 69 | local = (func: (env: E2) => E): Reader => 70 | new Reader(env => this.run(func(env))) 71 | 72 | compose = (m: Reader): Reader => 73 | new Reader(env => this.run(m.run(env))) 74 | } 75 | 76 | export const ask = Reader.ask 77 | 78 | export const asks = Reader.asks 79 | -------------------------------------------------------------------------------- /src/Either.ts: -------------------------------------------------------------------------------- 1 | import {Eq, Nil, Monad, eq, empty, exists} from './Functional' 2 | import Maybe from './Maybe' 3 | 4 | export enum EitherType { 5 | Left, 6 | Right, 7 | } 8 | 9 | /** 10 | * A class to represent something that can be one value or the other. 11 | */ 12 | export default class Either implements Monad, Eq> { 13 | /** @ignore */ 14 | private readonly type: EitherType 15 | /** @ignore */ 16 | private readonly left: L 17 | /** @ignore */ 18 | private readonly right: R 19 | /** @ignore */ 20 | private readonly emptyLeft: L 21 | /** @ignore */ 22 | private readonly emptyRight: R 23 | 24 | /** @ignore */ 25 | private constructor (type: EitherType, values: {l?: L | Nil, r?: R | Nil}) { 26 | const {l, r} = values 27 | 28 | this.emptyLeft = empty() 29 | this.emptyRight = empty() 30 | 31 | this.type = type 32 | this.left = l || this.emptyLeft 33 | this.right = r || this.emptyRight 34 | } 35 | 36 | static Left = (l: L) => new Either(EitherType.Left, {l}) 37 | 38 | static Right = (r: R) => new Either(EitherType.Right, {r}) 39 | 40 | /** 41 | * If the value is Just something, turn it into a Right. If the value is Nothing, use the 42 | * provided default as a Left. 43 | */ 44 | static fromMaybe = (l: L, m: Maybe): Either => m.toBoolean() 45 | ? Right(m.toNullable()!) 46 | : Left(l) 47 | 48 | static fromNullable = (l: L, r?: R): Either => Boolean(r) 49 | ? Right(r!) 50 | : Left(l) 51 | 52 | isLeft = () => this.type === EitherType.Left 53 | 54 | isRight = () => this.type === EitherType.Right 55 | 56 | fmap = (func: (r: R) => Either) => this.isRight() 57 | ? func(this.right!) 58 | : Left(this.left!) 59 | 60 | /** 61 | * Lifts functions into the Either type. 62 | */ 63 | map = (func: (r: R) => B): Either => this.fmap(val => Right(func(val))) 64 | 65 | apply = (m: Either B>): Either => 66 | this.isLeft() 67 | ? Left(m.left === m.emptyLeft ? this.left! : m.left) 68 | : m.isLeft() 69 | ? Left(m.left!) 70 | : Right(m.right(this.right!)) 71 | 72 | equals = (m: Either) => this.isLeft() 73 | ? m.isLeft() && eq(this.left, m.left) 74 | : m.isRight() && eq(this.right, m.right) 75 | 76 | getOr = (r: R): R => this.isLeft() ? r : this.right! 77 | } 78 | 79 | export const Left = Either.Left 80 | 81 | export const Right = Either.Right 82 | 83 | export const either = (l: L | Nil, r: R | Nil) => { 84 | if (exists(l) && exists(r)) { 85 | throw new Error('Cannot construct an Either with both a Left and a Right') 86 | } 87 | 88 | if (!exists(l) && !exists(r)) { 89 | throw new Error('Cannot construct an Either with neither a Left or a Right') 90 | } 91 | 92 | if (exists(l) && !exists(r)) { 93 | return Left(l!) 94 | } 95 | 96 | return Right(r!) 97 | } 98 | -------------------------------------------------------------------------------- /src/__tests__/Writer.test.ts: -------------------------------------------------------------------------------- 1 | import Writer, {tell, unit, writer} from '../Writer' 2 | 3 | describe('Writer', () => { 4 | describe('tell()', () => { 5 | it('appends the provided story value to the current story', () => { 6 | /** 7 | * Returns the greatest common denominator, keeping a log of actions 8 | */ 9 | const gcd = (a: number, b: number): Writer => { 10 | if (b === 0) { 11 | return unit(a) 12 | } 13 | 14 | if (a === 0) { 15 | return unit(b) 16 | } 17 | 18 | return tell([`gcd ${a} ${b}`]).fmap(_ => { 19 | if (a > b) { 20 | return gcd(a - b, b) 21 | } 22 | 23 | return gcd(a, b - a) 24 | }) 25 | } 26 | 27 | const story = [ 28 | 'gcd 21 15', 29 | 'gcd 6 15', 30 | 'gcd 6 9', 31 | 'gcd 6 3', 32 | 'gcd 3 3', 33 | ] 34 | 35 | expect(gcd(21, 15).equals(writer(story, 3))).toBe(true) 36 | }) 37 | }) 38 | 39 | describe('map()', () => { 40 | it('applies the given function to the value', () => { 41 | const w = writer(['a'], 1) 42 | const double = (n: number): number => n * 2 43 | 44 | expect(w.map(double).equals(writer(['a'], 2))).toBe(true) 45 | }) 46 | }) 47 | 48 | describe('listen()', () => { 49 | it('modifies the Writer to include changes to the value', () => { 50 | const getCount = (w: Writer) => w.exec().length 51 | 52 | const story = [ 53 | 'first', 54 | 'second', 55 | 'third', 56 | ] 57 | 58 | const result = writer(story, 'unimportant value').listen(getCount) 59 | 60 | expect(result.equals(writer(story, 3))).toBe(true) 61 | }) 62 | }) 63 | 64 | describe('censor()', () => { 65 | it('modifies the Writer to include changes to the story', () => { 66 | const appendLabel = (s: string[]) => s.map(e => `${e} entry`) 67 | 68 | const story = [ 69 | 'first', 70 | 'second', 71 | 'third', 72 | ] 73 | 74 | const result = tell(story).censor(appendLabel) 75 | 76 | expect(result.exec()).toEqual([ 77 | 'first entry', 78 | 'second entry', 79 | 'third entry', 80 | ]) 81 | }) 82 | }) 83 | 84 | describe('apply()', () => { 85 | it('applies a function stored as the value of another Writer to the value of this Writer', () => { 86 | const appendLabel = (s: string) => `${s} value` 87 | const w = unit(appendLabel) 88 | 89 | const result = unit('test').apply(w) 90 | 91 | expect(result.eval()).toEqual('test value') 92 | }) 93 | }) 94 | 95 | describe('on()', () => { 96 | it('runs a side-effect on the value', () => { 97 | const w = writer(['a'], 1) 98 | const callback = jest.fn().mockReturnValue(2) 99 | 100 | w.on(callback) 101 | 102 | expect(w.equals(writer(['a'], 1))).toBe(true) 103 | expect(callback).toHaveBeenCalledWith(1) 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /src/Functional.ts: -------------------------------------------------------------------------------- 1 | export type Nil = null | undefined 2 | 3 | export type Empty = A & {_tag?: 'EMPTY'} 4 | 5 | /** 6 | * Monoid provides for types that can be "empty" 7 | */ 8 | export interface Monoid { 9 | empty: A 10 | } 11 | 12 | /** 13 | * Eq provides the `equals` function for measuring equality. 14 | */ 15 | export interface Eq { 16 | equals (a: A): boolean 17 | } 18 | 19 | /** 20 | * Alt allows for a choice to be made between two types. 21 | */ 22 | export interface Alt { 23 | alt (m: Alt): Alt 24 | } 25 | 26 | /** 27 | * Functors can transform from one type to another. 28 | */ 29 | export interface Functor { 30 | map (func: (a: A) => B): Functor 31 | } 32 | 33 | /** 34 | * Apply represents something that can be "applied", which unpacks a type wrapping a function into 35 | * a function that takes a type wrapping the input returning a type wrapping the output. 36 | * 37 | * For example: 38 | * `Maybe<(a: A) => B>` would be transformed into `(a: Maybe) => Maybe` 39 | */ 40 | export interface Apply extends Functor { 41 | apply (func: Apply<(m: A) => B>): Apply 42 | } 43 | 44 | /** 45 | * A Monad is an operation that supports additional computations, chaining each to the end of the 46 | * last. Then `fmap` function is also known as `flatMap` or `bind` in many functional languages. 47 | */ 48 | export interface Monad extends Apply { 49 | fmap (func: (a: A) => Monad): Monad 50 | } 51 | 52 | /** 53 | * Extend allows sequencing of functions that accept a value of the given type and return a 54 | * result of the type that the type is wrapping. 55 | */ 56 | export interface Extend extends Apply { 57 | extend (func: (a: Extend) => B): Extend 58 | } 59 | 60 | export interface Foldable { 61 | fold (b: B, func: (a: A) => B): B 62 | } 63 | 64 | /*** 65 | * Utils 66 | */ 67 | 68 | export function eq (a: A, b: A): boolean 69 | export function eq (a: A[], b: A[]): boolean 70 | export function eq> (a: A, b: A): boolean { 71 | if (Array.isArray(a) && Array.isArray(b)) { 72 | if (a.length > 0 && a.length === b.length) { 73 | return a.map((item, i) => eq(item, b[i])) 74 | .reduce((memo, item) => memo && Boolean(item)) 75 | } 76 | 77 | return false 78 | } 79 | 80 | if (a && a.equals) { 81 | return a.equals(b) 82 | } 83 | 84 | return a === b 85 | } 86 | 87 | /** 88 | * Cast a Symbol as an empty value, to represent emptiness without undefined. 89 | */ 90 | const emptySymbol = Symbol('MONADISM:EMPTY') 91 | 92 | export const empty = () => emptySymbol as unknown as Empty 93 | 94 | /** 95 | * Ensure a value is not `null` or `undefined`. 96 | */ 97 | export const exists = (t: T) => t !== null && t !== undefined 98 | 99 | export const identity = (a: A) => a 100 | 101 | /** 102 | * A classic functional `compose`. 103 | */ 104 | export const compose = (fn1: (a: R) => R, ...fns: ((a: R) => R)[]) => 105 | fns.reduce((prevFn, nextFn) => value => prevFn(nextFn(value)), fn1) 106 | -------------------------------------------------------------------------------- /src/Writer.ts: -------------------------------------------------------------------------------- 1 | import {Eq, Monad, Monoid, eq, empty} from './Functional' 2 | 3 | /** 4 | * The Writer monad provides the ability to accumulate a secondary Story (S) value in addition to 5 | * the return value (A) of a computation. 6 | * 7 | * @typeparam S - The array-based value of the Story (the accumulator). 8 | * @typeparam A - The current return value of the computation. 9 | */ 10 | export default class Writer implements Eq>, Monoid, Monad { 11 | empty = empty() 12 | 13 | /** @ignore */ 14 | private readonly story: S[] 15 | /** @ignore */ 16 | private readonly value: A 17 | 18 | /** @ignore */ 19 | private constructor(story: S[], value: A) { 20 | this.story = story 21 | this.value = value 22 | } 23 | 24 | /** 25 | * Creates a new Writer with a story and a value. 26 | */ 27 | static writer = (story: S[], value: A) => new Writer(story, value) 28 | 29 | /** 30 | * Creates a new Writer with an initial story and an empty value. 31 | */ 32 | static tell = (story: S[]) => new Writer(story, empty()) 33 | 34 | /** 35 | * Creates a new Writer with an empty story and an initial value. 36 | */ 37 | static unit = (a: A) => new Writer([], a) 38 | 39 | isEmpty = () => this.value === this.empty 40 | 41 | /** 42 | * Returns the value at the end of a sequence of Writer computations. 43 | */ 44 | eval = () => this.value 45 | 46 | /** 47 | * Returns the story at the end of a sequence of Writer computations. 48 | */ 49 | exec = () => this.story 50 | 51 | /** 52 | * Checks equality between two Writers of the same type. 53 | */ 54 | equals = (other: Writer) => eq(this.story, other.story) && eq(this.value, other.value) 55 | 56 | /** 57 | * Modify the Writer's story by applying a function. 58 | */ 59 | censor = (func: (s: S[]) => S[]): Writer => writer(func(this.story), this.value) 60 | 61 | /** 62 | * Applies a function to the accumulated value of a Writer. 63 | */ 64 | map = (func: (a: A) => B): Writer => this.fmap(value => unit(func(value))) 65 | 66 | /** 67 | * Applies a function stored as the value of another Writer to the value of this Writer. 68 | */ 69 | apply = (func: Writer B>): Writer => 70 | writer(this.story.concat(func.story), func.value(this.value)) 71 | 72 | /** 73 | * Binds a new operation to this Writer. 74 | */ 75 | fmap = (func: (a: A) => Writer): Writer => { 76 | const wb = func(this.value) 77 | 78 | return writer(this.story.concat(wb.story), wb.value) 79 | } 80 | 81 | /** 82 | * Modifies the Writer to include changes to the value. 83 | */ 84 | listen = (func: (w: Writer) => B): Writer => writer(this.story, func(this)) 85 | 86 | /** 87 | * Run a side effect on the value. 88 | */ 89 | on = (callback: (a: A) => void) => this.map(val => { 90 | callback(val) 91 | 92 | return val 93 | }) 94 | } 95 | 96 | export const writer = Writer.writer 97 | 98 | export const tell = Writer.tell 99 | 100 | export const unit = Writer.unit 101 | -------------------------------------------------------------------------------- /examples/signal/mario/src/Mario.ts: -------------------------------------------------------------------------------- 1 | import {compose} from '../../../../src/Functional' 2 | 3 | export enum Activity { 4 | Walking = 'walk', 5 | Standing = 'stand', 6 | Jumping = 'jump', 7 | } 8 | 9 | export enum Direction { 10 | Left = 'left', 11 | Right = 'right', 12 | } 13 | 14 | export type Character = { 15 | node: HTMLElement 16 | x: number 17 | y: number 18 | dx: number 19 | dy: number 20 | dir: Direction 21 | } 22 | 23 | export interface Inputs {left: boolean, right: boolean, jump: boolean} 24 | 25 | const gravity = 0.15 // px / frame^2 26 | 27 | const maxMoveSpeed = 2.5 // px / frame 28 | 29 | const groundAccel = 0.06 // px / frame^2 30 | 31 | const airAccel = 0.04 // px / frame^2 32 | 33 | const groundFriction = 0.1 // px / frame^2 34 | 35 | const airFriction = 0.02 // px / frame^2 36 | 37 | const jumpCoefficient = 0.8 // px / frame^3 38 | 39 | const minJumpSpeed = 4.0 // px / frame 40 | 41 | const isAirborne = (c: Character): boolean => c.y > 0.0 42 | 43 | const isStanding = (c: Character): boolean => c.dx === 0.0 44 | 45 | const currentActivity = (c: Character): Activity => { 46 | if (isAirborne(c)) { 47 | return Activity.Jumping 48 | } 49 | 50 | if (isStanding(c)) { 51 | return Activity.Jumping 52 | } 53 | 54 | return Activity.Walking 55 | } 56 | 57 | export const charSpriteDescriptor = (c: Character): string => 58 | `character ${currentActivity(c)} ${c.dir}` 59 | 60 | const accel = (c: Character): number => isAirborne(c) ? airAccel : groundAccel 61 | 62 | const friction = (c: Character): number => isAirborne(c) ? airFriction : groundFriction 63 | 64 | /** 65 | * When Mario is in motion, his position changes 66 | */ 67 | const velocity = (c: Character): Character => ({...c, x: c.x + c.dx, y: c.y + c.dy}) 68 | 69 | /** 70 | * When Mario is above the ground, he is continuously pulled downward 71 | */ 72 | const applyGravity = (c: Character): Character => { 73 | if (c.y <= -c.dy) { 74 | return {...c, y: 0, dy: 0} 75 | } 76 | 77 | return {...c, dy: c.dy - gravity} 78 | } 79 | 80 | const applyFriction = (c: Character): Character => { 81 | if (c.dx === 0.0) { 82 | return c 83 | } 84 | 85 | if (Math.abs(c.dx) <= friction(c)) { 86 | return {...c, dx: 0.0} 87 | } 88 | 89 | if (c.dx > 0.0) { 90 | return {...c, dx: c.dx - friction(c)} 91 | } 92 | 93 | return {...c, dx: c.dx + friction(c)} 94 | } 95 | 96 | /** 97 | * Mario can move himself left/right with a fixed acceleration 98 | */ 99 | const walk = (left: boolean, right: boolean) => (c: Character) => { 100 | if (left && !right) { 101 | return {...c, dx: Math.max(-maxMoveSpeed, c.dx - accel(c)), dir: Direction.Left} 102 | } 103 | 104 | if (right && !left) { 105 | return {...c, dx: Math.min(maxMoveSpeed, c.dx + accel(c)), dir: Direction.Right} 106 | } 107 | 108 | return applyFriction(c) 109 | } 110 | 111 | const jump = (jmp: boolean) => (c: Character): Character => { 112 | if (jmp && !isAirborne(c)) { 113 | return {...c, dy: minJumpSpeed + (jumpCoefficient * Math.abs(c.dx))} 114 | } 115 | 116 | if (!jmp && isAirborne(c) && c.dy > 0.0) { 117 | return {...c, dy: c.dy - gravity} 118 | } 119 | 120 | return c 121 | } 122 | 123 | export const marioLogic = (inputs: Inputs) => compose( 124 | velocity, 125 | applyGravity, 126 | walk(inputs.left, inputs.right), 127 | jump(inputs.jump) 128 | ) 129 | -------------------------------------------------------------------------------- /docs/examples/signal/mario/src/Mario.ts: -------------------------------------------------------------------------------- 1 | import {compose} from '../../../../src/Functional' 2 | 3 | export enum Activity { 4 | Walking = 'walk', 5 | Standing = 'stand', 6 | Jumping = 'jump', 7 | } 8 | 9 | export enum Direction { 10 | Left = 'left', 11 | Right = 'right', 12 | } 13 | 14 | export type Character = { 15 | node: HTMLElement 16 | x: number 17 | y: number 18 | dx: number 19 | dy: number 20 | dir: Direction 21 | } 22 | 23 | export interface Inputs {left: boolean, right: boolean, jump: boolean} 24 | 25 | const gravity = 0.15 // px / frame^2 26 | 27 | const maxMoveSpeed = 2.5 // px / frame 28 | 29 | const groundAccel = 0.06 // px / frame^2 30 | 31 | const airAccel = 0.04 // px / frame^2 32 | 33 | const groundFriction = 0.1 // px / frame^2 34 | 35 | const airFriction = 0.02 // px / frame^2 36 | 37 | const jumpCoefficient = 0.8 // px / frame^3 38 | 39 | const minJumpSpeed = 4.0 // px / frame 40 | 41 | const isAirborne = (c: Character): boolean => c.y > 0.0 42 | 43 | const isStanding = (c: Character): boolean => c.dx === 0.0 44 | 45 | const currentActivity = (c: Character): Activity => { 46 | if (isAirborne(c)) { 47 | return Activity.Jumping 48 | } 49 | 50 | if (isStanding(c)) { 51 | return Activity.Jumping 52 | } 53 | 54 | return Activity.Walking 55 | } 56 | 57 | export const charSpriteDescriptor = (c: Character): string => 58 | `character ${currentActivity(c)} ${c.dir}` 59 | 60 | const accel = (c: Character): number => isAirborne(c) ? airAccel : groundAccel 61 | 62 | const friction = (c: Character): number => isAirborne(c) ? airFriction : groundFriction 63 | 64 | /** 65 | * When Mario is in motion, his position changes 66 | */ 67 | const velocity = (c: Character): Character => ({...c, x: c.x + c.dx, y: c.y + c.dy}) 68 | 69 | /** 70 | * When Mario is above the ground, he is continuously pulled downward 71 | */ 72 | const applyGravity = (c: Character): Character => { 73 | if (c.y <= -c.dy) { 74 | return {...c, y: 0, dy: 0} 75 | } 76 | 77 | return {...c, dy: c.dy - gravity} 78 | } 79 | 80 | const applyFriction = (c: Character): Character => { 81 | if (c.dx === 0.0) { 82 | return c 83 | } 84 | 85 | if (Math.abs(c.dx) <= friction(c)) { 86 | return {...c, dx: 0.0} 87 | } 88 | 89 | if (c.dx > 0.0) { 90 | return {...c, dx: c.dx - friction(c)} 91 | } 92 | 93 | return {...c, dx: c.dx + friction(c)} 94 | } 95 | 96 | /** 97 | * Mario can move himself left/right with a fixed acceleration 98 | */ 99 | const walk = (left: boolean, right: boolean) => (c: Character) => { 100 | if (left && !right) { 101 | return {...c, dx: Math.max(-maxMoveSpeed, c.dx - accel(c)), dir: Direction.Left} 102 | } 103 | 104 | if (right && !left) { 105 | return {...c, dx: Math.min(maxMoveSpeed, c.dx + accel(c)), dir: Direction.Right} 106 | } 107 | 108 | return applyFriction(c) 109 | } 110 | 111 | const jump = (jmp: boolean) => (c: Character): Character => { 112 | if (jmp && !isAirborne(c)) { 113 | return {...c, dy: minJumpSpeed + (jumpCoefficient * Math.abs(c.dx))} 114 | } 115 | 116 | if (!jmp && isAirborne(c) && c.dy > 0.0) { 117 | return {...c, dy: c.dy - gravity} 118 | } 119 | 120 | return c 121 | } 122 | 123 | export const marioLogic = (inputs: Inputs) => compose( 124 | velocity, 125 | applyGravity, 126 | walk(inputs.left, inputs.right), 127 | jump(inputs.jump) 128 | ) 129 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react", 4 | "tslint-config-standard", 5 | "tslint-microsoft-contrib", 6 | "tslint-config-prettier" 7 | ], 8 | "rules": { 9 | "array-bracket-spacing": [true, "never"], 10 | "arrow-return-shorthand": true, 11 | "no-backbone-get-set-outside-model": false, 12 | "completed-docs": false, 13 | "curly": [true, "ignore-same-line"], 14 | "cyclomatic-complexity": [true, 24], 15 | "eofline": true, 16 | "export-name": false, 17 | "function-name": false, 18 | "import-name": false, 19 | "import-spacing": true, 20 | "interface-name": [true, "never-prefix"], 21 | "jsx-alignment": false, 22 | "jsx-boolean-value": false, 23 | "jsx-curly-spacing": false, 24 | "jsx-no-multiline-js": false, 25 | "match-default-export-name": false, 26 | "max-func-body-length": 200, 27 | "member-access": [true, "no-public"], 28 | "member-ordering": {"options": [{"order": "fields-first"}]}, 29 | "missing-jsdoc": false, 30 | "mocha-no-side-effect-code": false, 31 | "no-arg": true, 32 | "no-banned-terms": false, 33 | "no-bitwise": true, 34 | "no-console": false, 35 | "no-construct": true, 36 | "no-default-export": false, 37 | "no-duplicate-switch-case": true, 38 | "no-duplicate-variable": true, 39 | "no-empty-interface": false, 40 | "no-eval": true, 41 | "no-function-expression": false, 42 | "no-http-string": false, 43 | "no-implicit-dependencies": false, 44 | "no-multiline-string": false, 45 | "no-null-keyword": false, 46 | "no-non-null-assertion": false, 47 | "no-relative-imports": false, 48 | "no-require-imports": {"severity": "warning"}, 49 | "no-reserved-keywords": false, 50 | "no-return-await": true, 51 | "no-single-line-block-comment": false, 52 | "no-string-throw": true, 53 | "no-submodule-imports": false, 54 | "no-suspicious-comment": false, 55 | "no-use-before-declare": false, 56 | "no-unsafe-any": false, 57 | "no-unused-variable": false, 58 | "no-var-requires": true, 59 | "no-void-expression": false, 60 | "object-curly-spacing": [true, "never"], 61 | "ordered-imports": false, 62 | "prefer-for-of": true, 63 | "prefer-object-spread": true, 64 | "prefer-type-cast": false, 65 | "quotemark": [true, "single", "jsx-double", "avoid-escape"], 66 | "semicolon": [true, "never"], 67 | "strict-boolean-expressions": false, 68 | "strict-type-predicates": false, 69 | "ter-func-call-spacing": [true, "never"], 70 | "trailing-comma": [true, "always"], 71 | "type-literal-delimiter": false, 72 | "typedef": false, 73 | "typedef-whitespace": [ 74 | true, 75 | { 76 | "call-signature": "nospace", 77 | "index-signature": "nospace", 78 | "parameter": "nospace", 79 | "property-declaration": "nospace", 80 | "variable-declaration": "nospace" 81 | }, 82 | { 83 | "call-signature": "onespace", 84 | "index-signature": "onespace", 85 | "parameter": "onespace", 86 | "property-declaration": "onespace", 87 | "variable-declaration": "onespace" 88 | } 89 | ], 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-operator", 96 | "check-typecast", 97 | "check-rest-spread", 98 | "check-type-operator" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/State.ts: -------------------------------------------------------------------------------- 1 | import {Monad} from './Functional' 2 | 3 | /** 4 | * State is a Monad with the operations `get` and `put`, which can be used to model a single piece 5 | * of mutable state (S) with a return value (A). 6 | */ 7 | export default class State implements Monad { 8 | /** 9 | * The wrapped computation. 10 | */ 11 | readonly run: (s: S) => [A, S] 12 | 13 | /** @ignore */ 14 | private constructor (run: (s: S) => [A, S]) { 15 | this.run = run 16 | } 17 | 18 | /** 19 | * Get the current state. 20 | * 21 | * ```ts 22 | * const state = get() 23 | * 24 | * state.eval(1) // 1 25 | * state.exec(1) // 1 26 | * ``` 27 | */ 28 | static get = (): State => new State(s => [s, s]) 29 | 30 | /** 31 | * Set the state. 32 | * 33 | * ```ts 34 | * const state = put(2) 35 | * 36 | * state.eval(1) // undefined 37 | * state.exec(1) // 2 38 | * ``` 39 | */ 40 | static put = (s: S): State => new State(() => [undefined, s]) 41 | 42 | /** 43 | * Create a new state that will replace the return value when run. 44 | */ 45 | static of = (a: A): State => new State(s => [a, s]) 46 | 47 | /** 48 | * Modify the state by applying a function to the current state. 49 | * 50 | * ```ts 51 | * const double = (n: number) => n * 2 52 | * 53 | * const state = modify(double) 54 | * 55 | * state.eval(1) // undefined 56 | * state.exec(1) // 2 57 | * ``` 58 | */ 59 | static modify = (func: (s: S) => S): State => 60 | new State(s => [undefined, func(s)]) 61 | 62 | /** 63 | * Get a value which depends on the current state. 64 | * 65 | * ```ts 66 | * const state = gets(double) 67 | * 68 | * state.eval(1) // 2 69 | * state.exec(1) // 1 70 | * ``` 71 | */ 72 | static gets = (func: (s: S) => A): State => new State(s => [func(s), s]) 73 | 74 | /** 75 | * Run a computation, discarding the final state. 76 | */ 77 | eval = (s: S) => this.run(s)[0] 78 | 79 | /** 80 | * Run a computation, discarding the result. 81 | */ 82 | exec = (s: S) => this.run(s)[1] 83 | 84 | /** 85 | * Change the type of the result in an action. 86 | * 87 | * ```ts 88 | * const double = (n: number) => n * 2 89 | * 90 | * const state = State.of(1).map(double) 91 | * 92 | * state.eval(0) // 2 93 | * ``` 94 | */ 95 | map = (func: (a: A) => B): State => new State(s => { 96 | const [a, s1] = this.run(s) 97 | 98 | return [func(a), s1] 99 | }) 100 | 101 | /** 102 | * Bind a new computation to the State, transforming the result. 103 | * 104 | * ```ts 105 | * const double = (n: number) => n * 2 106 | * 107 | * const state = State.of(1).fmap(a => put(double(a))) 108 | * 109 | * state.exec(0) // 2 110 | * ``` 111 | */ 112 | fmap = (func: (a: A) => State) => new State(s => { 113 | const [a, s1] = this.run(s) 114 | 115 | return func(a).run(s1) 116 | }) 117 | 118 | /** 119 | * Apply a function in the result of one State to the result in this State. 120 | * 121 | * ```ts 122 | * const doubled = State.of(double) 123 | * const initial = State.of(1) 124 | * 125 | * const state = initial.apply(doubled) 126 | * 127 | * state.eval(0) // 2 128 | * ``` 129 | */ 130 | apply = (m: State B>): State => m.fmap(func => this.map(func)) 131 | } 132 | 133 | export const get = State.get 134 | 135 | export const put = State.put 136 | 137 | export const modify = State.modify 138 | 139 | export const gets = State.gets 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monadism 2 | 3 | [![npm](https://img.shields.io/npm/v/monadism.svg)](https://www.npmjs.com/package/monadism) [![license](https://img.shields.io/github/license/communityfunded/monadism.svg)](LICENSE) [![GitHub issues](https://img.shields.io/github/issues/communityfunded/monadism.svg)](https://github.com/communityfunded/monadism/issues) [![github]( https://img.shields.io/github/stars/communityfunded/monadism.svg?style=social)](https://github.com/communityfunded/monadism) 4 | 5 | A set of practical monads implemented in TypeScript, with the goal of being easy to learn and use in daily work. 6 | 7 | ## Installation 8 | 9 | Install with [Yarn]: 10 | 11 | ```sh 12 | yarn add monadism 13 | ``` 14 | 15 | Or with [npm]: 16 | 17 | ```sh 18 | npm i monadism 19 | ``` 20 | 21 | Then, import ES-module style: 22 | 23 | ```ts 24 | import {Just, Nothing, maybe} from 'monadism' 25 | ``` 26 | 27 | Or, via `require`: 28 | 29 | ```ts 30 | const {Just, Nothing, maybe} = require('monadism') 31 | ``` 32 | 33 | ## Docs 34 | 35 | * [Maybe](https://communityfunded.github.io/monadism/classes/_maybe_.maybe.html) 36 | * Represent optional values (A) without `null` or `undefined`. 37 | * [Either](https://communityfunded.github.io/monadism/classes/_either_.either.html) 38 | * A value that can be either the type on the Left, or the type on the Right. 39 | * [State](https://communityfunded.github.io/monadism/classes/_state_.state.html) 40 | * A Monad which can be used to model a single piece of mutable state (S). 41 | * [Reader](https://communityfunded.github.io/monadism/classes/_reader_.reader.html) 42 | * A computation with a return value (A) which can read values from a shared environment (E). 43 | * [Writer](https://communityfunded.github.io/monadism/classes/_writer_.writer.html) 44 | * Accumulate a secondary story (S) value alongside the return value (A) of a computation. 45 | * [Signal](https://communityfunded.github.io/monadism/classes/_signal_signal_.signal.html) 46 | * A lightweight FRP-like Monad heavily inspired by the Elm Signal. 47 | * Ported from [purescript-signal] by [Bodil Stokke](https://github.com/bodil). 48 | 49 | ## Influences 50 | 51 | Monadism builds on the inspiration of a variety of different projects. Check them out for a deep dive into things like category theory and different data structures! 52 | 53 | * [purescript-transformers](https://github.com/purescript/purescript-transformers) - Monad and comonad transformers based on [mtl](http://hackage.haskell.org/package/mtl). 54 | * [bs-abstract](https://github.com/Risto-Stevcev/bs-abstract) - Bucklescript interfaces and implementations for category theory and abstract algebra. 55 | * [fp-ts](https://github.com/gcanti/fp-ts) - A library for typed functional programming in TypeScript. 56 | * [TsMonad](https://github.com/cbowdon/TsMonad) - Little monad library designed for TypeScript. 57 | 58 | ## Examples 59 | 60 | ### Signal 61 | 62 | ### Monadism Mario 63 | 64 | [examples/signal/mario](examples/signal/mario) 65 | 66 | An example of a Mario game screen using Signals to manage user input and game state. Adapted from [Michael Ficarra](https://github.com/michaelficarra)'s example at https://github.com/michaelficarra/purescript-demo-mario 67 | 68 | ![Mario](https://user-images.githubusercontent.com/30199/56088170-97d4dc80-5e38-11e9-945b-293123d4fca7.gif) 69 | 70 | Play a live demo [here](https://communityfunded.github.io/monadism/examples/signal/mario/)! 71 | 72 | ## Development 73 | 74 | Install dependencies with [Yarn]: 75 | 76 | ```sh 77 | yarn 78 | ``` 79 | 80 | Or with [npm]: 81 | 82 | ```sh 83 | npm i 84 | ``` 85 | 86 | To build changes to the TypeScript code: 87 | 88 | ```sh 89 | yarn build 90 | ``` 91 | 92 | To build in watch mode: 93 | 94 | ```sh 95 | yarn build.wach 96 | ``` 97 | 98 | To build the docs: 99 | 100 | ```sh 101 | yarn build.docs 102 | ``` 103 | 104 | ## Authors 105 | 106 | * [Brandon Konkle](https://github.com/bkonkle) 107 | * [Emma Ramirez](https://github.com/EmmaRamirez) 108 | 109 | [Yarn]: https://yarnpkg.com 110 | [npm]: https://www.npmjs.com 111 | [purescript-signal]: https://github.com/bodil/purescript-signal 112 | -------------------------------------------------------------------------------- /examples/signal/mario/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mario! 6 | 7 | 122 | 123 | 124 |
125 |
126 |
127 | Mario 128 | 5 129 |
130 | 0 131 |
132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/examples/signal/mario/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mario! 6 | 7 | 122 | 123 | 124 |
125 |
126 |
127 | Mario 128 | 5 129 |
130 | 0 131 |
132 | 133 | 134 | -------------------------------------------------------------------------------- /src/signal/SignalDOM.ts: -------------------------------------------------------------------------------- 1 | import raf from 'raf' 2 | 3 | import Signal, {Time, constant} from './Signal' 4 | 5 | export type CoordinatePair = {x: number, y: number} 6 | export type DimensionPair = {w: number, h: number} 7 | 8 | export enum MouseButton { 9 | MouseLeftButton, 10 | MouseMiddleButton, 11 | MouseIE8MiddleButton, 12 | MouseRightButton, 13 | } 14 | 15 | /** 16 | * Creates a signal which will be `true` when the key matching the given key code is pressed, and 17 | * `false` when it's released. 18 | */ 19 | export const keyPressed = (keyCode: number) => { 20 | const out = constant(false) 21 | 22 | window.addEventListener('keydown', (e) => { 23 | if (e.keyCode === keyCode) { 24 | // @ts-ignore - internal exception 25 | out.set(true) 26 | } 27 | }) 28 | 29 | window.addEventListener('keyup', (e) => { 30 | if (e.keyCode === keyCode) { 31 | // @ts-ignore - internal exception 32 | out.set(false) 33 | } 34 | }) 35 | 36 | return out 37 | } 38 | 39 | /** 40 | * Creates a signal which will be `true` when the given mouse button is pressed, and `false` when 41 | * it's released. 42 | */ 43 | export const mouseButton = (button: number) => { 44 | const out = constant(false) 45 | 46 | window.addEventListener('mousedown', (e) => { 47 | if (e.button === button) { 48 | // @ts-ignore - internal exception 49 | out.set(true) 50 | } 51 | }) 52 | 53 | window.addEventListener('mouseup', (e) => { 54 | if (e.button === button) { 55 | // @ts-ignore - internal exception 56 | out.set(false) 57 | } 58 | }) 59 | 60 | return out 61 | } 62 | 63 | /** 64 | * Creates a signal which will be `true` when the given mouse button is pressed, and `false` when 65 | * it's released. Note: in IE8 and earlier you need to use MouseIE8MiddleButton if you want to query 66 | * the middle button. 67 | */ 68 | export const mouseButtonPressed = (button: MouseButton) => { 69 | if (button === MouseButton.MouseLeftButton) { 70 | return mouseButton(0) 71 | } 72 | 73 | if (button === MouseButton.MouseRightButton) { 74 | return mouseButton(2) 75 | } 76 | 77 | if (button === MouseButton.MouseMiddleButton) { 78 | return mouseButton(2) 79 | } 80 | 81 | return mouseButton(4) 82 | } 83 | 84 | /** 85 | * A signal containing the current state of the touch device. 86 | */ 87 | export const touch = () => { 88 | const initial: Touch[] = [] 89 | const out = constant(initial) 90 | 91 | const report = (e: TouchEvent) => { 92 | const touches = [] 93 | const l = e.touches.length 94 | 95 | for (let i = 0; i < l; i = i + 1) { 96 | const t = e.touches.item(i) 97 | if (t) { 98 | touches.push(t) 99 | } 100 | } 101 | 102 | // @ts-ignore - internal exception 103 | out.set(touches) 104 | } 105 | 106 | window.addEventListener('touchstart', report) 107 | window.addEventListener('touchend', report) 108 | window.addEventListener('touchmove', report) 109 | window.addEventListener('touchcancel', report) 110 | 111 | return out 112 | } 113 | 114 | /** 115 | * A signal which will be `true` when at least one finger is touching the touch device, and `false` 116 | * otherwise. 117 | */ 118 | export const tap = () => touch().map(t => t === [] ? false : true) 119 | 120 | /** 121 | * A signal containing the current mouse position. 122 | */ 123 | export const mousePos = (): Signal => { 124 | const out = constant({x: 0, y: 0}) 125 | 126 | window.addEventListener('mousemove', (e) => { 127 | if (e.pageX !== undefined && e.pageY !== undefined) { 128 | // @ts-ignore - internal exception 129 | out.set({x: e.pageX, y: e.pageY}) 130 | } else if (e.clientX !== undefined && e.clientY !== undefined) { 131 | // @ts-ignore - internal exception 132 | out.set({ 133 | x: e.clientX + document.body.scrollLeft + 134 | document.documentElement.scrollLeft, 135 | y: e.clientY + document.body.scrollTop + 136 | document.documentElement.scrollTop 137 | }) 138 | } else { 139 | throw new Error('Mouse event has no coordinates I recognise!') 140 | } 141 | }) 142 | 143 | return out 144 | } 145 | 146 | /** 147 | * A signal which yields the current time, as determined by `now`, on every animation frame. 148 | */ 149 | export const animationFrame = (): Signal
(initial: number, interval: number, values: A[]) => { 7 | const vals = values.slice() 8 | 9 | const out = constant(maybe(vals.shift())) 10 | 11 | if (vals.length) { 12 | setTimeout(function pop () { 13 | // @ts-ignore - test function 14 | out.set(maybe(vals.shift())) 15 | 16 | if (vals.length) { 17 | setTimeout(pop, interval) 18 | } 19 | }, initial) 20 | } 21 | 22 | return out 23 | } 24 | 25 | const getMaybeResults = (check: jest.Mock) => check.mock.calls 26 | .map(call => call[0].toNullable()) 27 | 28 | describe('Signal', () => { 29 | describe('constant()', () => { 30 | it('yields a single value', () => { 31 | const check = jest.fn() 32 | 33 | constant('lol').fmap(check) 34 | 35 | expect(check).toHaveBeenCalledWith('lol') 36 | }) 37 | 38 | it('merging two constants yields the first constant', () => { 39 | const check = jest.fn() 40 | 41 | constant('foo').merge(constant('bar')).fmap(check) 42 | 43 | expect(check).toHaveBeenCalledWith('foo') 44 | }) 45 | }) 46 | 47 | describe('map()', () => { 48 | it('maps a function over a Signal', async () => { 49 | const check = jest.fn() 50 | 51 | const double = (m: Maybe) => m.map(x => x * 2) 52 | const ticker = tick(1, 1, [1, 2, 3]) 53 | 54 | ticker.map(double) 55 | // @ts-ignore - test function 56 | .subscribe(check) 57 | 58 | await wait(50) 59 | 60 | expect(getMaybeResults(check)).toEqual([2, 4, 6]) 61 | }) 62 | }) 63 | 64 | describe('on()', () => { 65 | it('maps an effectful function over a signal', async () => { 66 | const check = jest.fn() 67 | 68 | const signalConverter = (m: Maybe<{state: number}>) => m.on(value => { 69 | value.state += 1 70 | }) 71 | const ticker = tick(1, 1, [{state: 1}, {state: 2}, {state: 3}]) 72 | 73 | ticker.on(signalConverter) 74 | // @ts-ignore - test function 75 | .subscribe(check) 76 | 77 | await wait(50) 78 | 79 | expect(getMaybeResults(check)).toEqual([{state: 2}, {state: 3}, {state: 4}]) 80 | }) 81 | }) 82 | 83 | describe('sampleOn()', () => { 84 | it('samples values from one Signal when another Signal changes', async () => { 85 | const check = jest.fn() 86 | 87 | const ticker = tick(10, 20, [1, 2, 3, 4, 5, 6]) 88 | 89 | every(40).sampleOn(ticker) 90 | // @ts-ignore - test function 91 | .subscribe(check) 92 | 93 | await wait(150) 94 | 95 | expect(getMaybeResults(check)).toEqual([1, 3, 5, 6]) 96 | }) 97 | }) 98 | 99 | describe('dropRepeats()', () => { 100 | it('only yields when value is != previous', async () => { 101 | const check = jest.fn() 102 | 103 | const ticker = tick(1, 1, [1, 1, 2, 2, 1, 3, 3]) 104 | 105 | ticker.dropRepeats() 106 | // @ts-ignore - test function 107 | .subscribe(check) 108 | 109 | await wait(50) 110 | 111 | expect(getMaybeResults(check)).toEqual([1, 2, 1, 3]) 112 | }) 113 | }) 114 | 115 | describe('foldp()', () => { 116 | it('can sum up values', async () => { 117 | const check = jest.fn() 118 | 119 | const ticker = tick(1, 1, [1, 2, 3, 4, 5]) 120 | 121 | ticker.foldp(m => b => m.fmap(a => b.map(c => a + c)), Just(0)) 122 | // @ts-ignore - test function 123 | .subscribe(check) 124 | 125 | await wait(50) 126 | 127 | expect(getMaybeResults(check)).toEqual([1, 3, 6, 10, 15]) 128 | }) 129 | }) 130 | 131 | describe('filter()', () => { 132 | it('filters values', async () => { 133 | const check = jest.fn() 134 | 135 | const ticker = tick(1, 1, [5, 3, 8, 4]) 136 | 137 | ticker.filter(m => m.map(n => n < 5).toBoolean(), Just(0)) 138 | // @ts-ignore - test function 139 | .subscribe(check) 140 | 141 | await wait(50) 142 | 143 | expect(getMaybeResults(check)).toEqual([0, 3, 4]) 144 | }) 145 | }) 146 | 147 | describe('flatten()', () => { 148 | it('flattens values', async () => { 149 | const check = jest.fn() 150 | 151 | const ticker = tick(10, 1, [[1, 2], [3, 4], [], [5, 6, 7]]).map(a => a.getOrThrow()) 152 | 153 | ticker.flatten(0) 154 | // @ts-ignore - test function 155 | .subscribe(check) 156 | 157 | await wait(50) 158 | 159 | const result = check.mock.calls.map(call => call[0]) 160 | 161 | expect(result).toEqual([1, 2, 3, 4, 5, 6, 7]) 162 | }) 163 | 164 | it('uses the seed when it doesn\'t have new values yet', async () => { 165 | const check = jest.fn() 166 | 167 | const ticker = tick(10, 1, [[], [1, 2], [3, 4], [], [5, 6, 7]]).map(a => a.getOrThrow()) 168 | 169 | ticker.flatten(0) 170 | // @ts-ignore - test function 171 | .subscribe(check) 172 | 173 | await wait(50) 174 | 175 | const result = check.mock.calls.map(call => call[0]) 176 | 177 | expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7]) 178 | }) 179 | }) 180 | 181 | describe('delayed()', () => { 182 | it('yields the same values', async () => { 183 | const check = jest.fn() 184 | 185 | const ticker = tick(1, 1, [1, 2, 3, 4, 5]) 186 | 187 | ticker.delay(40) 188 | // @ts-ignore - test function 189 | .subscribe(check) 190 | 191 | await wait(50) 192 | 193 | expect(getMaybeResults(check)).toEqual([1, 2, 3, 4, 5]) 194 | }) 195 | }) 196 | 197 | describe('since()', () => { 198 | it('yields true only once for multiple yields, then false', async () => { 199 | const check = jest.fn() 200 | 201 | const ticker = tick(1, 1, [1, 2, 3]) 202 | 203 | ticker.since(10) 204 | // @ts-ignore - test function 205 | .subscribe(check) 206 | 207 | await wait(25) 208 | 209 | const result = check.mock.calls.map(call => call[0]) 210 | 211 | expect(result).toEqual([false, true, false]) 212 | }) 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /src/__tests__/Maybe.test.ts: -------------------------------------------------------------------------------- 1 | import Maybe, {Just, Nothing, maybe} from '../Maybe' 2 | 3 | describe('Maybe', () => { 4 | describe('Just', () => { 5 | it("doesn't squash values that are intentionally undefined or null", () => { 6 | expect(Just(null).equals(Nothing())).toBe(false) 7 | 8 | // @ts-ignore 9 | expect(Just(null).option).toEqual([null]) 10 | // @ts-ignore 11 | expect(Just(undefined).option).toEqual([undefined]) 12 | }) 13 | }) 14 | 15 | describe('maybe()', () => { 16 | it('takes an optional that may be null or undefined and returns a Maybe', () => { 17 | expect(Just(2).getOr(1)).toEqual(2) 18 | expect(Nothing().getOr(1)).toEqual(1) 19 | expect(maybe(null).getOr(1)).toEqual(1) 20 | expect(maybe(undefined).getOr(1)).toEqual(1) 21 | }) 22 | 23 | it('does squash values that are undefined or null', () => { 24 | expect(maybe(null).equals(Nothing())).toBe(true) 25 | }) 26 | }) 27 | 28 | describe('getOr()', () => { 29 | it('returns the value if it is present', () => { 30 | const result = Just(2).getOr(1) 31 | 32 | expect(result).toEqual(2) 33 | }) 34 | 35 | it('returns the default if the value is not present', () => { 36 | const result = Nothing().getOr(1) 37 | 38 | expect(result).toEqual(1) 39 | }) 40 | 41 | it("shouldn't interpret falsey values as nullable", () => { 42 | expect(Just(0).getOr(1)).toEqual(0) 43 | expect(Just(false).getOr(true)).toEqual(false) 44 | }) 45 | }) 46 | 47 | describe('getOrThrow()', () => { 48 | it('throws an error if the value isn\'t present', () => { 49 | const result = () => Nothing().getOrThrow() 50 | 51 | expect(result).toThrowError('Maybe was Nothing') 52 | }) 53 | }) 54 | 55 | describe('getOrThrowMessage()', () => { 56 | it('uses the message', () => { 57 | const result = () => Nothing().getOrThrowMessage('test-error') 58 | 59 | expect(result).toThrowError('test-error') 60 | }) 61 | }) 62 | 63 | describe('map()', () => { 64 | it('maps a function over the current Maybe', () => { 65 | const square = (a: number) => a * a 66 | 67 | const squared = Just(6).map(square) 68 | 69 | expect(squared.toNullable()).toEqual(36) 70 | }) 71 | 72 | it('handles functions that map to undefined smoothly', () => { 73 | const undef = (_n: number) => undefined 74 | 75 | const result = Just(6).map(undef) 76 | 77 | expect(result.equals(Nothing())).toBeTruthy() 78 | }) 79 | }) 80 | 81 | describe('fmap()', () => { 82 | it('maps a function that returns a new Maybe over the current Maybe', () => { 83 | const squareIfEven = (a: number): Maybe => a % 2 === 0 ? Just(a * a) : Just(a) 84 | 85 | const squared = Just(6).fmap(squareIfEven) 86 | 87 | expect(squared.toNullable()).toEqual(36) 88 | 89 | const notSquared = Just(5).fmap(squareIfEven) 90 | 91 | expect(notSquared.toNullable()).toEqual(5) 92 | }) 93 | 94 | it('handles more cases', () => { 95 | const f = (n: number) => Just(n * 2) 96 | const g = () => Nothing() // tslint:disable-line no-unnecessary-callback-wrapper 97 | 98 | expect(Just(2).fmap(f).equals(Just(4))).toBe(true) 99 | expect(Just(2).fmap(g).equals(Nothing())).toBe(true) 100 | expect(Nothing().fmap(f).equals(Nothing())).toBe(true) 101 | }) 102 | }) 103 | 104 | describe('apply()', () => { 105 | it('unpacks a Maybe for a function from A to B into a function from Maybe A to Maybe B', () => { 106 | interface Tire { 107 | type: 'rubber' 108 | } 109 | 110 | interface Wheel { 111 | size: 'large' 112 | tire: Tire 113 | } 114 | 115 | interface Car { 116 | wheels: Wheel[] 117 | } 118 | 119 | const car: Car = { 120 | wheels: [{size: 'large', tire: {type: 'rubber'}}] 121 | } 122 | 123 | // Here's a contrived scenario: You have a function to get wheels 124 | const getWheels = (c: Car) => c.wheels 125 | 126 | // You have a function to get tires 127 | const getTire = (w: Wheel) => w.tire 128 | const getTires = (ws: Wheel[]) => ws.map(getTire) 129 | 130 | // You might have a car, you might not 131 | const maybeCar = Just(car) 132 | 133 | // Say you only want to fire "getTires" when there is a car present 134 | const maybeGetTires = maybeCar.when(getTires) 135 | 136 | // With `apply`, you can take your `maybeGetTires` function and apply it to the results 137 | const result = maybeCar.map(getWheels).apply(maybeGetTires) 138 | 139 | expect(result.toNullable()).toEqual([{type: 'rubber'}]) 140 | 141 | // This is functionally equivalent to the below, but way easier to work with! 142 | const altResult = maybeGetTires.fmap(func => maybeCar.map(c => func(getWheels(c)))) 143 | 144 | expect(altResult.toNullable()).toEqual([{type: 'rubber'}]) 145 | }) 146 | 147 | it('handles some more cases', () => { 148 | const f = (n: number) => n * 2 149 | 150 | expect(Just(2).apply(Just(f)).equals(Just(4))).toBe(true) 151 | expect(Nothing().apply(Just(f)).equals(Nothing())).toBe(true) 152 | expect(Just(2).apply(Nothing<(a: number) => number>()).equals(Nothing())).toBe(true) 153 | expect(Just(2).apply(Just(f)).equals(Just(4))).toBe(true) 154 | expect(maybe(2).apply(Just(f)).equals(Just(4))).toBe(true) 155 | expect(maybe(undefined).apply(Just(f)).equals(Nothing())).toBe(true) 156 | }) 157 | }) 158 | 159 | describe('prop()', () => { 160 | it('returns a Maybe for the value at the specified key of an object', () => { 161 | const m = Just({test: 123, variation: 'string'}).prop('test') 162 | 163 | expect(m.getOrThrow()).toEqual(123) 164 | }) 165 | 166 | it('chains correctly', () => { 167 | interface Elf { 168 | pointyEars: boolean 169 | } 170 | 171 | interface Shelf { 172 | id: number 173 | name?: string 174 | elf?: Elf 175 | } 176 | 177 | interface Cottage { 178 | id: number 179 | name?: string 180 | shelf?: Shelf 181 | } 182 | 183 | const cottage: Cottage = { 184 | id: 1 185 | } 186 | 187 | const maybeElf = Just(cottage).prop('shelf').prop('elf') 188 | 189 | const defaultElf = {pointyEars: false} 190 | 191 | const elf = maybeElf.getOr(defaultElf) 192 | 193 | expect(elf).toBe(defaultElf) 194 | }) 195 | }) 196 | 197 | describe('equals()', () => { 198 | it('checks equality', () => { 199 | expect(Nothing().equals(Nothing())).toBe(true) 200 | expect(Just(2).equals(Just(2))).toBe(true) 201 | }) 202 | 203 | it('checks inequality', () => { 204 | expect(Nothing().equals(Just(1))).toBe(false) 205 | expect(Just(1).equals(Nothing())).toBe(false) 206 | expect(Just(2).equals(Just(1))).toBe(false) 207 | expect(Just(1).equals(Just(2))).toBe(false) 208 | }) 209 | }) 210 | 211 | describe('alt()', () => { 212 | it('returns the first Just encountered ', () => { 213 | expect(Just(1).alt(Just(2)).equals(Just(1))).toBe(true) 214 | expect(Just(2).alt(Nothing()).equals(Just(2))).toBe(true) 215 | expect(Nothing().alt(Just(1)).equals(Just(1))).toBe(true) 216 | expect(Nothing().alt(Nothing()).equals(Nothing())).toBe(true) 217 | }) 218 | }) 219 | 220 | describe('extend()', () => { 221 | it('applies Maybe values correctly', () => { 222 | const f = (m: Maybe) => m.getOr(0) 223 | 224 | expect(Just(2).extend(f).equals(Just(2))).toBe(true) 225 | expect(Nothing().extend(f).equals(Nothing())).toBe(true) 226 | expect(maybe(2).extend(f).equals(Just(2))).toBe(true) 227 | }) 228 | }) 229 | 230 | describe('fold()', () => { 231 | it('applies a function to each case in the data structure', () => { 232 | const f = 'none' 233 | const g = (s: string) => `some${s.length}` 234 | 235 | expect(Nothing().fold(f, g)).toEqual('none') 236 | expect(Just('abc').fold(f, g)).toEqual('some3') 237 | }) 238 | }) 239 | }) 240 | -------------------------------------------------------------------------------- /src/signal/Signal.ts: -------------------------------------------------------------------------------- 1 | import {Monad, empty, eq} from '../Functional' 2 | 3 | export type Time = number & {_tag?: 'Time'} 4 | 5 | /** 6 | * Signal is a lightweight FRP-like Monad heavily inspired by the Elm Signal implementation. It 7 | * was ported from an original PureScript implementation created by 8 | * [Bodil Stokke](https://github.com/bodil/purescript-signal). 9 | * 10 | * This class provides tools to manipulate values that a Signal receives, allowing you to apply 11 | * transforms to each value sent to the Signal. To get started, however, you'll want to use the 12 | * [[Channel]] or the (upcoming) Loop classes to set up a new signal for usage. 13 | * 14 | * @typeparam A - The Type of value the Signal yields 15 | */ 16 | export default class Signal implements Monad { 17 | /** @ignore */ 18 | private value: A 19 | /** @ignore */ 20 | private readonly subscriptions: ((a: A) => void)[] = [] 21 | 22 | /** @ignore */ 23 | private constructor (value: A) { 24 | this.value = value 25 | } 26 | 27 | /** 28 | * Create a signal with a constant value. 29 | */ 30 | static constant = (value: A) => new Signal(value) 31 | 32 | /** 33 | * Given a Signal of effects with no return value, run each effect as it comes in. 34 | */ 35 | static run = (s: Signal) => s.subscribe(value => value()) 36 | 37 | /** 38 | * Takes a signal of effects of a, and produces an effect which returns a signal which will take 39 | * each effect produced by the input signal, run it, and yield its returned value. 40 | */ 41 | static unwrap = (s: Signal) => { 42 | const out = constant(s.get()()) 43 | 44 | s.subscribe(value => { 45 | out.set(value()) 46 | }) 47 | 48 | return out 49 | } 50 | 51 | /** 52 | * Creates a signal which yields the current time (according to now) every given number of 53 | * milliseconds. 54 | */ 55 | static every = (interval: number): Signal): Signal => { 104 | const out = constant(this.get()) 105 | 106 | s.subscribe(out.set) 107 | this.subscribe(out.set) 108 | 109 | return out 110 | } 111 | 112 | /** 113 | * Creates a past dependent signal. The function argument takes the value of the input signal, and 114 | * the previous value of the output signal, to produce the new value of the output signal. 115 | */ 116 | foldp = (func: (a: A) => (b: B) => B, seed: B): Signal => { 117 | let acc = seed 118 | 119 | const out = constant(acc) 120 | 121 | this.subscribe(value => { 122 | acc = func(value)(acc) 123 | 124 | out.set(acc) 125 | }) 126 | 127 | return out 128 | } 129 | 130 | /** 131 | * Creates a signal which yields the current value of the second signal every time the first 132 | * signal yields. 133 | */ 134 | sampleOn = (s: Signal): Signal => { 135 | const out = constant(s.get()) 136 | 137 | this.subscribe(() => { 138 | out.set(s.get()) 139 | }) 140 | 141 | return out 142 | } 143 | 144 | /** 145 | * Create a signal which only yields values which aren't equal to the previous value of the input 146 | * signal. 147 | */ 148 | dropRepeats = (): Signal => { 149 | let prev = this.get() 150 | 151 | const out = constant(prev) 152 | 153 | this.subscribe(next => { 154 | if (!eq(prev, next)) { 155 | prev = next 156 | 157 | out.set(prev) 158 | } 159 | }) 160 | 161 | return out 162 | } 163 | 164 | /** 165 | * Takes a signal and filters out yielded values for which the provided predicate function returns 166 | * false. 167 | */ 168 | filter = (func: (a: A) => boolean, a: A): Signal => { 169 | const out = constant(func(this.get()) ? this.get() : a) 170 | 171 | this.subscribe(value => { 172 | if (func(value)) { 173 | out.set(value) 174 | } 175 | }) 176 | 177 | return out 178 | } 179 | 180 | /** 181 | * Turn a signal of collections of items into a signal of each item inside each collection, 182 | * in order. 183 | */ 184 | flatten = (b: B): Signal => { 185 | let seed = b 186 | 187 | const value = this.get() 188 | 189 | if (Array.isArray(value)) { 190 | const none = empty() 191 | 192 | let first: B[] = value.slice() 193 | 194 | if (first.length > 0) { 195 | seed = first[0] 196 | } else { 197 | first = none 198 | } 199 | 200 | const out = constant(seed) 201 | 202 | const feed = (items: B[]) => { 203 | items.forEach(out.set) 204 | } 205 | 206 | setTimeout(() => { 207 | this.subscribe(val => { 208 | if (first === none) { 209 | feed(val as unknown as B[]) 210 | } else { 211 | feed(first.slice(1)) 212 | 213 | first = none 214 | } 215 | }) 216 | }, 0) 217 | 218 | return out 219 | } 220 | 221 | throw new Error('Cannot flatten a value that is not an array') 222 | } 223 | 224 | /** 225 | * Runs side effects over the values of a Signal. 226 | */ 227 | on = (func: (a: A) => void): Signal => { 228 | this.subscribe(value => { 229 | func(value) 230 | }) 231 | 232 | return this 233 | } 234 | 235 | /** 236 | * Takes a signal and delays its yielded values by a given number of milliseconds. 237 | */ 238 | delay = (time: number) => { 239 | const out = constant(this.get()) 240 | 241 | let first = true 242 | 243 | this.subscribe(value => { 244 | if (first) { 245 | first = false 246 | } else { 247 | setTimeout(() => { 248 | out.set(value) 249 | }, time) 250 | } 251 | }) 252 | 253 | return out 254 | } 255 | 256 | /** 257 | * Takes a signal and a time value, and creates a signal which yields True when the input signal 258 | * yields, then goes back to False after the given number of milliseconds have elapsed, unless the 259 | * input signal yields again in the interim. 260 | */ 261 | since = (time: number) => { 262 | const out = constant(false) 263 | 264 | let first = true 265 | let timer: NodeJS.Timer | undefined 266 | 267 | const tick = () => { 268 | out.set(false) 269 | 270 | timer = undefined 271 | } 272 | 273 | this.subscribe(() => { 274 | if (first) { 275 | first = false 276 | 277 | return 278 | } 279 | 280 | if (timer === undefined) { 281 | out.set(true) 282 | 283 | timer = setTimeout(tick, time) 284 | } else { 285 | clearTimeout(timer) 286 | 287 | timer = setTimeout(tick, time) 288 | } 289 | }) 290 | 291 | return out 292 | } 293 | 294 | /** @ignore */ 295 | private readonly get = () => this.value 296 | 297 | /** @ignore */ 298 | private readonly set = (value: A) => { 299 | this.value = value 300 | 301 | for (const sub of this.subscriptions) { 302 | sub(value) 303 | } 304 | } 305 | 306 | /** @ignore */ 307 | private readonly subscribe = (sub: (a: A) => void) => { 308 | this.subscriptions.push(sub) 309 | 310 | sub(this.get()) 311 | } 312 | } 313 | 314 | export const constant = Signal.constant 315 | 316 | export const run = Signal.run 317 | 318 | export const unwrap = Signal.unwrap 319 | 320 | export const every = Signal.every 321 | -------------------------------------------------------------------------------- /src/Maybe.ts: -------------------------------------------------------------------------------- 1 | import {Eq, Extend, Nil, Monoid, Monad, empty, eq, exists} from './Functional' 2 | 3 | /** 4 | * A Maybe represents an optional value (A) with a convenient chaining syntax and strong type 5 | * inference. To create one, use the top-level constructors [[Just]], or [[Nothing]]. 6 | * 7 | * ```ts 8 | * import {Just, Maybe, Nothing, exists} from 'monadism' 9 | * 10 | * import Cache from '@my/app/Cache' 11 | * 12 | * const getUser = (userId: string): Maybe => { 13 | * const val = Cache.get(userId) 14 | * 15 | * return exists(val) ? Just(val) : Nothing() 16 | * } 17 | * ``` 18 | * 19 | * Or, use the [[maybe]] convenience function: 20 | * 21 | * ```ts 22 | * import {maybe} from 'monadism' 23 | * 24 | * import {User} from '@my/app/Types' 25 | * 26 | * const getUser = (userId: string): Maybe => maybe(Cache.get(userId)) 27 | * ``` 28 | * 29 | * @typeparam A - The type of optional value the Maybe represents. 30 | */ 31 | export default class Maybe implements Eq>, Monoid, Monad, Extend { 32 | empty = empty() 33 | 34 | /** 35 | * The internal value of the Maybe 36 | * @ignore 37 | */ 38 | private readonly option: [A?] 39 | 40 | /** 41 | * Not intended to be used directly. Use `Just`, `Nothing`, or `maybe` exported at the 42 | * top of the file. 43 | * @ignore 44 | */ 45 | private constructor (value: A) { 46 | this.option = [value] 47 | } 48 | 49 | /** 50 | * Wrap a value in a Maybe. 51 | */ 52 | static Just = (value: A) => new Maybe(value) 53 | 54 | /** 55 | * Return an empty Maybe. 56 | */ 57 | static Nothing = () => new Maybe(empty()) 58 | 59 | /** 60 | * Create a Maybe from a nullable value. 61 | * 62 | * ```ts 63 | * maybe(2).getOr(1) // 2 64 | * maybe(undefined).getOr(1) // 1 65 | * ``` 66 | */ 67 | static maybe = (value: A | Nil): Maybe> => 68 | exists(value) ? Just(value!) : Nothing() 69 | 70 | /** 71 | * An alias for `maybe`. 72 | */ 73 | // tslint:disable-next-line - member-ordering 74 | static fromNullable = Maybe.maybe 75 | 76 | /** 77 | * Return `true` or `false` depending on whether there is Just something or Nothing. 78 | */ 79 | isNothing = () => this.option[0] === this.empty 80 | 81 | /** 82 | * Use of this function should be discouraged. Use one of the stronger methods below in most 83 | * cases. 84 | */ 85 | toNullable = (): A | undefined => this.isNothing() ? undefined : this.option[0] 86 | 87 | /** 88 | * If the value is Just something, return the Boolean value of it. Otherwise, return false. 89 | */ 90 | toBoolean = () => this.isNothing() ? false : Boolean(this.toNullable()) 91 | 92 | /** 93 | * If the value of the current Maybe is Nothing, return a default value instaed. 94 | * 95 | * ```ts 96 | * Just(2).getOr(1) // 2 97 | * Nothing().getOr(1) // 1 98 | * ``` 99 | */ 100 | getOr = (def: A): A => this.isNothing() ? def : this.option[0]! 101 | 102 | /** 103 | * If the value of the current Maybe is Nothing, throw the given error message. 104 | * WARNING: Unsafe - could throw an exception. 105 | * 106 | * ```ts 107 | * Nothing().getOrThrowMessage("It didn't work!") // Error: It didn't work! 108 | * ``` 109 | */ 110 | getOrThrowMessage = (message: string) => { 111 | if (!this.isNothing()) { 112 | return this.option[0] as A 113 | } 114 | 115 | throw new Error(message) 116 | } 117 | 118 | /** 119 | * If the value of the current Maybe is Nothing, throw a default error message. 120 | * WARNING: Unsafe - could throw an exception. 121 | * 122 | * ```ts 123 | * Nothing().getOrThrow() // Error: Maybe was Nothing 124 | * ``` 125 | */ 126 | getOrThrow = () => this.getOrThrowMessage('Maybe was Nothing') 127 | 128 | /** 129 | * Our name for `flatMap`. Allows sequencing of Maybe values and functions that return a Maybe. 130 | * 131 | * For example, if you have a Maybe for a display name you might want to do something with it, 132 | * like split a display name into first and last names: 133 | * 134 | * ```ts 135 | * const squareIfEven = (a: number): Maybe => a % 2 === 0 ? Just(a * a) : Just(a) 136 | * 137 | * Just(6).fmap(squareIfEven) // Just(36) 138 | * ``` 139 | * 140 | * The `fmap` method allows you to transform the value with a function that accepts it and returns 141 | * another Maybe. 142 | * 143 | * @typeparam B - The type of the resulting value. 144 | */ 145 | fmap = (func: (value: A) => Maybe): Maybe => !this.isNothing() 146 | ? func(this.option[0]!) 147 | : Nothing() 148 | 149 | /** 150 | * Allows sequencing of Maybe values and functions that accept a Maybe and return a non-Maybe 151 | * value. 152 | * 153 | * ```ts 154 | * const f = (m: Maybe) => m.getOr(0) 155 | * 156 | * Just(2).extend(f) // Just(2) 157 | * Nothing().extend(f) // Nothing() 158 | * ``` 159 | */ 160 | extend = (func: (value: Maybe) => B): Maybe => !this.isNothing() 161 | ? new Maybe(func(this)) 162 | : Nothing() 163 | 164 | /** 165 | * Take a function that maps one type to another and lift it to work with Maybes. 166 | * 167 | * ```ts 168 | * const square = (a: number) => a * a 169 | * 170 | * Just(6).map(square) // Just(36) 171 | * ``` 172 | * 173 | * @typeparam B - The type of the resulting value. 174 | */ 175 | map = (func: (value: A) => B): Maybe => this.fmap(val => { 176 | // Work around a type issue with maybe() and NonNullable 177 | const r = func(val) 178 | 179 | return r ? Just(r) : Nothing() 180 | }) 181 | 182 | /** 183 | * Replaces the return value with what is provided if there is Nothing. 184 | */ 185 | instead = (def: A): Maybe => Just(this.getOr(def)) 186 | 187 | /** 188 | * Replaces the return value with what is provided if there is Just something, discarding the 189 | * original value. 190 | */ 191 | when = (b: B): Maybe => this.map(_ => b) 192 | 193 | /** 194 | * Run a side effect if the value is Just something, as opposed to Nothing. 195 | * 196 | * For example, if we just want to `console.log()` our Maybe with we can use the `on` function 197 | * to apply a function to Just values, ignoring the result: 198 | * 199 | * ```ts 200 | * name.on(val => { 201 | * console.log('Display name:', val.join(' ')) 202 | * }) 203 | * ``` 204 | */ 205 | on = (callback: (value: A) => void) => this.map(val => { 206 | callback(val) 207 | 208 | return val 209 | }) 210 | 211 | /** 212 | * Run a side effect if the value is Nothing. 213 | */ 214 | unless = (callback: () => void): Maybe => { 215 | if (this.isNothing()) { 216 | callback() 217 | } 218 | 219 | return this 220 | } 221 | 222 | /** 223 | * Unpacks a Maybe for a function from `A` to `B` into a function from `Maybe` to `Maybe`. 224 | * Allows functions contained within a Just to transform a value contained within a Just. 225 | * 226 | * ```ts 227 | * const f = (n: number) => n * 2 228 | * 229 | * Just(2).apply(Just(f)) // Just(4) 230 | * ``` 231 | */ 232 | apply = (m: Maybe<(value: A) => B>): Maybe => this.fmap(val => m.map(func => func(val))) 233 | 234 | /** 235 | * Returns a Maybe for the value at the given key. 236 | */ 237 | prop =

(key: P): Maybe> => this.fmap( 238 | val => (val && key in val) ? maybe(val[key]) : Nothing() 239 | ) 240 | 241 | /** 242 | * If the value is Nothing, returns false. If the value is Just something, returns the boolean 243 | * value of something. 244 | * 245 | * ```ts 246 | * Just(2).equals(Just(2)) // true 247 | * ``` 248 | */ 249 | equals = (m: Maybe): boolean => { 250 | const a = this.option[0] 251 | const b = m.option[0] 252 | 253 | return !a ? !b : !b ? false : eq(a, b) 254 | } 255 | 256 | /** 257 | * The first Maybe that is Just something is returned, otherwise Nothing is returned. 258 | * 259 | * ```ts 260 | * Just(1).alt(Just(2)) // Just(1) 261 | * Just(2).alt(Nothing()) // Just(2) 262 | * ``` 263 | */ 264 | alt = (m: Maybe): Maybe => { 265 | if (!this.isNothing()) { 266 | return Just(this.option[0] as A) 267 | } 268 | 269 | if (!m.isNothing()) { 270 | return Just(m.option[0] as A) 271 | } 272 | 273 | return Nothing() 274 | } 275 | 276 | /** 277 | * Apply a function to each case in the data structure. 278 | * 279 | * ```ts 280 | * const f = 'none' 281 | * const g = (s: string) => `some${s.length}` 282 | * 283 | * Nothing().fold(f, g)) // 'none' 284 | * Just('abc').fold(f, g)) // 'some3 285 | * ``` 286 | */ 287 | fold = (b: B, func: (value: A) => B): B => this.map(func).getOr(b) 288 | } 289 | 290 | export const Just = Maybe.Just 291 | 292 | export const Nothing = Maybe.Nothing 293 | 294 | export const maybe = Maybe.maybe 295 | -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Monadism 7 | 8 | 9 | 10 | 11 | 12 |

13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 59 |

Monadism

60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |

Index

68 |
69 |
70 |
71 |

External modules

72 | 83 |
84 |
85 |
86 |
87 |
88 | 128 |
129 |
130 |
131 |
132 |

Legend

133 |
134 |
    135 |
  • Module
  • 136 |
  • Object literal
  • 137 |
  • Variable
  • 138 |
  • Function
  • 139 |
  • Function with type parameter
  • 140 |
  • Index signature
  • 141 |
  • Type alias
  • 142 |
143 |
    144 |
  • Enumeration
  • 145 |
  • Enumeration member
  • 146 |
  • Property
  • 147 |
  • Method
  • 148 |
149 |
    150 |
  • Interface
  • 151 |
  • Interface with type parameter
  • 152 |
  • Constructor
  • 153 |
  • Property
  • 154 |
  • Method
  • 155 |
  • Index signature
  • 156 |
157 |
    158 |
  • Class
  • 159 |
  • Class with type parameter
  • 160 |
  • Constructor
  • 161 |
  • Property
  • 162 |
  • Method
  • 163 |
  • Accessor
  • 164 |
  • Index signature
  • 165 |
166 |
    167 |
  • Inherited constructor
  • 168 |
  • Inherited property
  • 169 |
  • Inherited method
  • 170 |
  • Inherited accessor
  • 171 |
172 |
    173 |
  • Protected property
  • 174 |
  • Protected method
  • 175 |
  • Protected accessor
  • 176 |
177 |
    178 |
  • Private property
  • 179 |
  • Private method
  • 180 |
  • Private accessor
  • 181 |
182 |
    183 |
  • Static property
  • 184 |
  • Static method
  • 185 |
186 |
187 |
188 |
189 |
190 |

Generated using TypeDoc

191 |
192 |
193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /docs/modules/_signal_signalchannel_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "signal/SignalChannel" | Monadism 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "signal/SignalChannel"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Variables

81 | 84 |
85 |
86 |
87 |
88 |
89 |

Variables

90 |
91 | 92 |

Const channel

93 |
channel: channel = Channel.channel
94 | 99 |
100 |
101 |
102 | 148 |
149 |
150 |
151 |
152 |

Legend

153 |
154 |
    155 |
  • Module
  • 156 |
  • Object literal
  • 157 |
  • Variable
  • 158 |
  • Function
  • 159 |
  • Function with type parameter
  • 160 |
  • Index signature
  • 161 |
  • Type alias
  • 162 |
163 |
    164 |
  • Enumeration
  • 165 |
  • Enumeration member
  • 166 |
  • Property
  • 167 |
  • Method
  • 168 |
169 |
    170 |
  • Interface
  • 171 |
  • Interface with type parameter
  • 172 |
  • Constructor
  • 173 |
  • Property
  • 174 |
  • Method
  • 175 |
  • Index signature
  • 176 |
177 |
    178 |
  • Class
  • 179 |
  • Class with type parameter
  • 180 |
  • Constructor
  • 181 |
  • Property
  • 182 |
  • Method
  • 183 |
  • Accessor
  • 184 |
  • Index signature
  • 185 |
186 |
    187 |
  • Inherited constructor
  • 188 |
  • Inherited property
  • 189 |
  • Inherited method
  • 190 |
  • Inherited accessor
  • 191 |
192 |
    193 |
  • Protected property
  • 194 |
  • Protected method
  • 195 |
  • Protected accessor
  • 196 |
197 |
    198 |
  • Private property
  • 199 |
  • Private method
  • 200 |
  • Private accessor
  • 201 |
202 |
    203 |
  • Static property
  • 204 |
  • Static method
  • 205 |
206 |
207 |
208 |
209 |
210 |

Generated using TypeDoc

211 |
212 |
213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /docs/modules/_reader_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "Reader" | Monadism 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "Reader"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Variables

81 | 85 |
86 |
87 |
88 |
89 |
90 |

Variables

91 |
92 | 93 |

Const ask

94 |
ask: ask = Reader.ask
95 | 100 |
101 |
102 | 103 |

Const asks

104 |
asks: asks = Reader.asks
105 | 110 |
111 |
112 |
113 | 162 |
163 |
164 |
165 |
166 |

Legend

167 |
168 |
    169 |
  • Module
  • 170 |
  • Object literal
  • 171 |
  • Variable
  • 172 |
  • Function
  • 173 |
  • Function with type parameter
  • 174 |
  • Index signature
  • 175 |
  • Type alias
  • 176 |
177 |
    178 |
  • Enumeration
  • 179 |
  • Enumeration member
  • 180 |
  • Property
  • 181 |
  • Method
  • 182 |
183 |
    184 |
  • Interface
  • 185 |
  • Interface with type parameter
  • 186 |
  • Constructor
  • 187 |
  • Property
  • 188 |
  • Method
  • 189 |
  • Index signature
  • 190 |
191 |
    192 |
  • Class
  • 193 |
  • Class with type parameter
  • 194 |
  • Constructor
  • 195 |
  • Property
  • 196 |
  • Method
  • 197 |
  • Accessor
  • 198 |
  • Index signature
  • 199 |
200 |
    201 |
  • Inherited constructor
  • 202 |
  • Inherited property
  • 203 |
  • Inherited method
  • 204 |
  • Inherited accessor
  • 205 |
206 |
    207 |
  • Protected property
  • 208 |
  • Protected method
  • 209 |
  • Protected accessor
  • 210 |
211 |
    212 |
  • Private property
  • 213 |
  • Private method
  • 214 |
  • Private accessor
  • 215 |
216 |
    217 |
  • Static property
  • 218 |
  • Static method
  • 219 |
220 |
221 |
222 |
223 |
224 |

Generated using TypeDoc

225 |
226 |
227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /docs/enums/_either_.eithertype.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EitherType | Monadism 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 65 |

Enumeration EitherType

66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |

Index

74 |
75 |
76 |
77 |

Enumeration members

78 | 82 |
83 |
84 |
85 |
86 |
87 |

Enumeration members

88 |
89 | 90 |

Left

91 |
Left:
92 | 97 |
98 |
99 | 100 |

Right

101 |
Right:
102 | 107 |
108 |
109 |
110 | 177 |
178 |
179 |
180 |
181 |

Legend

182 |
183 |
    184 |
  • Module
  • 185 |
  • Object literal
  • 186 |
  • Variable
  • 187 |
  • Function
  • 188 |
  • Function with type parameter
  • 189 |
  • Index signature
  • 190 |
  • Type alias
  • 191 |
192 |
    193 |
  • Enumeration
  • 194 |
  • Enumeration member
  • 195 |
  • Property
  • 196 |
  • Method
  • 197 |
198 |
    199 |
  • Interface
  • 200 |
  • Interface with type parameter
  • 201 |
  • Constructor
  • 202 |
  • Property
  • 203 |
  • Method
  • 204 |
  • Index signature
  • 205 |
206 |
    207 |
  • Class
  • 208 |
  • Class with type parameter
  • 209 |
  • Constructor
  • 210 |
  • Property
  • 211 |
  • Method
  • 212 |
  • Accessor
  • 213 |
  • Index signature
  • 214 |
215 |
    216 |
  • Inherited constructor
  • 217 |
  • Inherited property
  • 218 |
  • Inherited method
  • 219 |
  • Inherited accessor
  • 220 |
221 |
    222 |
  • Protected property
  • 223 |
  • Protected method
  • 224 |
  • Protected accessor
  • 225 |
226 |
    227 |
  • Private property
  • 228 |
  • Private method
  • 229 |
  • Private accessor
  • 230 |
231 |
    232 |
  • Static property
  • 233 |
  • Static method
  • 234 |
235 |
236 |
237 |
238 |
239 |

Generated using TypeDoc

240 |
241 |
242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /docs/modules/_maybe_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "Maybe" | Monadism 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "Maybe"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Variables

81 | 86 |
87 |
88 |
89 |
90 |
91 |

Variables

92 |
93 | 94 |

Const Just

95 |
Just: Just = Maybe.Just
96 | 101 |
102 |
103 | 104 |

Const Nothing

105 |
Nothing: Nothing = Maybe.Nothing
106 | 111 |
112 |
113 | 114 |

Const maybe

115 |
maybe: maybe = Maybe.maybe
116 | 121 |
122 |
123 |
124 | 176 |
177 |
178 |
179 |
180 |

Legend

181 |
182 |
    183 |
  • Module
  • 184 |
  • Object literal
  • 185 |
  • Variable
  • 186 |
  • Function
  • 187 |
  • Function with type parameter
  • 188 |
  • Index signature
  • 189 |
  • Type alias
  • 190 |
191 |
    192 |
  • Enumeration
  • 193 |
  • Enumeration member
  • 194 |
  • Property
  • 195 |
  • Method
  • 196 |
197 |
    198 |
  • Interface
  • 199 |
  • Interface with type parameter
  • 200 |
  • Constructor
  • 201 |
  • Property
  • 202 |
  • Method
  • 203 |
  • Index signature
  • 204 |
205 |
    206 |
  • Class
  • 207 |
  • Class with type parameter
  • 208 |
  • Constructor
  • 209 |
  • Property
  • 210 |
  • Method
  • 211 |
  • Accessor
  • 212 |
  • Index signature
  • 213 |
214 |
    215 |
  • Inherited constructor
  • 216 |
  • Inherited property
  • 217 |
  • Inherited method
  • 218 |
  • Inherited accessor
  • 219 |
220 |
    221 |
  • Protected property
  • 222 |
  • Protected method
  • 223 |
  • Protected accessor
  • 224 |
225 |
    226 |
  • Private property
  • 227 |
  • Private method
  • 228 |
  • Private accessor
  • 229 |
230 |
    231 |
  • Static property
  • 232 |
  • Static method
  • 233 |
234 |
235 |
236 |
237 |
238 |

Generated using TypeDoc

239 |
240 |
241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /docs/modules/_writer_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "Writer" | Monadism 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "Writer"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |

Variables

81 | 86 |
87 |
88 |
89 |
90 |
91 |

Variables

92 |
93 | 94 |

Const tell

95 |
tell: tell = Writer.tell
96 | 101 |
102 |
103 | 104 |

Const unit

105 |
unit: unit = Writer.unit
106 | 111 |
112 |
113 | 114 |

Const writer

115 |
writer: writer = Writer.writer
116 | 121 |
122 |
123 |
124 | 176 |
177 |
178 |
179 |
180 |

Legend

181 |
182 |
    183 |
  • Module
  • 184 |
  • Object literal
  • 185 |
  • Variable
  • 186 |
  • Function
  • 187 |
  • Function with type parameter
  • 188 |
  • Index signature
  • 189 |
  • Type alias
  • 190 |
191 |
    192 |
  • Enumeration
  • 193 |
  • Enumeration member
  • 194 |
  • Property
  • 195 |
  • Method
  • 196 |
197 |
    198 |
  • Interface
  • 199 |
  • Interface with type parameter
  • 200 |
  • Constructor
  • 201 |
  • Property
  • 202 |
  • Method
  • 203 |
  • Index signature
  • 204 |
205 |
    206 |
  • Class
  • 207 |
  • Class with type parameter
  • 208 |
  • Constructor
  • 209 |
  • Property
  • 210 |
  • Method
  • 211 |
  • Accessor
  • 212 |
  • Index signature
  • 213 |
214 |
    215 |
  • Inherited constructor
  • 216 |
  • Inherited property
  • 217 |
  • Inherited method
  • 218 |
  • Inherited accessor
  • 219 |
220 |
    221 |
  • Protected property
  • 222 |
  • Protected method
  • 223 |
  • Protected accessor
  • 224 |
225 |
    226 |
  • Private property
  • 227 |
  • Private method
  • 228 |
  • Private accessor
  • 229 |
230 |
    231 |
  • Static property
  • 232 |
  • Static method
  • 233 |
234 |
235 |
236 |
237 |
238 |

Generated using TypeDoc

239 |
240 |
241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /examples/signal/mario/lib/Main.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Mario=e():t.Mario=e()}(window,function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="2Pgx")}({"2Pgx":function(t,e,n){"use strict";(function(t){Object.defineProperty(e,"__esModule",{value:!0}),e.main=void 0;var r,o=(r=n("Sgzc"))&&r.__esModule?r:{default:r},i=n("RJlM"),u=n("PIvJ"),c=n("lJmG");function a(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}var s=37,f=39,l=32,d=function(t){return function(e){return function(t){for(var e=1;e1)for(var n=1;n0?e=c[0]:c=i;var a=u(e),s=function(t){t.forEach(a.set)};return setTimeout(function(){n.subscribe(function(t){c===i?s(t):(s(c.slice(1)),c=i)})},0),a}throw new Error("Cannot flatten a value that is not an array")}),o(this,"on",function(t){return n.subscribe(function(e){t(e)}),n}),o(this,"delay",function(t){var e=u(n.get()),r=!0;return n.subscribe(function(n){r?r=!1:setTimeout(function(){e.set(n)},t)}),e}),o(this,"since",function(t){var e,r=u(!1),o=!0,i=function(){r.set(!1),e=void 0};return n.subscribe(function(){o?o=!1:void 0===e?(r.set(!0),e=setTimeout(i,t)):(clearTimeout(e),e=setTimeout(i,t))}),r}),o(this,"get",function(){return n.value}),o(this,"set",function(t){n.value=t;var e=!0,r=!1,o=void 0;try{for(var i,u=n.subscriptions[Symbol.iterator]();!(e=(i=u.next()).done);e=!0){(0,i.value)(t)}}catch(t){r=!0,o=t}finally{try{e||null==u.return||u.return()}finally{if(r)throw o}}}),o(this,"subscribe",function(t){n.subscriptions.push(t),t(n.get())}),this.value=e};e.default=i,o(i,"constant",function(t){return new i(t)}),o(i,"run",function(t){return t.subscribe(function(t){return t()})}),o(i,"unwrap",function(t){var e=u(t.get()());return t.subscribe(function(t){e.set(t())}),e}),o(i,"every",function(t){var e=u(Date.now());return setInterval(function(){e.set(Date.now())},t),e});var u=i.constant;e.constant=u;var c=i.run;e.run=c;var a=i.unwrap;e.unwrap=a;var s=i.every;e.every=s},YuTi:function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},bQgK:function(t,e,n){(function(e){(function(){var n,r,o,i,u,c;"undefined"!=typeof performance&&null!==performance&&performance.now?t.exports=function(){return performance.now()}:null!=e&&e.hrtime?(t.exports=function(){return(n()-u)/1e6},r=e.hrtime,i=(n=function(){var t;return 1e9*(t=r())[0]+t[1]})(),c=1e9*e.uptime(),u=i-c):Date.now?(t.exports=function(){return Date.now()-o},o=Date.now()):(t.exports=function(){return(new Date).getTime()-o},o=(new Date).getTime())}).call(this)}).call(this,n("8oxB"))},kMv7:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.eq=function t(e,n){if(Array.isArray(e)&&Array.isArray(n))return e.length>0&&e.length===n.length&&e.map(function(e,r){return t(e,n[r])}).reduce(function(t,e){return t&&Boolean(e)});if(e&&e.equals)return e.equals(n);return e===n},e.compose=e.identity=e.exists=e.empty=void 0;var r=Symbol("MONADISM:EMPTY");e.empty=function(){return r};e.exists=function(t){return null!=t};e.identity=function(t){return t};e.compose=function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r0};e.charSpriteDescriptor=function(t){return"character ".concat(function(t){return a(t)?r.Jumping:function(t){return 0===t.dx}(t)?r.Jumping:r.Walking}(t)," ").concat(t.dir)};var s=function(t){return a(t)?.04:.06},f=function(t){return a(t)?.02:.1},l=function(t){return u({},t,{x:t.x+t.dx,y:t.y+t.dy})},d=function(t){return t.y<=-t.dy?u({},t,{y:0,dy:0}):u({},t,{dy:t.dy-.15})},p=function(t,e){return function(n){return t&&!e?u({},n,{dx:Math.max(-2.5,n.dx-s(n)),dir:o.Left}):e&&!t?u({},n,{dx:Math.min(2.5,n.dx+s(n)),dir:o.Right}):function(t){return 0===t.dx?t:Math.abs(t.dx)<=f(t)?u({},t,{dx:0}):t.dx>0?u({},t,{dx:t.dx-f(t)}):u({},t,{dx:t.dx+f(t)})}(n)}};e.marioLogic=function(t){return(0,i.compose)(l,d,p(t.left,t.right),(e=t.jump,function(t){return e&&!a(t)?u({},t,{dy:4+.8*Math.abs(t.dx)}):!e&&a(t)&&t.dy>0?u({},t,{dy:t.dy-.15}):t}));var e}},xEkU:function(t,e,n){(function(e){for(var r=n("bQgK"),o="undefined"==typeof window?e:window,i=["moz","webkit"],u="AnimationFrame",c=o["request"+u],a=o["cancel"+u]||o["cancelRequest"+u],s=0;!c&&s1)for(var n=1;n0?e=c[0]:c=i;var a=u(e),s=function(t){t.forEach(a.set)};return setTimeout(function(){n.subscribe(function(t){c===i?s(t):(s(c.slice(1)),c=i)})},0),a}throw new Error("Cannot flatten a value that is not an array")}),o(this,"on",function(t){return n.subscribe(function(e){t(e)}),n}),o(this,"delay",function(t){var e=u(n.get()),r=!0;return n.subscribe(function(n){r?r=!1:setTimeout(function(){e.set(n)},t)}),e}),o(this,"since",function(t){var e,r=u(!1),o=!0,i=function(){r.set(!1),e=void 0};return n.subscribe(function(){o?o=!1:void 0===e?(r.set(!0),e=setTimeout(i,t)):(clearTimeout(e),e=setTimeout(i,t))}),r}),o(this,"get",function(){return n.value}),o(this,"set",function(t){n.value=t;var e=!0,r=!1,o=void 0;try{for(var i,u=n.subscriptions[Symbol.iterator]();!(e=(i=u.next()).done);e=!0){(0,i.value)(t)}}catch(t){r=!0,o=t}finally{try{e||null==u.return||u.return()}finally{if(r)throw o}}}),o(this,"subscribe",function(t){n.subscriptions.push(t),t(n.get())}),this.value=e};e.default=i,o(i,"constant",function(t){return new i(t)}),o(i,"run",function(t){return t.subscribe(function(t){return t()})}),o(i,"unwrap",function(t){var e=u(t.get()());return t.subscribe(function(t){e.set(t())}),e}),o(i,"every",function(t){var e=u(Date.now());return setInterval(function(){e.set(Date.now())},t),e});var u=i.constant;e.constant=u;var c=i.run;e.run=c;var a=i.unwrap;e.unwrap=a;var s=i.every;e.every=s},YuTi:function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},bQgK:function(t,e,n){(function(e){(function(){var n,r,o,i,u,c;"undefined"!=typeof performance&&null!==performance&&performance.now?t.exports=function(){return performance.now()}:null!=e&&e.hrtime?(t.exports=function(){return(n()-u)/1e6},r=e.hrtime,i=(n=function(){var t;return 1e9*(t=r())[0]+t[1]})(),c=1e9*e.uptime(),u=i-c):Date.now?(t.exports=function(){return Date.now()-o},o=Date.now()):(t.exports=function(){return(new Date).getTime()-o},o=(new Date).getTime())}).call(this)}).call(this,n("8oxB"))},kMv7:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.eq=function t(e,n){if(Array.isArray(e)&&Array.isArray(n))return e.length>0&&e.length===n.length&&e.map(function(e,r){return t(e,n[r])}).reduce(function(t,e){return t&&Boolean(e)});if(e&&e.equals)return e.equals(n);return e===n},e.compose=e.identity=e.exists=e.empty=void 0;var r=Symbol("MONADISM:EMPTY");e.empty=function(){return r};e.exists=function(t){return null!=t};e.identity=function(t){return t};e.compose=function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r0};e.charSpriteDescriptor=function(t){return"character ".concat(function(t){return a(t)?r.Jumping:function(t){return 0===t.dx}(t)?r.Jumping:r.Walking}(t)," ").concat(t.dir)};var s=function(t){return a(t)?.04:.06},f=function(t){return a(t)?.02:.1},l=function(t){return u({},t,{x:t.x+t.dx,y:t.y+t.dy})},d=function(t){return t.y<=-t.dy?u({},t,{y:0,dy:0}):u({},t,{dy:t.dy-.15})},p=function(t,e){return function(n){return t&&!e?u({},n,{dx:Math.max(-2.5,n.dx-s(n)),dir:o.Left}):e&&!t?u({},n,{dx:Math.min(2.5,n.dx+s(n)),dir:o.Right}):function(t){return 0===t.dx?t:Math.abs(t.dx)<=f(t)?u({},t,{dx:0}):t.dx>0?u({},t,{dx:t.dx-f(t)}):u({},t,{dx:t.dx+f(t)})}(n)}};e.marioLogic=function(t){return(0,i.compose)(l,d,p(t.left,t.right),(e=t.jump,function(t){return e&&!a(t)?u({},t,{dy:4+.8*Math.abs(t.dx)}):!e&&a(t)&&t.dy>0?u({},t,{dy:t.dy-.15}):t}));var e}},xEkU:function(t,e,n){(function(e){for(var r=n("bQgK"),o="undefined"==typeof window?e:window,i=["moz","webkit"],u="AnimationFrame",c=o["request"+u],a=o["cancel"+u]||o["cancelRequest"+u],s=0;!c&&s