1
Fork 0

Add typescript/option

This commit is contained in:
prescientmoon 2024-03-04 15:04:03 +01:00
commit 41d4b10b29
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
68 changed files with 5899 additions and 0 deletions

View file

@ -3,4 +3,5 @@
| Name | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
| [lunardash](./lunardash/) | Rhythm game I dropped super early into development |
| [option](./option/) | Typescript implementation of the `Maybe` monad |
| [wave38](./wave38/) | Remake of [wave37](https://github.com/Mateiadrielrafael/wave37) I dropped super early into development. |

View file

@ -0,0 +1,25 @@
name: Release
on:
push:
branches:
- master
jobs:
release:
name: release
runs-on: ubuntu-latest
steps:
# check out repository code and setup node
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: '12.x'
# install dependencies and run semantic-release
- run: npm i -g pnpm
- run: pnpm install
- run: pnpm test
- run: pnpm run build
- run: pnpx semantic-release
env:
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

4
typescript/option/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
dist
sandbox
lib

View file

@ -0,0 +1,5 @@
{
"semi": false,
"tabWidth": 4,
"singleQuote": true
}

View file

@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true
}

39
typescript/option/LICENSE Normal file
View file

@ -0,0 +1,39 @@
The Prosperity Public License 2.0.0
Contributor: Matei Adriel
Source Code: https://github.com/Mateiadrielrafael/ecs
This license lets you use and share this software for free,
with a trial-length time limit on commercial use. Specifically:
If you follow the rules below, you may do everything with this
software that would otherwise infringe either the contributor's
copyright in it, any patent claim the contributor can license
that covers this software as of the contributor's latest
contribution, or both.
1. You must limit use of this software in any manner primarily
intended for or directed toward commercial advantage or
private monetary compensation to a trial period of 32
consecutive calendar days. This limit does not apply to use in
developing feedback, modifications, or extensions that you
contribute back to those giving this license.
2. Ensure everyone who gets a copy of this software from you, in
source code or any other form, gets the text of this license
and the contributor and source code lines above.
3. Do not make any legal claim against anyone for infringing any
patent claim they would infringe by using this software alone,
accusing this software, with or without changes, alone or as
part of a larger application.
You are excused for unknowingly breaking rule 1 if you stop
doing anything requiring this license within 30 days of
learning you broke the rule.
**This software comes as is, without any warranty at all. As far
as the law allows, the contributor will not be liable for any
damages related to this software or this license, for any kind of
legal claim.**

View file

@ -0,0 +1,71 @@
![npm (scoped)](https://img.shields.io/npm/v/@adrielus/option?style=for-the-badge)
![npm bundle size (scoped)](https://img.shields.io/bundlephobia/minzip/@adrielus/option?style=for-the-badge)
[![forthebadge](https://forthebadge.com/images/badges/powered-by-water.svg)](https://forthebadge.com)
# Option
Probably the most opinionated implementation of the Option type for TypeScript.
## Features:
- Lazy and async versions of helpers:
One of the goals of this lib is to provide variations of helpers which are lazy (don't compute something if it's not needed) or async (make mixing Promises and Options easier). If there is any function you want one of those variations of, be sure to open an issue:)
- Large amount of helpers (curently 30), more than f#'s and elm's core libraries combined.
- Typesafe:
```ts
const foo0: Option<string> = None // works
const foo1: Option<string> = Some('foo1') // works
const foo2: Option<string> = 'foo2' // errors out
const foo3: Option<string> = null // errors out
const foo4: Option<string> = Some(4) // errors out
```
- Reference equality:
```ts
Some(7) === Some(7) // true
Some(7) === Some(5) // false
Some(7) === None // false
```
## Limitations
Both limitaions bellow come from the lack of nominal-typing offered by TypeScript and are inherited from the `Brand` type offered by the [utility-types](https://github.com/piotrwitek/utility-types) library
- Due to the way the library works (using the `Brand`
type from [utility-types](https://github.com/piotrwitezutility-types)) `Some(4) === 4` will return true, similarly to how `4 == "4"` returns true (except in this libraries case the `===` operator will behave the same way).
- The inner value of `Option` cannot have a `__brand` prop
(well, tehnically it can but it would be overwritten by the `Brand` type from [utility-types](https://github.com/piotrwitek/utility-types))
## Installation
```sh
npm install @adrielus/option
```
(There is also an amd build at `/dist/bundle.umd.js` which uses the `Option` namespace)
## Usage
For detailed usage read [the docs](https://github.com/Mateiadrielrafael/option/tree/master/docs/main.md)
> Note: The docs are still work in progress. Contributions are welcome:)
# Contributing
First, clone this repo:
```sh
git clone https://github.com/Mateiadrielrafael/option
cd option
```
Then use **_pnpm_** to install the dependencies:
```sh
pnpm install
```
You can use the `build` command to build the package (this is dont automatically by github actions):
```sh
pnpm run build
```

View file

@ -0,0 +1,230 @@
# Documentation
## Table of contents:
### General:
- [Option](#Option)
- [Some](#Some)
- [None](#None)
### Helpers:
- [bind](#Bind)
- [count](#Count)
- [exists](#Exists)
- [filter](#Filter)
- [fold](#Fold)
- [foldback](#Foldback)
- [forall](#Forall)
- [fromNullable](#FromNullable)
- [fromArray](#FromArray)
# General
## Option
Data type holding an optional value (can be either None or Some(x))
### Signature
```ts
type Option<T> = Internals.SomeClass<T> | Internals.NoneClass
```
## None
Value holding nothing
### Signature
```ts
const None: Internals.NoneClass
```
## Some
Creates an Option instance holding a value
### Signature
```ts
const Some: <T>(v: T) => Internals.SomeClass<T>
```
### Usage
```ts
import { Some } from '@adrielus/option'
Some(x) // Some(x)
```
# Helpers
## Bind
Invokes a function on an optional value that itself yields an option.
### Signature
```ts
const bind: <T, U>(binder: Mapper<T, Option<U>>, option: Option<T>) => Option<U>
```
### Usage
```ts
import { Some, None, bind } from '@adrielus/option'
const half = (x: number) => (x % 2 ? None : Some(x / 2))
bind(half, Some(14)) // Some(7)
bind(half, Some(13)) // None
bind(half, None) // None
```
## Count
Returns a zero if the option is None, a one otherwise.
### Signature:
```ts
const count: <T>(option: Option<T>) => number
```
### Usage
```ts
import { Some, None, count } from '@adrielus/option'
count(Some(x)) // 1
count(None) // 0
```
## Exists
Returns false if the option is None, otherwise it returns the result of applying the predicate to the option value.
### Signature
```ts
const exists: <T>(predicate: Mapper<T, boolean>, option: Option<T>) => boolean
```
### Usage
```ts
import { Some, None, exists } from '@adrielus/option'
exists(() => true, None) // false
exists(() => true, Some(x)) // true
exists(() => false, Some(x)) // false
```
## Filter
Invokes a function on an optional value that itself yields an option.
### Signature:
```ts
const filter: <T>(predicate: Mapper<T, boolean>, option: Option<T>) => NoneClass
```
### Usage
```ts
import { Some, None, filter } from '@adrielus/option'
filter(() => true, None) // None
filter(() => true, Some(x)) // Some(x)
filter(() => false, Some(x)) // None
```
## Fold
A function to update the state data when given a value from an option.
### Signature
```ts
const fold: <T, U>(folder: Folder<T, U>, initial: U, option: Option<T>) => U
```
### Usage
```ts
import { Some, None, fold } from '@adrielus/option'
const add = (a: number, b: number) => a + b
fold(add, x, None) // x
fold(add, x, Some(y)) // x + y
```
## Foldback
A function to update the state data when given a value from an option.
### Signature
```ts
const foldback: <T, U>(
folder: BackFolder<T, U>,
option: Option<T>,
initial: U
) => U
```
### Usage
```ts
import { Some, None, foldback } from '@adrielus/option'
const add = (a: number, b: number) => a + b
foldback(add, None, x) // x
foldback(add, Some(y), x) // x + y
```
# FromNullable
A function to create options from nullable values.
### Signature
```ts
const fromNullable: <T>(value: Nullable<T>) => Option<T>
```
### Usage
```ts
import { Some, None, fromNullable } from '@adrielus/option'
fromNullable(7) // Some(7)
fromNullable(null) // None
```
## FromArray
A function to create options from arrays. If the given array is empty produces None, else Some of the first element.
### Signature
```ts
const fromArray: <T>(value: [T] | []) => Option<T>
```
### Usage
```ts
import { Some, None, fromArray } from '@adrielus/option'
fromArray([7]) // Some(7)
fromArray([]) // None
```
**_This is still work in progress, right now only covering about 60% of the library. Contributions are welcome_**

View file

@ -0,0 +1,68 @@
{
"name": "@adrielus/option",
"version": "0.0.0-development",
"description": "Typescript version of fsharps Option module",
"main": "dist/bundle.cjs.js",
"module": "dist/index.esm.js",
"typings": "dist/index.esm.d.ts",
"browser": "dist/bundle.umd.js",
"scripts": {
"prebuild": "rimraf dist",
"build": "rollup -c rollup.config.ts",
"test": "mocha -r ts-node/register src/**/*.test.ts"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"keywords": [
"typescript",
"fsharp",
"fp",
"functional-programming",
"monad",
"immutable",
"stateless",
"classless",
"option",
"typesafe",
"functor",
"pure",
"option",
"some",
"just",
"none",
"nothing",
"maybe",
"nullable"
],
"sideEffects": false,
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-node-resolve": "^6.0.0",
"@types/chai": "^4.2.7",
"@types/mocha": "^5.2.7",
"@types/node": "^12.12.21",
"@types/sinon": "^7.5.1",
"@wessberg/rollup-plugin-ts": "^1.1.83",
"chai": "^4.2.0",
"mocha": "^6.2.2",
"rimraf": "^3.0.0",
"rollup": "^1.27.14",
"rollup-plugin-filesize": "^6.2.1",
"rollup-plugin-terser": "^5.1.3",
"semantic-release": "^15.14.0",
"sinon": "^8.0.1",
"ts-node": "^8.5.4",
"typescript": "^3.7.4"
},
"author": "Matei Adriel",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@thi.ng/compose": "^1.3.6",
"tslib": "^1.10.0",
"utility-types": "^3.10.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
import { terser } from 'rollup-plugin-terser'
import { resolve } from 'path'
import ts from '@wessberg/rollup-plugin-ts'
import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import _package from './package.json'
import filesize from 'rollup-plugin-filesize'
const inputFile = resolve(__dirname, 'src/index.ts')
const dev = Boolean(process.env.ROLLUP_WATCH)
const commonPlugins = [nodeResolve(), commonjs()]
export default [
{
input: inputFile,
output: [
{
file: _package.main,
format: 'cjs',
sourcemap: true
},
{
file: _package.browser,
sourcemap: true,
format: 'umd',
name: 'Option'
}
],
plugins: [...commonPlugins, ts(), !dev && terser()]
},
{
input: inputFile,
output: [
{
file: _package.module,
format: 'esm',
sourcemap: true
}
],
plugins: [
...commonPlugins,
ts({
tsconfig: {
declaration: true,
...require(resolve(__dirname, 'tsconfig.json'))[
'compilerOptions'
]
}
}),
!dev && terser(),
filesize({
showBrotliSize: true
})
]
}
]

View file

@ -0,0 +1,32 @@
import { expect } from 'chai'
import { bind } from './bind'
import { Some, None } from '../types'
import { constantly } from '@thi.ng/compose'
describe('The bind helper', () => {
it('should return None for any callback when given None', () => {
// act
const result = bind(Some, None)
// assert
expect(result).to.equal(None)
})
describe('When given Some', () => {
it('should return None if the callback returns None', () => {
// act
const result = bind(constantly(None), Some(3))
// assert
expect(result).to.equal(None)
})
it('should return Some if the callback returns Some', () => {
// act
const result = bind(x => Some(x + 1), Some(3))
// assert
expect(result).to.equal(Some(4))
})
})
})

View file

@ -0,0 +1,10 @@
import { Binder } from '../internalTypes'
import { Option, None } from '../types'
import { unwrap } from './unwrap'
export const bind = <T, U>(
binder: Binder<T, U>,
option: Option<T>
): Option<U> => {
return unwrap(None, binder, option)
}

View file

@ -0,0 +1,31 @@
import { expect } from 'chai'
import { Some, None } from '../types'
import { bindAsync } from './bindAsync'
describe('The bindAsync helper', () => {
it('should return None for any callback when given None', async () => {
// act
const result = await bindAsync(async v => Some(v), None)
// assert
expect(result).to.equal(None)
})
describe('When given Some', () => {
it('should return None if the callback returns None', async () => {
// act
const result = await bindAsync(async _ => None, Some(3))
// assert
expect(result).to.equal(None)
})
it('should return Some if the callback returns Some', async () => {
// act
const result = await bindAsync(async x => Some(x + 1), Some(3))
// assert
expect(result).to.equal(Some(4))
})
})
})

View file

@ -0,0 +1,10 @@
import { Mapper } from '../internalTypes'
import { Option, None } from '../types'
import { unwrap } from './unwrap'
export const bindAsync = <T, U>(
binder: Mapper<T, Promise<Option<U>>>,
option: Option<T>
): Promise<Option<U>> => {
return unwrap(Promise.resolve(None), binder, option)
}

View file

@ -0,0 +1,26 @@
import { expect } from 'chai'
import { combine } from './combine'
import { someX } from '../../test/constants'
import { None } from '../types'
import { isSome } from './isSome'
describe('The combine helepr', () => {
it('should return None if the iterable contains any Nones', () => {
// act
const result = combine([someX, someX, None, someX, someX])
// assert
expect(result).to.equal(None)
})
it("should return Some when the iterable doesn't contain any None", () => {
// arrange
const items = Array(50).fill(someX)
// act
const result = combine(items)
// act
expect(isSome(result)).to.be.true
})
})

View file

@ -0,0 +1,19 @@
import { Option, None, Some } from '../types'
import { isNone } from './isNone'
/**
* If every Option in the list is present, return all of the values unwrapped.
* If there are any Nones, the whole function fails and returns None.
*
* @param iterable The iterable to combine.
*/
export const combine = <T>(iterable: Iterable<Option<T>>) => {
const set = new Set(iterable)
if (set.has(None)) {
return None
}
const array = Array.from(set) as T[]
return array as Option<T[]>
}

View file

@ -0,0 +1,22 @@
import { expect } from 'chai'
import { Some, None } from '../types'
import { count } from './count'
import { x } from '../../test/constants'
describe('The count helper', () => {
it('should return 1 when given Some', () => {
// act
const result = count(Some(x))
// assert
expect(result).to.equal(1)
})
it('should return 0 when given None', () => {
// act
const result = count(None)
// assert
expect(result).to.equal(0)
})
})

View file

@ -0,0 +1,4 @@
import { isSome } from './isSome'
import { Option } from '../types'
export const count = <T>(option: Option<T>) => Number(isSome(option))

View file

@ -0,0 +1,33 @@
import { expect } from 'chai'
import { exists } from './exists'
import { constantly } from '@thi.ng/compose'
import { None, Some } from '../types'
import { x } from '../../test/constants'
describe('The exists helper', () => {
it('should return false when given None', () => {
// act
const result = exists(constantly(true), None)
// assert
expect(result).to.equal(false)
})
describe('When given Some', () => {
it('should return true if the callback returns true', () => {
// act
const result = exists(constantly(true), Some(x))
// assert
expect(result).to.equal(true)
})
it('should return false if the callback returns false', () => {
// act
const result = exists(constantly(false), Some(x))
// assert
expect(result).to.equal(false)
})
})
})

View file

@ -0,0 +1,7 @@
import { unwrap } from './unwrap'
import { Predicate } from '../internalTypes'
import { Option } from '../types'
export const exists = <T>(predicate: Predicate<T>, option: Option<T>) => {
return unwrap(false, predicate, option)
}

View file

@ -0,0 +1,30 @@
export * from './bind'
export * from './bindAsync'
export * from './combine'
export * from './count'
export * from './exists'
export * from './filter'
export * from './first'
export * from './flat'
export * from './fold'
export * from './foldback'
export * from './forall'
export * from './fromArray'
export * from './fromNullable'
export * from './get'
export * from './isNone'
export * from './isSome'
export * from './iter'
export * from './map'
export * from './mapAsync'
export * from './oneOf'
export * from './optionify'
export * from './or'
export * from './orLazy'
export * from './toArray'
export * from './toNullable'
export * from './unpack'
export * from './unwrap'
export * from './values'
export * from './withDefault'
export * from './withDefaultLazy'

View file

@ -0,0 +1,47 @@
import { expect } from 'chai'
import { constantly } from '@thi.ng/compose'
import { filter } from './filter'
import { None, Some } from '../types'
import { someX } from '../../test/constants'
describe('The filter helper', () => {
describe('When the predicate returns true', () => {
const predicate = constantly(true)
it('should return None when given None', () => {
// act
const result = filter(predicate, None)
// assert
expect(result).to.equal(None)
})
it('should return Some(x) when given Some(x)', () => {
// act
const result = filter(predicate, someX)
// assert
expect(result).to.equal(someX)
})
})
describe('When the predicate returns false', () => {
const predicate = constantly(false)
it('should return None when given Some', () => {
// act
const result = filter(predicate, someX)
// assert
expect(result).to.equal(None)
})
it('should return None when given None', () => {
// act
const result = filter(predicate, None)
// assert
expect(result).to.equal(None)
})
})
})

View file

@ -0,0 +1,7 @@
import { unwrap } from './unwrap'
import { Some, None, Option } from '../types'
import { Predicate } from '../internalTypes'
export const filter = <T>(predicate: Predicate<T>, option: Option<T>) => {
return unwrap(None, v => (predicate(v) ? Some(v) : None), option)
}

View file

@ -0,0 +1,32 @@
import { constantly } from '@thi.ng/compose'
import { expect } from 'chai'
import { someX } from '../../test/constants'
import { None, Option } from '../types'
import { first } from './first'
describe('The first helper', () => {
it('should return the first Some if there is any', () => {
// act
const head = first([someX, None])
const middle = first([None, someX, None])
const tail = first([None, someX])
// assert
expect(head).to.equal(someX)
expect(middle).to.equal(someX)
expect(tail).to.equal(someX)
})
it("should return None if there isn't any Some", () => {
// arrange
const array: Option<unknown>[] = Array(50)
.fill(1)
.map(constantly(None))
// act
const result = first(array)
// assert
expect(result).to.equal(None)
})
})

View file

@ -0,0 +1,17 @@
import { isSome } from './isSome'
import { Option, None } from '../types'
/**
* Returns the first Some in an iterable. If there isn't any returns None.
*
* @param elemenets The elements to find the first Some in.
*/
export const first = <T>(elemenets: Iterable<Option<T>>) => {
for (const option of elemenets) {
if (isSome(option)) {
return option
}
}
return None
}

View file

@ -0,0 +1,25 @@
import { expect } from 'chai'
import { flat } from './flat'
import { None, Some } from '../types'
import { someX } from '../../test/constants'
describe('The flat helper', () => {
it('should return None when given None', () => {
// act
const result = flat(None)
// assert
expect(result).to.equal(None)
})
it('should return the inner Some(x) when given Some(Some(x))', () => {
// arrange
const value = Some(someX)
// act
const result = flat(value)
// assert
expect(result).to.equal(someX)
})
})

View file

@ -0,0 +1,7 @@
import { bind } from './bind'
import { identity } from '@thi.ng/compose'
import { Option } from '../types'
export const flat = <T>(option: Option<Option<T>>): Option<T> => {
return bind(identity, option)
}

View file

@ -0,0 +1,11 @@
import { unwrap } from './unwrap'
import { Option } from '../types'
import { Folder } from '../internalTypes'
export const fold = <T, U>(
folder: Folder<T, U>,
initial: U,
option: Option<T>
) => {
return unwrap(initial, v => folder(initial, v), option)
}

View file

@ -0,0 +1,11 @@
import { unwrap } from './unwrap'
import { Option } from '../types'
import { BackFolder } from '../internalTypes'
export const foldback = <T, U>(
folder: BackFolder<T, U>,
option: Option<T>,
initial: U
) => {
return unwrap(initial, v => folder(v, initial), option)
}

View file

@ -0,0 +1,7 @@
import { unwrap } from './unwrap'
import { Predicate } from '../internalTypes'
import { Option } from '../types'
export const forall = <T>(predicate: Predicate<T>, option: Option<T>) => {
return unwrap(true, predicate, option)
}

View file

@ -0,0 +1,5 @@
import { None, Some, Option } from '../types'
export const fromArray = <T>(value: [T] | []): Option<T> => {
return value[0] === undefined ? None : Some(value[0])
}

View file

@ -0,0 +1,6 @@
import { Nullable } from '../internalTypes'
import { Some, None, Option } from '../types'
export const fromNullable = <T>(value: Nullable<T>): Option<T> => {
return value === null ? None : Some(value)
}

View file

@ -0,0 +1,22 @@
import { expect } from 'chai'
import { get } from './get'
import { None } from '../types'
import { someX, x } from '../../test/constants'
describe('The get helper', () => {
it('should throw when given None', () => {
// act
const callable = () => get(None)
// assert
expect(callable).to.throw()
})
it('should return the innter value when given Some', () => {
// act
const result = get(someX)
// assert
expect(result).to.equal(x)
})
})

View file

@ -0,0 +1,10 @@
import { Option } from '../types'
import { isSome } from './isSome'
export const get = <T>(option: Option<T>): T => {
if (isSome(option)) {
return option as T
}
throw new Error(`Cannot get value of None`)
}

View file

@ -0,0 +1,22 @@
import { expect } from 'chai'
import { isNone } from './isNone'
import { None } from '../types'
import { someX } from '../../test/constants'
describe('The isNone helper', () => {
it('should return false when given Some', () => {
// act
const result = isNone(someX)
// assert
expect(result).to.equal(false)
})
it('should return true when given None', () => {
// act
const result = isNone(None)
// assert
expect(result).to.equal(true)
})
})

View file

@ -0,0 +1,4 @@
import { Option } from '../types'
import { none } from '../internals'
export const isNone = <T>(option: Option<T>) => option.__brand === none

View file

@ -0,0 +1,22 @@
import { expect } from 'chai'
import { None } from '../types'
import { someX } from '../../test/constants'
import { isSome } from './isSome'
describe('The isSome helper', () => {
it('should return true when given Some', () => {
// act
const result = isSome(someX)
// assert
expect(result).to.equal(true)
})
it('should return false when given None', () => {
// act
const result = isSome(None)
// assert
expect(result).to.equal(false)
})
})

View file

@ -0,0 +1,4 @@
import { Option } from '../types'
import { isNone } from './isNone'
export const isSome = <T>(option: Option<T>) => !isNone(option)

View file

@ -0,0 +1,9 @@
import { Mapper } from '../internalTypes'
import { Option } from '../types'
import { isSome } from './isSome'
export const iter = <T>(mapper: Mapper<T, void>, option: Option<T>) => {
if (isSome(option)) {
mapper(option as T)
}
}

View file

@ -0,0 +1,10 @@
import { unwrap } from './unwrap'
import { Mapper } from '../internalTypes'
import { Option, Some, None } from '../types'
export const map = <T, U>(
mapper: Mapper<T, U>,
option: Option<T>
): Option<U> => {
return unwrap(None, v => Some(mapper(v)), option)
}

View file

@ -0,0 +1,14 @@
import { Option, None, Some } from '../types'
import { Mapper } from '../internalTypes'
import { unwrap } from './unwrap'
export const mapAsync = <T, U>(
mapper: Mapper<T, Promise<U>>,
option: Option<T>
) => {
return unwrap(
Promise.resolve(None),
value => mapper(value).then(Some),
option
)
}

View file

@ -0,0 +1,55 @@
import { constantly } from '@thi.ng/compose'
import { expect } from 'chai'
import { spy } from 'sinon'
import { x, alwaysSomeX } from '../../test/constants'
import { None, Some } from '../types'
import { oneOf } from './oneOf'
const alwaysSome = <T>(v: T) => constantly(Some(v))
describe('The oneOf helper', () => {
it('should return None on an empty array', () => {
// act
const result = oneOf(x, [])
// assert
expect(result).to.equal(None)
})
it('should return the result of the first function which evaluates to Some', () => {
// arrange
const alwaysNone = constantly(None)
// act
const head = oneOf(x, [alwaysSome('head'), alwaysNone])
const middle = oneOf(x, [alwaysNone, alwaysSome('middle'), alwaysNone])
const tail = oneOf(x, [alwaysNone, alwaysSome('tail')])
// assert
expect(head).to.equal(Some('head'))
expect(middle).to.equal(Some('middle'))
expect(tail).to.equal(Some('tail'))
})
it('should not evaluate any more functions after it found the result', () => {
// arrange
const func = spy(alwaysSomeX)
// act
oneOf(x, [alwaysSomeX, func])
// assert
expect(func.called).to.be.false
})
it('should pass the provided input to the functions', () => {
// arrange
const func = spy(alwaysSomeX)
// act
oneOf(x, [func])
// assert
expect(func.calledWith(x)).to.be.true
})
})

View file

@ -0,0 +1,23 @@
import { Binder } from '../internalTypes'
import { isSome } from './isSome'
import { None } from '../types'
/**
* Try a list of functions against a value.
* Return the value of the first call that succeeds (aka returns Some).
* If no function retursn Some this will default to None.
*
* @param input The input to pass to the functions.
* @param functions Iterable of functions to try against the input.
*/
export const oneOf = <T, U>(input: T, functions: Binder<T, U>[]) => {
for (const func of functions) {
const result = func(input)
if (isSome(result)) {
return result
}
}
return None
}

View file

@ -0,0 +1,17 @@
import { expect } from 'chai'
import { optionify } from './optionify'
import { fromNullable } from './fromNullable'
describe('The optionify helper', () => {
it('should create a function which returns an option instead of a nullable', () => {
// arrange
const func = (a: number, b: number) => (a > b ? a + b : null)
// act
const result = optionify(func)
// assert
expect(result(1, 2)).to.equal(fromNullable(func(1, 2)))
expect(result(2, 1)).to.equal(fromNullable(func(2, 1)))
})
})

View file

@ -0,0 +1,15 @@
import { fromNullable } from './fromNullable'
/**
* Takes a function which returns a nullable and creates
* a function which returns an Option.
* In functional programming this would be the same as
* composing the function with fromNullable.
*
* @param f The function to optionify
*/
export const optionify = <T extends unknown[], U>(
f: (...args: T) => U | null
) => {
return (...args: T) => fromNullable(f(...args))
}

View file

@ -0,0 +1,26 @@
import { expect } from 'chai'
import { or } from './or'
import { someX } from '../../test/constants'
import { None } from '../types'
describe('The or helper', () => {
describe('When the first argument is None', () => {
it('should return the second argument', () => {
// act
const orSome = or(None, someX)
const orNone = or(None, None)
// assert
expect(orSome).to.equal(someX)
expect(orNone).to.equal(None)
})
})
it("should return the first argument when it's not None", () => {
// act
const result = or(someX, None)
// assert
expect(result).to.equal(someX)
})
})

View file

@ -0,0 +1,20 @@
import { Option } from '../types'
import { isSome } from './isSome'
/**
* Returns the first value that is present, like the boolean ||.
* Both values will be computed.
* There is no short-circuiting.
* If your second argument is expensive to calculate and
* you need short circuiting, use orLazy instead.
*
* @param a The first argument.
* @param b The second argument.
*/
export const or = <T>(a: Option<T>, b: Option<T>): Option<T> => {
if (isSome(a)) {
return a
} else {
return b
}
}

View file

@ -0,0 +1,48 @@
import { expect } from 'chai'
import { constantly } from '@thi.ng/compose'
import { orLazy } from './orLazy'
import { someX } from '../../test/constants'
import { None } from '../types'
import { spy } from 'sinon'
describe('The orLazy helper', () => {
it("should return the first argument if it's Some", () => {
// act
const result = orLazy(someX, constantly(None))
// asser
expect(result).to.equal(someX)
})
it('should return the return of the second argument if the first is None', () => {
// act
const orSome = orLazy(None, constantly(someX))
const orNone = orLazy(None, constantly(None))
// assert
expect(orSome).to.equal(someX)
expect(orNone).to.equal(None)
})
it('should not evaluate the second argument if the first one is Some', () => {
// arrange
const func = spy(constantly(someX))
// act
orLazy(someX, func)
// assert
expect(func.called).to.be.false
})
it('should evaluate the second argument if the first one is None', () => {
// arrange
const func = spy(constantly(someX))
// act
orLazy(None, func)
// assert
expect(func.called).to.be.true
})
})

View file

@ -0,0 +1,17 @@
import { isSome } from './isSome'
import { Option } from '../types'
/**
* Lazy version of or.
* The second argument will only be evaluated if the first argument is Nothing.
*
* @param a The first argument.
* @param b The second argument.
*/
export const orLazy = <T>(a: Option<T>, b: () => Option<T>) => {
if (isSome(a)) {
return a
}
return b()
}

View file

@ -0,0 +1,6 @@
import { unwrap } from './unwrap'
import { Option } from '../types'
export const toArray = <T>(option: Option<T>) => {
return unwrap([], v => [v], option)
}

View file

@ -0,0 +1,7 @@
import { unwrap } from './unwrap'
import { identity } from '@thi.ng/compose'
import { Option } from '../types'
export const toNullable = <T>(option: Option<T>) => {
return unwrap(null, identity, option)
}

View file

@ -0,0 +1,61 @@
import { expect } from 'chai'
import { None } from '../types'
import { alwaysX, x, someX } from '../../test/constants'
import { constantly } from '@thi.ng/compose'
import { spy } from 'sinon'
import { unpack } from './unpack'
describe('The unpack helper', () => {
describe('When given None', () => {
it('should return the default when given None', () => {
// act
const result = unpack(constantly(0), constantly(1), None)
// assert
expect(result).to.equal(0)
})
it('should call the lazy default', () => {
// arrange
const func = spy(alwaysX)
// act
unpack(func, alwaysX, None)
// assert
expect(func.called).to.be.true
})
})
describe('When given Some', () => {
it('should return the return of the mapper', () => {
// act
const result = unpack(constantly(0), constantly(1), someX)
// assert
expect(result).to.equal(1)
})
it('should not call the lazy default', () => {
// arrange
const func = spy(alwaysX)
// act
unpack(func, alwaysX, someX)
// assert
expect(func.called).to.be.false
})
it('should pass the inner value to the mapper', () => {
// arrange
const mapper = spy(alwaysX)
// act
unpack(alwaysX, mapper, someX)
// assert
expect(mapper.calledWith(x)).to.be.true
})
})
})

View file

@ -0,0 +1,20 @@
import { Lazy, Mapper } from '../internalTypes'
import { Option } from '../types'
import { withDefaultLazy } from './withDefaultLazy'
import { map } from './map'
/**
* Like unwrap, but the default value is lazy,
* and will only be computed if the Option is None.
*
* @param _default The lazy value to use in case option is None.
* @param mapper The function to pass the inner value to.
* @param option The option to unpack.
*/
export const unpack = <T, U>(
_default: Lazy<U>,
mapper: Mapper<T, U>,
option: Option<T>
) => {
return withDefaultLazy(_default, map(mapper, option))
}

View file

@ -0,0 +1,37 @@
import { constantly, identity } from '@thi.ng/compose'
import { expect } from 'chai'
import { spy } from 'sinon'
import { someX, x } from '../../test/constants'
import { None } from '../types'
import { unwrap } from './unwrap'
describe('The unwrap helper', () => {
it('should return the default when given None', () => {
// act
const result = unwrap(0, constantly(1), None)
// assert
expect(result).to.equal(0)
})
describe('When given Some', () => {
it('should return the result of the mapper ', () => {
// act
const result = unwrap(0, constantly(1), someX)
// assert
expect(result).to.equal(1)
})
it('should pass the inner value to the mapper', () => {
// arrange
const mapper = spy(identity)
// act
unwrap(0, mapper, someX)
// assert
expect(mapper.calledWith(x)).to.be.true
})
})
})

View file

@ -0,0 +1,23 @@
import { Option } from '../types'
import { Mapper } from '../internalTypes'
import { isSome } from './isSome'
/**
* Apply the function to the value in the Option and return it unwrapped.
* If the Option is None, use the default value instead.
*
* @param _default The default value to use.
* @param mapper Function to apply to the inner value.
* @param option Option to unwrap.
*/
export const unwrap = <T, U>(
_default: U,
caseSome: Mapper<T, U>,
option: Option<T>
) => {
if (isSome(option)) {
return caseSome(option as T)
}
return _default
}

View file

@ -0,0 +1,19 @@
import { expect } from 'chai'
import { values } from './values'
import { Some, None } from '../types'
describe('The values helper', () => {
it('should ignore all None values', () => {
// arrange
const items = Array(50)
.fill(1)
.map((_, i) => (i % 2 ? Some(i) : None))
// act
const result = values(items)
// assert
expect(result).to.not.contain(None)
expect(result, "ensure it didn't clear everything").to.not.be.empty
})
})

View file

@ -0,0 +1,11 @@
import { Option } from '../types'
import { toArray } from './toArray'
/**
* Take all the values that are present, throwing away any None
*
* @param iterable The iterable to collect the values from.
*/
export const values = <T>(iterable: Iterable<Option<T>>) => {
return Array.from(iterable).flatMap(toArray)
}

View file

@ -0,0 +1,22 @@
import { expect } from 'chai'
import { withDefault } from './withDefault'
import { x } from '../../test/constants'
import { None, Some } from '../types'
describe('The withDefault helper', () => {
it('should return the default when given None', () => {
// act
const result = withDefault(x, None)
// assert
expect(result).to.equal(x)
})
it('should return x when given Some(x)', () => {
// act
const result = withDefault(0, Some(1))
// assert
expect(result).to.equal(1)
})
})

View file

@ -0,0 +1,13 @@
import { unwrap } from './unwrap'
import { identity } from '@thi.ng/compose'
import { Option } from '../types'
/**
* Provide a default value, turning an optional value into a normal value.
*
* @param _default The default value to use.
* @param option The option to get the default of.
*/
export const withDefault = <T>(_default: T, option: Option<T>) => {
return unwrap(_default, identity, option)
}

View file

@ -0,0 +1,50 @@
import { expect } from 'chai'
import { withDefaultLazy } from './withDefaultLazy'
import { None, Some } from '../types'
import { alwaysX, x, someX } from '../../test/constants'
import { constantly } from '@thi.ng/compose'
import { spy } from 'sinon'
describe('The withDefaultLazy helper', () => {
describe('When given None', () => {
it('should return the default when given None', () => {
// act
const result = withDefaultLazy(alwaysX, None)
// assert
expect(result).to.equal(x)
})
it('should call the lazy default', () => {
// arrange
const func = spy(constantly(x))
// act
withDefaultLazy(func, None)
// assert
expect(func.called).to.be.true
})
})
describe('When given Some', () => {
it('should return the inner value', () => {
// act
const result = withDefaultLazy(constantly(0), Some(1))
// assert
expect(result).to.equal(1)
})
it('should not call the lazy default', () => {
// arrange
const func = spy(constantly(x))
// act
withDefaultLazy(func, someX)
// assert
expect(func.called).to.be.false
})
})
})

View file

@ -0,0 +1,16 @@
import { Option } from '../types'
import { isSome } from './isSome'
import { Lazy } from '../internalTypes'
import { get } from './get'
/**
* Same as withDefault but the default is only evaluated when the option is None.
*
* @param _default Function returning the default value to use.
* @param option The option to get the default of.
*/
export const withDefaultLazy = <T>(_default: Lazy<T>, option: Option<T>) => {
if (isSome(option)) {
return get(option)
} else return _default()
}

View file

@ -0,0 +1,2 @@
export * from './helpers/external'
export * from './types'

View file

@ -0,0 +1,9 @@
import { Option } from './types'
export type Mapper<T, U> = (v: T) => U
export type Binder<T, U> = (v: T) => Option<U>
export type Predicate<T> = (v: T) => boolean
export type Folder<T, U> = (s: U, v: T) => U
export type BackFolder<T, U> = (v: T, s: U) => U
export type Nullable<T> = T | null
export type Lazy<T> = () => T

View file

@ -0,0 +1 @@
export const none = Symbol('none')

View file

@ -0,0 +1,17 @@
import { identity } from '@thi.ng/compose'
import { Brand } from 'utility-types'
import { none } from './internals'
// This is never actually used outside of typing so we can just declare it
declare const some: unique symbol
type None = Brand<void, typeof none>
type Some<T> = Brand<T, typeof some>
export type Option<T> = Some<T> | None
export const None = {
__brand: none,
toString: () => 'None'
} as None
export const Some = identity as <T>(value: T) => Option<T>

View file

@ -0,0 +1,11 @@
import { constantly } from '@thi.ng/compose'
import { Some } from '../src'
// general value to pass around
export const x = Symbol('x')
// same as x but for some
export const someX = Some(x)
export const alwaysX = constantly(x)
export const alwaysSomeX = constantly(someX)

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "CommonJS",
"lib": ["esnext", "dom", "es2015.iterable"],
"moduleResolution": "node",
"strictNullChecks": true,
"sourceMap": true,
"downlevelIteration": true,
"target": "es6",
"resolveJsonModule": true
},
"include": ["src", "sandbox", "test", "package.json"],
"exclude": ["dist"]
}