This commit is contained in:
Rowan 2025-07-03 09:34:08 -04:00
parent b997bca5e3
commit 1ccb2b1b1c
34 changed files with 1838 additions and 4311 deletions

259
README.md
View file

@ -1,259 +0,0 @@
# kojima
a small functional/monad library
# Usage
## Example
```js
import { Option, Some, None, Result, Ok, Err } from 'kojima'
const maybe = Option.of('thing')
maybe.isSome() // true
const isnt = maybe.chain(None).map(_x => 'other') // None
isnt.isSome() // false
const result = Result.of('3:41am')
result.isOk() // true
result.chain(() => Err(new Error(-Infinity)))
.map(_x => '4:10am')
.chain(Ok)
result.isErr() // true
// crimes!
None == None() // true
```
## Documentation
### curry
> (* -> a) -> (* -> a)
Returns a curried equivalent of the provided function.
```js
import { curry } from './curry.js'
const add = (a, b, c) => a + b + c
const curriedAdd = curry(add)
const add1 = curriedAdd(1)
const add3 = add1(2)
const add4 = curriedAdd(2, 2)
const six = add3(3) // 6
const eight = add4(2) // 7
const twelve = curriedAdd(4, 4, 4) // 12
```
### Option
> Option\<T\> = Some\<T\> | None
Represents a value which may not exist.
#### Methods
##### of :: Option f => a -> f a
> Option.of\<a\>(value: a) -> Option\<a\>
Creates a new `Some<T>` from `T`
```js
const some = Option.of(1) // Some<number>(1)
```
##### zero :: Option f => () -> f a
> Option.zero() -> Option\<()\>
Creates a new `None`
```js
const none = Option.zero() // None
```
##### chain :: Option m => m a ~> (a -> m b) -> m b
> Option\<T\>.chain\<U\>(fn: (value: T) -> Option\<U\>) -> Option\<U\>
Transform a `Option<T>` into `Option<U>` by applying `fn(T) -> Option<U>`.
```js
const some = Option.of(1)
const next = some.chain(x => Some(`value ${x}`)) // Some<string>('value 1')
None.chain(x => Some(`value ${x}`)) // None
const none = some.chain(None) // None
none.chain(() => Some(1)) // None
```
##### map :: Option f => f a ~> (a -> b) -> f b
> Option\<T\>.map\<U\>(fn: (value: T) -> U) -> Option\<U\>
##### alt :: Option f => f a ~> f a -> f a
> Option\<T\>.alt(other: Option\<T\>) -> Option\<T\>
Choose between either the first or second `Option` based on existence.
```js
const some = Option.of(1)
const none = Option.zero()
some.alt(none) // Some<number>(1)
none.alt(some) // Some<number>(1)
some.alt(Some(2)) // Some<number>(1)
Some(2).alt(some) // Some<number>(2)
none.alt(None) // None
None.alt(none) // None
```
##### fold :: Option f => f a ~> ((b, a) -> b, b) -> b
> Option\<T\>.fold\<U\>(fn: ((acc: U, value: T) -> U, initial: U) -> U) -> U
Fold over a `Option`, accumulating the value. This is `unwrap_or_default` in Rust.
```js
const some = Some(1)
some.fold((acc, x) => x), 2) // 1
some.fold((acc, x) => acc), 2) // 2
const none = Option.zero()
none.fold((acc, x) => x, 2) // 2
```
##### isSome :: Option f => () -> boolean
> Option\<T\>.isSome() -> boolean
Returns a boolean based on whether the Option is `Some<T>`
```js
Some(1).isSome() // true
None.isSome() // false
```
##### isNone :: Option f => () -> boolean
> Option\<T\>.isNone() -> boolean
Returns a boolean based on whether the Option is `None`
```js
Some(1).isNone() // false
None.isNone() // true
```
### Result
> Result<T, E> = Ok\<T\> | Err\<E\>
Represents the result of a computation which may fail.
#### Methods
##### of :: Result f => a -> f a
> Result.of\<a\>(value: a) -> Result\<a, ()\>
Creates a new `Ok<T>` from `T`
```js
const ok = Result.of(1) // Ok<number>(1)
```
##### zero :: Result f => () -> f a
> Result.zero() -> Result\<(), ()\>
Creates a new `Err<()>`
```js
const err = Result.zero() // Err<()>()
```
##### chain :: Result m => m a ~> (a -> m b) -> m b
> Result\<T, E\>.chain\<U\>(fn: (value: T) -> Result<U, E>) -> Result\<U, E\>
Transform a `Result<T, E>` into `Result<U, E>` by applying `fn(T) -> Result<U, E>`.
```js
const ok = Result.of(1)
const next = ok.chain(x => Ok(`value ${x}`)) // Ok<string>('value 1')
Err(0).chain(x => Ok(1)) // Err(0)
const err = next.chain(() => Err(0)) // Err(0)
err.chain(() => Ok(1)) // Err(0)
```
##### map :: Result f => f a ~> (a -> b) -> f b
> Result\<T, E\>.map\<U\>(fn: (value: T) -> U) -> Result\<U, E\>
Transform a `Result<T, E>` into a `Result<U, E>` by applying `fn(T) -> U`
```js
const ok = Result.of(1)
const next = ok.map(x => `value ${x}`) // Ok<string>('value 1')
Err(0).map(x => Ok(1)) // Err(0)
```
##### alt :: Result f => f a ~> f a -> f a
> Result\<T, E\>.alt(other: Result\<T, E\>) -> Result\<T, E\>
Choose between either the first or second `Result` based on success.
```js
const ok = Result.of(1)
const err = Result.zero()
ok.alt(err) // Ok<number>(1)
err.alt(ok) // Ok<number>(1)
ok.alt(Ok(2)) // Ok<number>(1)
Ok(2).alt(ok) // Ok<number>(2)
err.alt(Err(new Error('wont see this'))) // Err<()>()
Err(new Error('hi! :3')).alt(err) // Err<Error>(new Error('hi! :3'))
```
##### fold :: Result f => f a ~> ((b, a) -> b, b) -> b
> Result\<T, E\>.fold\<U\>(fn: ((acc: U, value: T) -> U, initial: U) -> U) -> U
Fold over a `Result`, accumulating the value. This is `unwrap_or_default` in Rust.
```js
const ok = Ok(1)
ok.fold((acc, x) => x), 2) // 1
ok.fold((acc, x) => acc), 2) // 2
const err = Result.zero()
err.fold((acc, x) => x, 2) // 2
```
##### bimap :: Result f => f a c ~> (a -> b, c -> d) -> f b d
> Result\<T1, E1\>.bimap\<T2, E2\>(x: (value: T1) -> T2, y: (error: E1) -> E2) -> Result\<T2, E2\>
```js
const ok = Ok(1)
ok.bimap(
x => x + 1,
y => y * 2
) // Ok(2)
const err = Err(4)
err.bimap(
x => x + 1,
y => y * 2
) // Err(8)
```
##### isOk :: Result f => () -> boolean
> Result\<T, E\>.isOk() -> boolean
Returns a boolean based on whether the Result is `Ok<T>`
```js
Ok(1).isOk() // true
Err(1).isOk() // false
```
##### isErr :: Result f => () -> boolean
> Result\<T, E\>.isErr() -> boolean
Returns a boolean based on whether the Result is `Err<E>`
```js
Ok(1).isErr() // false
Err(1).isErr() // true
```

1372
dist/index.js vendored

File diff suppressed because it is too large Load diff

View file

@ -1,15 +0,0 @@
{
"compilerOptions": {
"module": "es2020",
"target": "es6",
"lib": ["esnext", "dom"],
"checkJs": true,
"paths": {
"/*": ["./*"]
}
},
"exclude": [
"node_modules"
]
}

485
package-lock.json generated
View file

@ -1,485 +0,0 @@
{
"name": "kojima",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kojima",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"esbuild": "^0.25.2",
"izuna": "git+https://git.kitsu.cafe/rowan/izuna.git",
"typescript": "^5.8.2"
},
"devDependencies": {
"folktest": "git+https://git.kitsu.cafe/rowan/folktest.git"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.2",
"@esbuild/android-arm": "0.25.2",
"@esbuild/android-arm64": "0.25.2",
"@esbuild/android-x64": "0.25.2",
"@esbuild/darwin-arm64": "0.25.2",
"@esbuild/darwin-x64": "0.25.2",
"@esbuild/freebsd-arm64": "0.25.2",
"@esbuild/freebsd-x64": "0.25.2",
"@esbuild/linux-arm": "0.25.2",
"@esbuild/linux-arm64": "0.25.2",
"@esbuild/linux-ia32": "0.25.2",
"@esbuild/linux-loong64": "0.25.2",
"@esbuild/linux-mips64el": "0.25.2",
"@esbuild/linux-ppc64": "0.25.2",
"@esbuild/linux-riscv64": "0.25.2",
"@esbuild/linux-s390x": "0.25.2",
"@esbuild/linux-x64": "0.25.2",
"@esbuild/netbsd-arm64": "0.25.2",
"@esbuild/netbsd-x64": "0.25.2",
"@esbuild/openbsd-arm64": "0.25.2",
"@esbuild/openbsd-x64": "0.25.2",
"@esbuild/sunos-x64": "0.25.2",
"@esbuild/win32-arm64": "0.25.2",
"@esbuild/win32-ia32": "0.25.2",
"@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/folktest": {
"version": "1.0.0",
"resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#708d44f1215be33fcceba426029f44b4f963dbe5",
"dev": true,
"license": "GPL-3.0-or-later"
},
"node_modules/izuna": {
"version": "1.0.0",
"resolved": "git+https://git.kitsu.cafe/rowan/izuna.git#e11a0870c27eeb5ea1a4ae3fedccca008eda15c2",
"license": "GPL-3.0-or-later"
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View file

@ -1,27 +1,17 @@
{
"name": "kojima",
"type": "module",
"author": "Rowan <rowan@kitsu.cafe> (https://kitsu.cafe)",
"version": "1.0.0",
"description": "",
"version": "2.0.0",
"main": "src/index.js",
"type": "module",
"scripts": {
"test": "./tests/index.js",
"build": "esbuild ./src/index.js --bundle --outfile=./dist/index.js"
"test": "node --test"
},
"keywords": [
"functional",
"functional programming",
"fp",
"monad"
],
"license": "GPL-3.0-or-later",
"devDependencies": {
"folktest": "git+https://git.kitsu.cafe/rowan/folktest.git"
"repository": {
"type": "git",
"url": "git@git.kitsu.cafe:rowan/kojima.git"
},
"dependencies": {
"esbuild": "^0.25.2",
"izuna": "git+https://git.kitsu.cafe/rowan/izuna.git",
"typescript": "^5.8.2"
}
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -1,177 +0,0 @@
import { Algebra, Monad } from './interfaces.js'
/** @import { Applicative, ApplicativeTypeRef, Apply, Chain, Functor, Morphism } from './types.js' */
const Interfaces = Algebra(Monad)
/**
* @template T
* @typedef {Applicative<T> & (Pure<T> | Impure<T>)} Free
*/
/**
* @template T
* @implements {Applicative<T>}
*/
export class Pure extends Interfaces {
#value
/**
* @param {T} value
*/
constructor(value) {
super()
this.#value = value
}
/**
* @template T
* @param {T} value
*/
static of(value) {
return liftF(value)
}
/**
* @template U
* @type {Chain<T>['chain']}
* @param {Morphism<T, Pure<U>>} f
* @returns {Pure<U>}
*/
chain(f) {
return f(this.#value)
}
/**
* @type {Functor<T>['map']}
*/
map(f) {
return this.chain(x => pure(f(x)))
}
/**
* @template U
* @type {Apply<T>['ap']}
* @param {Free<Morphism<T, U>>} b
* @returns {Free<U>}
*/
ap(b) {
return /** @type {Free<U>} */ (b.chain(f =>
/** @type {Chain<U>} */(this.map(f))
))
}
interpret() {
return this.#value
}
toString() {
return `Pure(${this.#value})`
}
}
/**
* @template T
* @implements {Applicative<T>}
*/
export class Impure extends Interfaces {
#value
#next
/**
* @param {T} value
* @param {(value: T) => Free<any>} next
*/
constructor(value, next) {
super()
this.#value = value
this.#next = next
}
/**
* @template T
* @param {T} value
*/
static of(value) {
return liftF(value)
}
/**
* @template U
* @type {Chain<T>['chain']}
* @param {Morphism<T, Free<U>>} f
* @returns {Free<T>}
*/
chain(f) {
return /** @type {Free<T>} */ (impure(
this.#value,
x => /** @type {Free<U>} */(this.#next(x).chain(f))
))
}
/**
* @template U
* @type {Functor<T>['map']}
* @param {Morphism<T, U>} f
* @returns {Free<T>}
*/
map(f) {
return /** @type {Free<T>} */ (impure(
this.#value,
x => /** @type Free<U>} */(this.#next(x).map(f))
))
}
/**
* @template U
* @type {Apply<T>['ap']}
* @param {Free<Morphism<T, U>>} b
* @returns {Free<T>}
*/
ap(b) {
return /** @type {Free<T>} */ (impure(
this.#value,
x => /** @type {Free<U>} */(b.chain(f =>
/** @type {Free<U>} */(this.#next(x).map(f)))
)
))
}
interpret() {
return this.#next(this.#value).interpret()
}
toString() {
return `Impure(${this.#value}, ${this.#next})`
}
}
/**
* @template T
* @param {T} value
*/
export const pure = value => new Pure(value)
/**
* @template T
* @param {T} x
* @param {(value: T) => Free<any>} f
*/
export const impure = (x, f) => new Impure(x, f)
/**
* @template T
* @param {T} value
*/
export const liftF = value => impure(value, pure)
/**
* @template T
* @type {ApplicativeTypeRef<T, Free<T>>}
*/
export const TypeRef = Pure
TypeRef.constructor['of'] = Pure

View file

@ -1,75 +0,0 @@
import { Algebra, Comonad, Monad } from './interfaces.js'
/** @import { Apply, Functor, Morphism } from './types.js' */
/** @template T */
export class Identity extends Algebra(Monad, Comonad) {
#value
/**
* @param {T} value
*/
constructor(value) {
super()
this.#value = value
}
/**
* @template T
* @param {T} value
*/
static of(value) {
return id(value)
}
/**
* @template U
* @type {Functor<T>['map']}
* @param {Morphism<T, U>} f
* @returns {Identity<U>}
*/
map(f) {
return id(f(this.#value))
}
/**
* @template U
* @type {Apply<T>['ap']}
* @param {Apply<Morphism<T, U>>} b
* @returns {Apply<U>}
*/
ap(b) {
return /** @type {Apply<U>} */ (b.map(f => f(this.#value)))
}
/**
* @template U
* @param {Morphism<T, Identity<U>>} f
*/
chain(f) {
return f(this.#value)
}
/**
* @param {(value: Identity<T>) => T} f
*/
extend(f) {
return id(f(this))
}
extract() {
return this.#value
}
toString() {
return `Identity(${this.#value})`
}
}
/**
* @template T
* @param {T} value
*/
export const id = value => new Identity(value)

View file

@ -1,7 +0,0 @@
export { TypeRef as Option, Some, None, some, none } from './option.js'
export { TypeRef as Result, Ok, Err, ok, err } from './result.js'
export { TypeRef as Free, liftF } from './free.js'
export { List, list } from './list.js'
export { IO } from './io.js'
export { Reader } from './reader.js'

View file

@ -1,388 +0,0 @@
import { mix, Mixin } from '../mixin.js'
/** @import { MixinFunction } from '../mixin.js' */
/** @import { Fn } from './types.js' */
export const ProtectedConstructor = Symbol('ProtectedConstructor')
export class ProtectedConstructorError extends Error {
/** @param {string} name */
constructor(name) {
super(`ProtectedConstructorError: ${name} cannot be directly constructed`)
}
}
class Derivations {
#inner
/**
* @param {Iterable<readonly [any, any]>} iterable
*/
constructor(iterable) {
this.#inner = new Map(iterable)
}
/**
* @param {Iterable<any>} keys
* @param {any} value
* @returns {this}
*/
set(keys, value) {
this.#inner.set(new Set(keys), value)
return this
}
/**
* @param {Iterable<any>} keys
* @returns {boolean}
*/
has(keys) {
return this.#inner.has(new Set(keys))
}
/**
* @param {Iterable<any>} keys
*/
get(keys) {
return this.#inner.get(new Set(keys))
}
/**
* @param {Iterable<any>} keys
*/
delete(keys) {
return this.#inner.delete(new Set(keys))
}
}
export class NotImplementedError extends Error {
/** @param {string} name */
constructor(name) {
super(`${name} is not implemented`)
}
}
/** @typedef {(mixin: MixinFunction) => MixinFunction} MixinWrapper */
/**
* @enum {number}
*/
const MethodType = Object.freeze({
Instance: 0,
Static: 1
})
class Method {
#name
/** @type {MethodType} */
#type = MethodType.Instance
/** @type {Fn} */
#implementation
/**
* @param {string | Method} name
*/
constructor(name) {
if (name instanceof Method) {
return name
}
this.#name = name
}
/**
* @param {string | Method} value
*/
static from(value) {
return new Method(value)
}
isInstance() {
this.#type = MethodType.Instance
return this
}
isStatic() {
this.#type = MethodType.Static
return this
}
/** @param {Fn} f */
implementation(f) {
this.#implementation = f
return this
}
/**
* @param {string} interfaceName
*/
_defaultImplementation(interfaceName) {
const err = new NotImplementedError(
`${interfaceName}::${this.#name}`
)
return function() { throw err }
}
/**
* @param {Function} target
*/
_getInstallationPoint(target) {
switch (this.#type) {
case 0: return Object.getPrototypeOf(target)
case 1: return target
default: return target
}
}
/**
* @param {Interface} builder
* @param {Function} target
*/
implement(builder, target) {
const impl = this.#implementation || this._defaultImplementation(builder.name)
this._getInstallationPoint(target)[this.#name] = impl
}
}
const Implementations = Symbol()
class Interface {
#name
/** @type {MixinFunction} */
#mixin
/** @type {Set<Method>} */
#methods = new Set()
/** @type {Set<Interface>} */
#interfaces = new Set()
/** @param {string} name */
constructor(name) {
this.#name = name
this.#mixin = Mixin(this.build.bind(this))
}
get name() {
return this.#name
}
findInterfaces(type) {
let interfaces = new Set()
let current = type
while (current != null) {
interfaces = new Set(current[Implementations]).union(interfaces)
current = Object.getPrototypeOf(current)
}
return interfaces
}
/**
* @param {{ name: string }} type
* @returns {boolean}
*/
implementedBy(type) {
return this.findInterfaces(type).has(this)
}
/**
* @param {...(PropertyKey | Method)} methods
* @returns {this}
*/
specifies(...methods) {
this.#methods = new Set(
methods
.map(Method.from)
.concat(...this.#methods)
)
return this
}
/**
* @param {...Interface} interfaces
* @returns {this}
*/
extends(...interfaces) {
this.#interfaces = new Set(interfaces.concat(...this.#interfaces))
return this
}
/**
* @returns {MixinFunction}
*/
asMixin() {
return this.#mixin
}
/**
* @param {FunctionConstructor} base
*/
build(base) {
const interfaces = [...this.#interfaces.values()]
const mixins = interfaces.map(x => x.asMixin())
const Interfaces = mix(base).with(...mixins)
const Algebra = class extends Interfaces { }
for (const method of this.#methods) {
method.implement(this, Algebra)
}
const prototype = Object.getPrototypeOf(Algebra)
prototype[Implementations] = new Set(interfaces)
return Algebra
}
}
export class BaseSet { }
/**
* @param {PropertyKey} key
* @param {object} obj
* @returns {boolean}
*/
export const hasOwn = (key, obj) => Object.hasOwn(obj, key)
/**
* @param {Function} left
* @param {Function} right
*/
export const EitherOf = (left, right) => mix(left).with(l => right(l))
/**
* @param {string} methodName
* @param {(...args: any[]) => any} f
* @param {object} obj
*/
export const apply = (methodName, f, obj) => {
if (hasOwn(methodName, obj)) {
return obj[methodName](f)
} else {
return f(obj)
}
}
/**
* @param {Function} base
* @returns {(...algebras: Interface[]) => FunctionConstructor}
*/
export const AlgebraWithBase = base => (...algebras) => {
return mix(base).with(...algebras.map(x => x.asMixin()))
}
/**
* @param {...Interface} algebras
* @returns {FunctionConstructor}
*/
export const Algebra = AlgebraWithBase(BaseSet)
export const Setoid = new Interface('Setoid')
.specifies('equals')
export const Ord = new Interface('Ord')
.specifies('lte')
export const Semigroupoid = new Interface('Semigroupoid')
.specifies('compose')
export const Category = new Interface('Category')
.extends(Semigroupoid)
.specifies(
Method.from('id').isStatic()
)
export const Semigroup = new Interface('Semigroup')
.specifies('concat')
export const Monoid = new Interface('Monoid')
.extends(Semigroup)
.specifies(
Method.from('empty').isStatic()
)
export const Group = new Interface('Group')
.extends(Monoid)
.specifies('invert')
export const Filterable = new Interface('Filterable')
.specifies('filter')
export const Functor = new Interface('Functor')
.specifies('map')
export const Contravariant = new Interface('Contravariant')
.specifies('contramap')
export const Apply = new Interface('Apply')
.extends(Functor)
.specifies('ap')
export const Applicative = new Interface('Applicative')
.extends(Apply)
.specifies(
Method.from('of').isStatic()
)
export const Alt = new Interface('Alt')
.extends(Functor)
.specifies('alt')
export const Plus = new Interface('Plus')
.extends(Alt)
.specifies(
Method.from('zero').isStatic()
)
export const Alternative = new Interface('Alternative')
.extends(Applicative, Plus)
export const Foldable = new Interface('Foldable')
.specifies('fold')
export const Traversable = new Interface('Traversable')
.extends(Functor, Foldable)
.specifies('traverse')
export const Chain = new Interface('Chain')
.extends(Apply)
.specifies(
Method.from('chain')
.implementation(function(f) {
return f(this._value)
})
)
export const ChainRef = new Interface('ChainRec')
.extends(Chain)
.specifies(
Method.from('chainRec').isStatic()
)
export const Monad = new Interface('Monad')
.extends(Applicative, Chain)
export const Extend = new Interface('Extend')
.extends(Functor)
.specifies('extend')
export const Comonad = new Interface('Comonad')
.extends(Extend)
.specifies('extract')
export const Bifunctor = new Interface('Bifunctor')
.extends(Functor)
.specifies('bimap')
export const Profunctor = new Interface('Profunctor')
.extends(Functor)
.specifies('promap')

View file

@ -1,63 +0,0 @@
import { Algebra, Monad } from './interfaces.js'
/** @import { Apply, Chain, Fn, Functor, Morphism } from './types.js' */
/** @template {Fn} T */
export class IO extends Algebra(Monad) {
_effect
/**
* @param {T} effect
*/
constructor(effect) {
super()
this._effect = effect
}
/**
* @template {Fn} T
* @param {T} a
*/
static of(a) {
return new IO(() => a)
}
/**
* @template {Fn} U
* @type {Chain<T>['chain']}
* @param {Morphism<T, IO<U>>} f
* @returns {IO<U>}
*/
chain(f) {
return /** @type {IO<U>} */ (IO.of(() => f(this.run()).run()))
}
/**
* @template {Fn} U
* @type {Functor<T>['map']}
* @param {Morphism<T, U>} f
* @returns {IO<U>}
*/
map(f) {
return /** @type {IO<U>} */ (IO.of(() => f(this.run())))
}
/**
* @template {Fn} U
* @type {Apply<T>['ap']}
* @param {IO<Morphism<T, U>>} other
* @returns {IO<U>}
*/
ap(other) {
return /** @type {IO<U>} */ (IO.of((() => other.run()(this.run()))))
}
run() {
return this._effect()
}
toString() {
return `IO(${this._effect})`
}
}

View file

@ -1,252 +0,0 @@
import { Algebra, Comonad, Foldable, Monoid, Semigroup } from './interfaces.js'
/** @import { Apply, Chain, Foldable as FoldableT, Functor, Morphism, Semigroup as SemigroupT } from './types.js' */
const Interfaces = Algebra(Semigroup, Foldable, Monoid, Comonad)
/**
* @template T
* @typedef {Element<T> | Empty<T>} List
*/
/** @template T */
class Empty extends Interfaces {
constructor() {
super()
}
/**
* @template U
* @type {Chain<T>['chain']}
* @this {Empty<U>}
* @param {Morphism<T, List<U>>} _f
* @returns {List<U>}
*/
chain(_f) {
return /** @type {List<U>} */ (this)
}
/**
* @template U
* @type {Functor<T>['map']}
* @this {Empty<U>}
* @param {Morphism<T, U>} _f
* @returns {List<U>}
*/
map(_f) {
return this
}
/**
* @template U
* @type {Apply<T>['ap']}
* @this {Empty<U>}
* @param {List<Morphism<T, U>>} _b
* @returns {List<U>}
*/
ap(_b) {
return /** @type {List<U>} */ (this)
}
/**
* @type {SemigroupT<T>['concat']}
*/
concat(b) {
return b
}
/**
* @type {FoldableT<T>['reduce']}
*/
reduce(_f, acc) {
return acc
}
count() {
return 0
}
/** @returns {this is Empty<T>} */
isEmpty() {
return true
}
toString() {
return `List(Empty)`
}
}
/** @template T */
class Element extends Interfaces {
#head
#tail
/** @type {List<T>} */
#cache
/**
* @param {T} head
* @param {() => List<T>} [tail]
*/
constructor(head, tail = List.empty) {
super()
this.#head = head
this.#tail = tail
this.#cache = null
}
/**
* @template U
* @type {Chain<T>['chain']}
* @this {Element<T>}
* @param {Morphism<T, List<U>>} f
* @returns {List<U>}
*/
chain(f) {
return /** @type {List<U>} */ (f(this.#head).concat(
/** @type {never} */(this.tail().chain(f))
))
}
/**
* @template U
* @type {Functor<T>['map']}
* @param {Morphism<T, U>} f
* @returns {List<U>}
*/
map(f) {
return new Element(
f(this.#head),
() => /** @type {List<U>} */(this.tail().map(f))
)
}
/**
* @template U
* @type {Apply<T>['ap']}
* @this {Element<T>}
* @param {List<Morphism<T, U>>} b
* @returns {List<U>}
*/
ap(b) {
if (b.isEmpty()) {
return List.empty()
}
const head = /** @type {List<U>} */ (this.map(b.head()))
const rest = /** @type {List<U>} */ (this.ap(b.tail()))
return /** @type {List<U>} */ (head.concat(rest))
}
/**
* @type {SemigroupT<T>['concat']}
*/
concat(b) {
return new Element(
this.#head,
() => /** @type {List<T>} */(this.tail().concat(b))
)
}
/**
* @type {FoldableT<T>['reduce']}
*/
reduce(f, acc) {
return this.tail().reduce(f, f(acc, this.#head))
}
head() {
return this.#head
}
tail() {
return this.#cache || (this.#cache = this.#tail())
}
count() {
return this.tail().count() + 1
}
/** @returns {this is Empty<T>} */
isEmpty() {
return false
}
toArray() {
return this.reduce(
reduceArray,
[]
)
}
toString() {
return `List(${this.toArray()})`
}
}
class TypeRef {
/**
* @template T
* @param {T} value
* @returns {List<T>}
*/
static of(value) {
return new Element(value)
}
/**
* @template T
* @param {Iterable<T>} iterable
* @returns {List<T>}
*/
static from(iterable) {
const iterator = Iterator.from(iterable)
return List.fromIterator(iterator)
}
/**
* @template T
* @param {Iterator<T>} iterator
* @returns {List<T>}
*/
static fromIterator(iterator) {
const next = iterator.next()
if (next.done) {
return List.empty()
} else {
return new Element(next.value, () => List.fromIterator(iterator))
}
}
/**
* @template T
* @param {T} head
* @param {List<T>} tail
* @returns {List<T>}
*/
static cons(head, tail) {
return new Element(head, () => tail)
}
static empty() {
return empty
}
}
/**
* @template T
* @param {T[]} acc
* @param {T} x
* @returns {T[]}
*/
const reduceArray = (acc, x) => acc.concat(x)
export const List = TypeRef
const empty = new Empty()
/**
* @template T
* @param {T} value
*/
export const list = value => List.of(value)

View file

@ -1,174 +0,0 @@
import { id, Identity } from './identity.js'
import { Algebra, Alternative, Foldable, Monad, Setoid } from './interfaces.js'
/** @import { Alt, Apply, Chain, Foldable as FoldableT, Functor, Morphism, Setoid as SetoidT } from './types.js' */
const Interfaces = Algebra(Setoid, Alternative, Monad, Foldable)
/**
* @template T
* @typedef {Some<T> | None<T>} Option
*/
/** @template T */
export class Some extends Interfaces {
/** @type {T} */
#value
/** @param {T} value */
constructor(value) {
super()
this.#value = value
}
/**
* @type {SetoidT<T>['equals']}
* @param {Some<T>} other
*/
equals(other) {
if (other instanceof Some) { return false }
const eq = /** @type {Some<T>} */ (other).chain(v => id(v === this.#value))
return /** @type {Identity<boolean>} */ (eq).extract()
}
/**
* @template U
* @type {Apply<T>['ap']}
* @param {Some<Morphism<T, U>>} b
* @returns {Some<U>}
*/
ap(b) {
return /** @type {Some<U>} */ (b.chain(f =>
/** @type {Some<U>} */(this.map(f))
))
}
/**
* @type {Alt<T>['alt']}
*/
alt(b) {
return this
}
/**
* @type {Chain<T>['chain']}
*/
chain(f) {
return f(this.#value)
}
/**
* @template U
* @type {Functor<T>['map']}
* @param {Morphism<T, U>} f
* @returns {Some<U>}
*/
map(f) {
return /** @type {Some<U>} */ (this.chain(v => some(f(v))))
}
/**
* @type {Functor<T>['map']}
*/
then(f) {
return this.map(f)
}
/**
* @template U
* @type {FoldableT<T>['reduce']}
* @param {(acc: U, value: T) => U} f
* @param {U} init
* @returns {U}
*/
reduce(f, init) {
return f(init, this.#value)
}
toString() {
return `Some(${this.#value})`
}
}
/** @template T */
export class None extends Interfaces {
/**
* @type {SetoidT<T>['equals']}
*/
equals(other) {
return other === none
}
/**
* @type {Apply<T>['ap']}
* @returns {this}
*/
ap(_b) {
return this
}
/**
* @type {Alt<T>['alt']}
*/
alt(b) {
return b
}
/**
* @template U
* @type {Chain<T>['chain']}
* @param {Morphism<T, U>} _f
* @returns {this}
*/
chain(_f) {
return this
}
/**
* @template U
* @type {Functor<T>['map']}
* @param {Morphism<T, U>} _f
* @returns {this}
*/
map(_f) {
return this
}
/**
* @template R
* @type {Functor<T>['map']}
* @param {Morphism<T, R>} _f
* @returns {None}
*/
then(_f) {
return this
}
/**
* @template U
* @type {FoldableT<T>['reduce']}
* @param {(acc: U, value: T) => U} _f
* @param {U} init
* @returns {U}
*/
reduce(_f, init) {
return init
}
toString() {
return 'None'
}
}
/**
* @template T
* @param {T} value
*/
export const some = value => new Some(value)
export const none = new None()
export const TypeRef = some
TypeRef.constructor['of'] = some
TypeRef.constructor['zero'] = none

View file

@ -1,88 +0,0 @@
import { id } from 'izuna'
import { Algebra, Monad } from './interfaces.js'
/** @import { ApplicativeTypeRef, Apply, Chain, Functor, InferredMorphism, Morphism } from './types.js' */
/**
* @template T
* @typedef {InferredMorphism<T>} ReaderFn
*/
/** @template T */
export class Reader extends Algebra(Monad) {
#run
/** @param {InferredMorphism<T>} run */
constructor(run) {
super()
this.#run = run
}
/**
* @template T
* @type {ApplicativeTypeRef<T, Reader<T>>['of']}
* @param {T} a
* @returns {Reader<T>}
*/
static of(a) {
return new Reader(/** @type {InferredMorphism<T>} */(_env => a))
}
/** @template T */
static ask() {
return new Reader(/** @type {InferredMorphism<T>} */(id))
}
/**
* @type {Functor<T>['map']}
* @param {InferredMorphism<T>} f
* @returns {Reader<T>}
*/
map(f) {
return /** @type {Reader<any>} */ (this.chain(value => Reader.of(f(value))))
}
/**
* @type {Chain<T>['chain']}
* @param {InferredMorphism<T>} f
* @returns {Reader<T>}
*/
chain(f) {
return new Reader(env => {
const result = this.#run(env)
const next = f(result)
return next.run(env)
})
}
/**
* @template U
* @type {Apply<T>['ap']}
* @param {Reader<Morphism<T, U>>} b
* @returns {Reader<U>}
*/
ap(b) {
return /** @type {Reader<U>} */ (b.chain(f =>
/** @type {Reader<U>} */(this.map(f))
))
}
/**
* @param {InferredMorphism<T>} f
* @returns {Reader<T>}
*/
local(f) {
return new Reader(/** @type {InferredMorphism<T>} */(env => this.#run(f(env))))
}
/**
* @template U
* @param {T} env
* @returns {U}
*/
run(env) {
return this.#run(env)
}
}

View file

@ -1,287 +0,0 @@
import { id, Identity } from './identity.js'
import { Algebra, Alternative, Bifunctor, Foldable, Monad, Setoid } from './interfaces.js'
/** @import { Alt, Apply, Chain, Functor, Bifunctor as BifunctorT, Foldable as FoldableT, Morphism, Setoid as SetoidT } from './types.js' */
const Interfaces = Algebra(Setoid, Alternative, Monad, Foldable, Bifunctor)
/**
* @template T, E
* @typedef {Ok<T, E> | Err<T, E>} Result
*/
/**
* @template T, E
*/
export class Ok extends Interfaces {
/** @type {T} */
#value
/**
* @param {T} value
* @constructs {Ok<T, E>}
*/
constructor(value) {
super()
this.#value = value
}
/**
* @type {SetoidT<T>['equals']}
* @param {Result<T, E>} other
* @returns {boolean}
*/
equals(other) {
if (!(other instanceof Ok)) { return false }
const eq = other.chain(v => (id(v === this.#value)))
return /** @type {Identity<boolean>} */ (eq).extract()
}
/** @returns {this is Ok<T, E>} */
isOk() { return true }
/** @returns {this is Err<T, E>} */
isErr() { return false }
/**
* @type {Chain<T>['chain']}
*/
chain(f) {
return f(this.#value)
}
/**
* @template E2
* @type {Chain<E>['chain']}
* @param {Morphism<E, E2>} _f
* @returns {this}
*/
chainErr(_f) {
return this
}
/**
* @template U
* @type {Functor<T>['map']}
* @param {Morphism<T, U>} f
* @returns {Functor<U>}
*/
map(f) {
return this.chain(v => ok(f(v)))
}
/**
* @template E2
* @type {Functor<E>['map']}
* @this {Result<T, E>}
* @param {Morphism<E, E2>} _f
* @returns {Result<T, E2>}
*/
mapErr(_f) {
return /** @type {never} */ (this)
}
/**
* @template U
* @type {Apply<T>['ap']}
* @param {Apply<Morphism<T, U>>} b
* @returns {Result<U, E>}
*/
ap(b) {
return /** @type {Result<U, E>} */ (this.chain(v =>
/** @type {Chain<U>} */(b.map(f => f(v)))
))
}
/**
* @type Alt<T>['alt']
*/
alt(_b) {
return this
}
/**
* @template U
* @borrows {Result~map}
* @param {Morphism<T, U>} f
*/
then(f) {
return this.map(f)
}
/**
* @template R
* @param {Morphism<E, R>} _f
* @returns {this}
*/
catch(_f) {
return this
}
/**
* @template U
* @type {FoldableT<T>['reduce']}
* @param {(acc: U, value: T) => U} f
* @param {U} init
* @returns {U}
*/
reduce(f, init) {
return f(init, this.#value)
}
/**
* @template T2, E2
* @type {BifunctorT<T, E>['bimap']}
* @param {Morphism<T, T2>} f
* @param {Morphism<E, E2>} _g
* @returns {Result<T2, E2>}
*/
bimap(f, _g) {
return /** @type {Result<T2, E2>} */ (this.map(f))
}
toString() {
return `Ok(${this.#value})`
}
}
/**
* @template T, E
*/
export class Err extends Interfaces {
/** @type {E} */
#value
/**
* @param {E} value
* @constructs {Err<T, E>}
*/
constructor(value) {
super()
this.#value = value
}
/**
* @type {SetoidT<T>['equals']}
* @param {Err<T, E>} other
* @returns {boolean}
*/
equals(other) {
if (!(other instanceof Err)) { return false }
const eq = other.chainErr(v => id(v === this.#value))
return /** @type {Identity<boolean>} */ (eq).extract()
}
/** @returns {this is Ok<T, E>} */
isOk() { return false }
/** @returns {this is Err<T, E>} */
isErr() { return true }
/**
* @type {Chain<T>['chain']}
* @returns {this}
*/
chain(_f) {
return this
}
/**
* @template E2
* @type {Chain<E>['chain']}
* @param {Morphism<E, Result<T, E2>>} f
* @returns {Result<T, E2>}
*/
chainErr(f) {
return f(this.#value)
}
/**
* @type {Functor<T>['map']}
* @returns {this}
*/
map(_f) {
return this
}
/**
* @template E2
* @type {Functor<E>['map']}
* @param {Morphism<E, E2>} f
* @returns {Result<T, E2>}
*/
mapErr(f) {
return /** @type {Result<T, E2>} */ (this.bimap(id, f))
}
/**
* @type {Functor<T>['map']}
* @returns {this}
*/
then(_f) {
return this
}
/**
* @template R
* @type {Functor<E>['map']}
* @param {Morphism<E, R>} f
* @returns {Err<T, R>}
*/
catch(f) {
return new Err(f(this.#value))
}
/**
* @type Alt<T>['alt']
*/
alt(b) {
return b
}
/**
* @template U
* @type {FoldableT<T>['reduce']}
* @param {(acc: U, value: T) => U} _f
* @param {U} init
* @returns {U}
*/
reduce(_f, init) {
return init
}
/**
* @template T2, E2
* @type {BifunctorT<T, E>['bimap']}
* @param {Morphism<T, T2>} _f
* @param {Morphism<E, E2>} g
* @returns {Result<T2, E2>}
*/
bimap(_f, g) {
return /** @type {Result<T2, E2>} */ (err(g(this.#value)))
}
toString() {
return `Err(${this.#value})`
}
}
/**
* @template T, E
* @param {T} v
* @returns {Ok<T, E>}
*/
export const ok = v => new Ok(v)
/**
* @template T, E
* @param {E} e
* @returns {Err<T, E>}
*/
export const err = e => new Err(e)
export const TypeRef = ok
TypeRef.constructor['of'] = ok
TypeRef.constructor['zero'] = err

View file

@ -1,222 +0,0 @@
export default {}
/**
* @template T, R
* @typedef {(value: T) => R} Morphism
*/
/**
* @template T
* @typedef {<R>(value: T) => R} InferredMorphism
*/
/**
* @template T
* @typedef {(value: T) => boolean} Predicate
*/
/**
* @template T
* @typedef {{
equals: (b: Setoid<T>) => boolean
* }} Setoid
*/
/**
* @template T
* @typedef {{
lte: (b: Ord<T>) => boolean
* }} Ord
*/
/**
* @template T, U
* @typedef {*} Semigroupoid
*/
/**
* @template I, J
* @typedef {{
compose: <K>(b: Semigroupoid<J, K>) => Semigroupoid<I, K>
* }} Category
*/
/**
* @template T, U
* @template {Category<T, U>} M
* @typedef {{
id: () => (value: T) => M
* }} CategoryTypeRef
*/
/**
* @template T
* @typedef {{
concat: (b: Semigroup<T>) => Semigroup<T>
* }} Semigroup
*/
/**
* @template T
* @typedef{Semigroup<T>} Monoid
*/
/**
* @template T
* @template {Monoid<T>} M
* @typedef {{
empty: () => M
* }} MonoidTypeRef
*/
/**
* @template T
* @typedef {
Monoid<T> &
{ invert: () => Group<T> }
* } Group
*/
/**
* @template T
* @typedef {{
filter: <U>(f: (acc: U, val: T) => U, init: U) => U
* }} Filterable
*/
/**
* @template T
* @typedef {{
map: <U>(f: Morphism<T, U>) => Functor<U>
* }} Functor
*/
/**
* @template T
* @typedef {{
contramap: <U>(f: Morphism<U, T>) => Contravariant<T>
* }} Contravariant
*/
/**
* @template T
* @typedef {
Functor<T> &
{ ap: <U>(f: Apply<Morphism<T, U>>) => Apply<U> }
* } Apply
*/
/**
* @template T
* @typedef {
Functor<T> &
Apply<T>
* } Applicative
*/
/**
* @template T
* @template {Applicative<T>} M
* @typedef {{
of: (value: T) => M
* }} ApplicativeTypeRef
*/
/**
* @template T
* @typedef {
Functor<T> &
{ alt: (b: Alt<T>) => Alt<T> }
* } Alt
*/
/**
* @template T
* @typedef {Alt<T>} Plus
*/
/**
* @template T
* @template {Plus<T>} M
* @typedef {
Alt<T> &
{ zero: () => M }
* } PlusTypeRef
*/
/**
* @template T
* @typedef {Applicative<T> & Plus<T>} Alternative
*/
/**
* @template T
* @template {Applicative<T> & Plus<T>} M
* @typedef {ApplicativeTypeRef<T, M> & PlusTypeRef<T, M>} AlternativeTypeRef
*/
/**
* @template T
* @typedef {{
reduce: <U>(f: (acc: U, val: T) => U, init: U) => U
* }} Foldable
*/
/**
* @template T
* @typedef {
Functor<T> & Foldable<T> &
{ traverse: <U>(A: ApplicativeTypeRef<U, Applicative<U>>, f: (val: T) => Applicative<U>) => Applicative<Traversable<U>> }
* } Traversable
*/
/**
* @template T
* @typedef {
Apply<T> &
{ chain: <U>(f: (value: T) => Chain<U>) => Chain<U> }
* } Chain
*/
/**
* @template T
* @typedef {Chain<T>} ChainRec
*/
/**
* @template T
* @typedef {Functor<T> & Applicative<T> & Chain<T>} Monad
*/
/**
* @template T
* @template {Monad<T>} M
* @typedef {ApplicativeTypeRef<T, M>} MonadTypeDef
*/
/**
* @template T
* @typedef {
Functor<T> &
{ extend: <U>(f: (val: Extend<T>) => U) => Extend<U>}
* } Extend
*/
/**
* @template T
* @typedef {
Extend<T> &
{ extract: () => T }
* } Comonad<T>
*/
/**
* @template A, X
* @typedef {
Functor<A> &
{ bimap: <B, Y>(f: Morphism<A, B>, g: Morphism<X, Y>) => Bifunctor<B, Y> }
* } Bifunctor
*/
/** @typedef {(...args: any[]) => any} Fn */

View file

@ -1,17 +0,0 @@
/**
* @template T, U
* @param {(acc: U, value: T) => U} f
* @param {U} acc
*/
export const Reducer = (f, acc) => {
function Const(value) {
this.value = value
}
Const.of = function() { return new Const(acc) }
Const.prototype.map = function() { return this }
Const.prototype.ap = function(b) {
return new Const(f(b.value, this.value))
}
return Const
}

8
src/alias.js Normal file
View file

@ -0,0 +1,8 @@
import { Method } from "./specification/index.js"
export const alias = (type, aliases) => {
Object.entries(aliases).forEach(([method, impl]) => {
Method.from(impl).bind(type, method)
})
}

7
src/common.js Normal file
View file

@ -0,0 +1,7 @@
/**
* @template T
* @param {T} value
* @returns {T}
*/
export const id = value => value

8
src/error.js Normal file
View file

@ -0,0 +1,8 @@
export class UnwrapError extends Error {
name = 'UnwrapError'
constructor(desc) {
super(`UnwrapError: ${desc}`)
}
}

View file

@ -1,2 +0,0 @@
export * from './algebra/index.js'

View file

@ -1,256 +0,0 @@
/**
* mixwith.js
* Author: Justin Fagnani (https://github.com/justinfagnani/)
* https://github.com/justinfagnani/mixwith.js
*/
'use strict'
// used by apply() and isApplicationOf()
const _appliedMixin = '__mixwith_appliedMixin'
/**
* A function that returns a subclass of its argument.
*
* @example
* const M = (superclass) => class extends superclass {
* getMessage() {
* return "Hello"
* }
* }
*
* @typedef {Function} MixinFunction
* @param {Function} superclass
* @return {Function} A subclass of `superclass`
*/
/**
* Applies `mixin` to `superclass`.
*
* `apply` stores a reference from the mixin application to the unwrapped mixin
* to make `isApplicationOf` and `hasMixin` work.
*
* This function is usefull for mixin wrappers that want to automatically enable
* {@link hasMixin} support.
*
* @example
* const Applier = (mixin) => wrap(mixin, (superclass) => apply(superclass, mixin))
*
* // M now works with `hasMixin` and `isApplicationOf`
* const M = Applier((superclass) => class extends superclass {})
*
* class C extends M(Object) {}
* let i = new C()
* hasMixin(i, M) // true
*
* @function
* @param {Function} superclass A class or constructor function
* @param {MixinFunction} mixin The mixin to apply
* @return {Function} A subclass of `superclass` produced by `mixin`
*/
export const apply = (superclass, mixin) => {
let application = mixin(superclass)
application.prototype[_appliedMixin] = unwrap(mixin)
return application
}
/**
* Returns `true` iff `proto` is a prototype created by the application of
* `mixin` to a superclass.
*
* `isApplicationOf` works by checking that `proto` has a reference to `mixin`
* as created by `apply`.
*
* @function
* @param {Object} proto A prototype object created by {@link apply}.
* @param {MixinFunction} mixin A mixin function used with {@link apply}.
* @return {boolean} whether `proto` is a prototype created by the application of
* `mixin` to a superclass
*/
export const isApplicationOf = (proto, mixin) =>
proto.hasOwnProperty(_appliedMixin) && proto[_appliedMixin] === unwrap(mixin)
/**
* Returns `true` iff `o` has an application of `mixin` on its prototype
* chain.
*
* @function
* @param {Object} o An object
* @param {MixinFunction} mixin A mixin applied with {@link apply}
* @return {boolean} whether `o` has an application of `mixin` on its prototype
* chain
*/
export const hasMixin = (o, mixin) => {
while (o != null) {
if (isApplicationOf(o, mixin)) return true
o = Object.getPrototypeOf(o)
}
return false
}
// used by wrap() and unwrap()
const _wrappedMixin = '__mixwith_wrappedMixin'
/**
* Sets up the function `mixin` to be wrapped by the function `wrapper`, while
* allowing properties on `mixin` to be available via `wrapper`, and allowing
* `wrapper` to be unwrapped to get to the original function.
*
* `wrap` does two things:
* 1. Sets the prototype of `mixin` to `wrapper` so that properties set on
* `mixin` inherited by `wrapper`.
* 2. Sets a special property on `mixin` that points back to `mixin` so that
* it can be retreived from `wrapper`
*
* @function
* @param {MixinFunction} mixin A mixin function
* @param {MixinFunction} wrapper A function that wraps {@link mixin}
* @return {MixinFunction} `wrapper`
*/
export const wrap = (mixin, wrapper) => {
Object.setPrototypeOf(wrapper, mixin)
if (!mixin[_wrappedMixin]) {
mixin[_wrappedMixin] = mixin
}
return wrapper
}
/**
* Unwraps the function `wrapper` to return the original function wrapped by
* one or more calls to `wrap`. Returns `wrapper` if it's not a wrapped
* function.
*
* @function
* @param {MixinFunction} wrapper A wrapped mixin produced by {@link wrap}
* @return {MixinFunction} The originally wrapped mixin
*/
export const unwrap = (wrapper) => wrapper[_wrappedMixin] || wrapper
const _cachedApplications = '__mixwith_cachedApplications'
/**
* Decorates `mixin` so that it caches its applications. When applied multiple
* times to the same superclass, `mixin` will only create one subclass, memoize
* it and return it for each application.
*
* Note: If `mixin` somehow stores properties its classes constructor (static
* properties), or on its classes prototype, it will be shared across all
* applications of `mixin` to a super class. It's reccomended that `mixin` only
* access instance state.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap with caching behavior
* @return {MixinFunction} a new mixin function
*/
export const Cached = (mixin) => wrap(mixin, (superclass) => {
// Get or create a symbol used to look up a previous application of mixin
// to the class. This symbol is unique per mixin definition, so a class will have N
// applicationRefs if it has had N mixins applied to it. A mixin will have
// exactly one _cachedApplicationRef used to store its applications.
let cachedApplications = superclass[_cachedApplications]
if (!cachedApplications) {
cachedApplications = superclass[_cachedApplications] = new Map()
}
let application = cachedApplications.get(mixin)
if (!application) {
application = mixin(superclass)
cachedApplications.set(mixin, application)
}
return application
})
/**
* Decorates `mixin` so that it only applies if it's not already on the
* prototype chain.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap with deduplication behavior
* @return {MixinFunction} a new mixin function
*/
export const DeDupe = (mixin) => wrap(mixin, (superclass) =>
(hasMixin(superclass.prototype, mixin))
? superclass
: mixin(superclass))
/**
* Adds [Symbol.hasInstance] (ES2015 custom instanceof support) to `mixin`.
*
* @function
* @param {MixinFunction} mixin The mixin to add [Symbol.hasInstance] to
* @return {MixinFunction} the given mixin function
*/
export const HasInstance = (mixin) => {
if (Symbol && Symbol.hasInstance && !mixin[Symbol.hasInstance]) {
Object.defineProperty(mixin, Symbol.hasInstance, {
value(o) {
return hasMixin(o, mixin)
},
})
}
return mixin
}
/**
* A basic mixin decorator that applies the mixin with {@link apply} so that it
* can be used with {@link isApplicationOf}, {@link hasMixin} and the other
* mixin decorator functions.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap
* @return {MixinFunction} a new mixin function
*/
export const BareMixin = (mixin) => wrap(mixin, (s) => apply(s, mixin))
/**
* Decorates a mixin function to add deduplication, application caching and
* instanceof support.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap
* @return {MixinFunction} a new mixin function
*/
export const Mixin = (mixin) => DeDupe(Cached(BareMixin(mixin)))
/**
* A fluent interface to apply a list of mixins to a superclass.
*
* ```javascript
* class X extends mix(Object).with(A, B, C) {}
* ```
*
* The mixins are applied in order to the superclass, so the prototype chain
* will be: X->C'->B'->A'->Object.
*
* This is purely a convenience function. The above example is equivalent to:
*
* ```javascript
* class X extends C(B(A(Object))) {}
* ```
*
* @function
* @param {Function} [superclass=Object]
* @return {MixinBuilder}
*/
export const mix = (superclass) => new MixinBuilder(superclass)
class MixinBuilder {
constructor(superclass) {
this.superclass = superclass || class { }
}
/**
* Applies `mixins` in order to the superclass given to `mix()`.
*
* @param {Array.<MixinFunction>} mixins
* @return {FunctionConstructor} a subclass of `superclass` with `mixins` applied
*/
with(...mixins) {
return mixins.reduce((c, m) => m(c), this.superclass)
}
}

450
src/option.js Normal file
View file

@ -0,0 +1,450 @@
import { id } from './common.js'
import { UnwrapError } from './error.js'
import { Result } from './result.js'
import { implementor } from './specification/index.js'
import { Alt, Applicative, Chain, Filterable, Functor, Ord, Setoid } from './specification/structures.js'
import { FantasyLand } from './specification/fantasy-land.js'
export class Option {
/**
* @template T
* @param {T} value
* @returns {Some<T>}
*/
static some(value) {
return new Some(value)
}
/** @returns {None} */
static none() {
return None
}
/**
* @param {(value: T) => Option<T>} fn
* @param {Option<T>} self
* @returns {Option<T>}
*/
static chain(fn, self) {
return self.bind(fn)
}
/**
* @param {(value: T) => bool} fn
* @param {Option<T>} self
* @returns {Option<T>}
*/
static filter(predicate, self) {
return self.filter(predicate)
}
/**
* @template V
* @param {(value: T) => V} fn
* @param {Option<T>} self
* @returns {Option<V>}
*/
static map(fn, self) {
return self.map(fn)
}
/**
* @param {Option<T>} other
* @param {Option<T>} self
* @returns {Option<T>}
*/
static alt(other, self) {
return self.alt(other)
}
/**
* @param {Option<T>} other
* @param {Option<T>} self
* @returns {Option<T>}
*/
static equals(other, self) {
return self.equals(other)
}
/**
* @param {Option<T>} other
* @param {Option<T>} self
* @returns {Option<T>}
*/
static lte(other, self) {
return self.lte(other)
}
}
/** @template T */
export class Some extends Option {
/** @type T */
#value
/** @param {T} value */
constructor(value) {
super()
this.#value = value
}
/**
* @returns {this is Some}
*/
isSome() {
return true
}
/**
* @returns {this is _None}
*/
isNone() {
return false
}
/**
* @param {(value: T) => Option<T>} fn
* @returns {Option<T>}
*/
bind(fn) {
return fn(this.#value)
}
/**
* @template V
* @param {(value: T) => V} fn
* @returns {Option<V>}
*/
map(fn) {
return new Some(fn(this.#value))
}
/**
* @param {Option<T>} other
* @returns {Option<T>}
*/
alt(_other) {
return this
}
/**
* @param {Some<T>} other
* @returns {Some<T>}
*/
and(other) {
return other
}
/**
* @param {() => Option<T>} other
* @returns {Option<T>}
*/
orElse(_fn) {
return this
}
/**
* @template T
* @param {(value: T) => bool} predicate
* @returns {Option<T>}
*/
filter(predicate) {
return predicate(this.#value) ? this : None
}
/**
* @param {Option<T>} other
* @returns {bool}
*/
equals(other) {
return other.isSome() && this.#value === other.#value
}
/**
* @param {Option<T>} other
* @returns {bool}
*/
lte(other) {
return other.isSome() && this.#value <= other.#value
}
/**
* @template [V=T]
* @param {(acc: V, value: T) => V} reducer
* @param {V} init
* @returns {V}
*/
reduce(reducer, init) {
return reducer(init, this.#value)
}
/**
* @returns {Option<T>}
*/
flatten() {
if (this.#value instanceof Option) {
return this.bind(id)
} else {
return this
}
}
/**
* @template E
* @param {E} _err
* @returns {Result<T, E>}
*/
okOr(_err) {
return Result.ok(this.#value)
}
/**
* @template E
* @template {() => E} F
* @param {F} _err
* @returns {Result<T, E>}
*/
okOrElse(_err) {
return Result.ok(this.#value)
}
/**
* @param {(value: T) => void} fn
* @returns {this}
*/
inspect(fn) {
fn(this.#value)
return this
}
/**
* @returns {T}
* @throws {UnwrapError}
*/
unwrap() {
return this.#value
}
/**
* @param {T} _value
* @returns {T}
*/
unwrapOr(_value) {
return this.unwrap()
}
/**
* @param {() => T} _fn
* @returns {T}
*/
unwrapOrElse(_fn) {
return this.unwrap()
}
/** @returns {string} */
toString() {
return `Some(${this.#value})`
}
}
class _None extends Option {
/**
* @returns {this is Some}
*/
isSome() {
return false
}
/**
* @returns {this is _None}
*/
isNone() {
return true
}
/**
* @template T
* @template {Option<T>} R
* @param {(value: T) => R} fn
* @returns {R}
*/
bind(_fn) {
return this
}
/**
* @template V
* @template {Option<T>} R
* @param {(value: T) => V} fn
* @returns {R}
*/
map(_fn) {
return this
}
/**
* @template T
* @param {Option<T>} other
* @returns {Option<T>}
*/
alt(other) {
return other
}
/**
* @template T
* @param {() => Option<T>} other
* @returns {Option<T>}
*/
orElse(fn) {
return fn()
}
/**
* @template T
* @param {Some<T>} _other
* @returns {Some<T>}
*/
and(_other) {
return this
}
/**
* @template T
* @param {(value: T) => bool} _predicate
* @returns {Option<T>}
*/
filter(_predicate) {
return this
}
/**
* @template T
* @param {Option<T>} other
* @returns {bool}
*/
equals(other) {
return other.isNone()
}
/**
* @template T
* @param {Option<T>} _other
* @returns {bool}
*/
lte(_other) {
return false
}
/**
* @template T, [V=T]
* @param {(acc: V, value: T) => V} _reducer
* @param {V} init
* @returns {V}
*/
reduce(_reducer, init) {
return init
}
/**
* @template T
* @template {Option<T>} R
* @returns {R}
*/
flatten() {
return this
}
/**
* @template T, E
* @template {Result<T, E>} R
* @param {E} err
* @returns {R}
*/
okOr(err) {
return Result.err(err)
}
/**
* @template T, E
* @template {Result<T, E>} R
* @template {() => E} F
* @param {F} err
* @returns {R}
*/
okOrElse(err) {
return Result.err(err())
}
/**
* @template T
* @param {(value: T) => void} _fn
* @returns {this}
*/
inspect(_fn) {
return this
}
/**
* @template T
* @returns {T}
* @throws {UnwrapError}
*/
unwrap() {
throw new UnwrapError('tried to unwrap None')
}
/**
* @param {T} value
* @returns {T}
*/
unwrapOr(value) {
return value
}
/**
* @template T
* @param {() => T} fn
* @returns {T}
*/
unwrapOrElse(fn) {
return fn()
}
/** @returns {string} */
toString() {
return 'None'
}
}
export const None = new _None()
implementor(Option)
.implements(Applicative)
.specifiedBy(FantasyLand)
.with({ of: Option.some })
const structures = [Setoid, Ord, Functor, Chain, Filterable, Alt]
implementor(Some)
.implements(...structures)
.specifiedBy(FantasyLand)
.with({
chain: Some.prototype.bind,
filter: Some.prototype.filter,
map: Some.prototype.map,
alt: Some.prototype.alt,
equals: Some.prototype.equals,
lte: Some.prototype.lte,
})
implementor(_None)
.implements(...structures)
.specifiedBy(FantasyLand)
.with({
chain: _None.prototype.bind,
filter: _None.prototype.filter,
map: _None.prototype.map,
alt: _None.prototype.alt,
equals: _None.prototype.equals,
lte: _None.prototype.lte,
})

426
src/result.js Normal file
View file

@ -0,0 +1,426 @@
import { id } from './common.js'
import { implementor } from './specification/index.js'
import { Alt, Applicative, Chain, Foldable, Functor, Ord, Setoid } from './specification/structures.js'
import { FantasyLand } from './specification/fantasy-land.js'
import { UnwrapError } from './error.js'
import { Option, None } from './option.js'
export class Result {
/**
* @template T
* @param {T} value
*/
static ok(value) {
return new Ok(value)
}
/**
* @template E
* @param {E} error
*/
static err(error) {
return new Err(error)
}
// static-land methods
static chain(fn, self) {
return self.bind(fn)
}
static map(fn, self) {
return self.map(fn)
}
static alt(other, self) {
return self.alt(other)
}
static equals(other, self) {
return self.equals(other)
}
static lte(other, self) {
return self.lte(other)
}
}
/** @template T */
export class Ok extends Result {
/** @type T */
#value
/** @param {T} value */
constructor(value) {
super()
this.#value = value
}
/**
* @returns {bool}
*/
isOk() {
return true
}
/**
* @returns {bool}
*/
isErr() {
return false
}
/**
* @template E
* @template {Result<T, E>} R
* @param {(value: T) => R} fn
* @returns {R}
*/
bind(fn) {
return fn(this.#value)
}
/**
* @template V, E
* @template {Result<V, E>} R
* @param {(value: T) => V} fn
* @returns {R}
*/
map(fn) {
return Result.ok(fn(this.#value))
}
/**
* @template E
* @param {Result<T, E>} other
* @returns {Result<T, E>}
*/
and(other) {
return other
}
/**
* @template E
* @param {Result<T, E>} other
* @returns {Result<T, E>}
*/
alt(_other) {
return this
}
/**
* @template E, F
* @param {(error: E) => Result<T, F>} other
* @returns {Result<T, F>}
*/
orElse(_fn) {
return this
}
/**
* @template E
* @param {Result<T, E>} other
* @returns {bool}
*/
equals(other) {
return other.isOk() && this.#value === other.#value
}
/**
* @template E
* @param {Result<T, E>} other
* @returns {bool}
*/
lte(other) {
return other.isOk() && this.#value <= other.#value
}
/**
* @template [V=T]
* @param {(acc: V, value: T) => V} reducer
* @param {V} init
* @returns {V}
*/
reduce(reducer, init) {
return reducer(init, this.#value)
}
/**
* @template E
* @template {Result<T, E>} R
* @returns {R}
*/
flatten() {
if (this.#value instanceof Result) {
return this.bind(id)
} else {
return this
}
}
/**
* @returns {Option<T>}
*/
ok() {
return Option.some(this.#value)
}
/**
* @returns {Option<T>}
*/
err() {
return None
}
/**
* @param {(value: T) => void} fn
* @returns {this}
*/
inspect(fn) {
fn(this.#value)
return this
}
/**
* @returns {T}
* @throws {UnwrapError}
*/
unwrap() {
return this.#value
}
/**
* @returns {E}
* @throws {UnwrapError}
*/
unwrapErr() {
throw new UnwrapError('tried to unwrapErr an Ok')
}
/**
* @param {T} _value
* @returns {T}
*/
unwrapOr(_value) {
return this.unwrap()
}
/**
* @param {() => T} _fn
* @returns {T}
*/
unwrapOrElse(_fn) {
return this.unwrap()
}
/** @returns {string} */
toString() {
return `Ok(${this.#value})`
}
}
/** @template E */
export class Err extends Result {
/** @type E */
#error
/** @param {E} value */
constructor(error) {
super()
this.#error = error
}
/**
* @returns {bool}
*/
isOk() {
return false
}
/**
* @returns {bool}
*/
isErr() {
return true
}
/**
* @template T
* @template {Result<T, E>} R
* @param {(value: T) => R} fn
* @returns {R}
*/
bind(_fn) {
return this
}
/**
* @template V, E
* @template {Result<V, E>} R
* @param {(value: T) => V} fn
* @returns {R}
*/
map(_fn) {
return this
}
/**
* @template T
* @param {Result<T, E>} _other
* @returns {Result<T, E>}
*/
and(_other) {
return this
}
/**
* @template T
* @param {Result<T, E>} other
* @returns {Result<T, E>}
*/
alt(other) {
return other
}
/**
* @template T, F
* @param {(error: E) => Result<T, F>} other
* @returns {Result<T, F>}
*/
orElse(fn) {
return fn(this.#error)
}
/**
* @template T
* @param {Result<T, E>} other
* @returns {bool}
*/
equals(other) {
return other.isErr() && this.#error === other.#error
}
/**
* @template T
* @param {Result<T, E>} other
* @returns {bool}
*/
lte(other) {
return other.isErr() && this.#error <= other.#error
}
/**
* @template [V=T]
* @param {(acc: V, value: T) => V} _reducer
* @param {V} init
* @returns {V}
*/
reduce(_reducer, init) {
return init
}
/**
* @template T
* @template {Result<T, E>} R
* @returns {R}
*/
flatten() {
if (this.#error instanceof Err) {
return this.bind(id)
} else {
return this
}
}
/**
* @returns {Option<T>}
*/
ok() {
return None
}
/**
* @returns {Option<T>}
*/
err() {
return Option.some(this.#error)
}
/**
* @template T
* @param {(value: T) => void} _fn
* @returns {this}
*/
inspect(_fn) {
return this
}
/**
* @template T
* @returns {T}
* @throws {UnwrapError}
*/
unwrap() {
throw new UnwrapError('tried to unwrap an Err')
}
/**
* @returns {E}
* @throws {UnwrapError}
*/
unwrapErr() {
return this.#error
}
/**
* @param {T} value
* @returns {T}
*/
unwrapOr(value) {
return value
}
/**
* @template T
* @param {() => T} fn
* @returns {T}
*/
unwrapOrElse(fn) {
return fn()
}
/** @returns {string} */
toString() {
return `Err(${this.#error})`
}
}
implementor(Result)
.implements(Applicative)
.specifiedBy(FantasyLand)
.with({ of: Result.ok })
const structures = [Setoid, Ord, Functor, Chain, Alt, Foldable]
implementor(Ok)
.implements(...structures)
.specifiedBy(FantasyLand)
.with({
chain: Ok.prototype.bind,
map: Ok.prototype.map,
reduce: Ok.prototype.reduce,
alt: Ok.prototype.alt,
equals: Ok.prototype.equals,
lte: Ok.prototype.lte,
})
implementor(Err)
.implements(...structures)
.specifiedBy(FantasyLand)
.with({
chain: Err.prototype.bind,
map: Err.prototype.map,
reduce: Err.prototype.reduce,
alt: Err.prototype.alt,
equals: Err.prototype.equals,
lte: Err.prototype.lte,
})

View file

@ -0,0 +1,19 @@
import { zip } from '../utils.js'
import { Specification } from './index.js'
import * as Structures from './structures.js'
const prefix = name => `fantasy-land/${name}`
const structures = Object.values(Structures).map(struct => {
const methods = struct.methods.map(m => m.name)
if (methods.length === 0) {
return struct
}
const overrides = methods.map(prefix)
const schema = Object.fromEntries(zip(methods, overrides))
return struct.with(schema)
})
export const FantasyLand = new Specification('fantasy-land', structures)

293
src/specification/index.js Normal file
View file

@ -0,0 +1,293 @@
import { path } from '../utils.js'
export class Method {
name
method
isStatic
constructor(name, method = name, { isStatic = false } = {}) {
this.name = name
this.method = method
this.isStatic = isStatic
}
static from(value, options) {
if (value instanceof Method) {
return value
} else {
return new Method(value, options)
}
}
static asStatic(value, options) {
return new Method(value, { isStatic: true, ...options })
}
#getBindTarget(target) {
if (this.isStatic) {
return target
} else {
return target.prototype
}
}
bind(target, impl) {
this.#getBindTarget(target)[this.method] = impl
}
clone() {
return new Method(this.name, this.method, { isStatic: this.isStatic })
}
}
export class Structure {
name
methods
dependencies
#methodsMap
constructor(name, methods = [], dependencies = []) {
this.name = name
this.dependencies = dependencies
this.methods = []
this.#methodsMap = new Map()
methods.forEach(m => this.hasMethod(m))
}
hasMethod(method, options) {
if (!(method instanceof Method)) {
method = Method.from(method, options)
}
this.methods.push(method)
this.#methodsMap.set(method.name, method)
return this
}
dependsOn(...algebras) {
this.dependencies.push(...algebras)
return this
}
getMethod(name) {
return this.#methodsMap.get(name)
}
clone() {
return new Structure(
this.name,
this.methods.map(m => m.clone()),
this.dependencies.slice())
}
with(aliases) {
const clonedStructure = this.clone()
if (!(aliases instanceof AliasMap)) {
aliases = AliasMap.from(aliases)
}
for (const [id, override] of aliases.schema.entries()) {
const method = clonedStructure.getMethod(id)
if (method) {
method.method = override
} else {
console.warn(`Warning: Attempted to override method '${id}' not found in Structure '${this.name}'.`)
}
}
return clonedStructure
}
}
class DependencyGraph {
#structures
#methods
constructor(structures) {
this.#structures = new Map()
for (const struct of structures) {
this.#structures.set(struct.name, struct)
}
this.#methods = new Map()
this.#build()
}
#build() {
const visited = new Set()
const stack = []
for (const structure of this.#structures.values()) {
stack.push(structure)
}
while (stack.length > 0) {
const current = stack.pop()
if (visited.has(current.name)) {
continue
}
visited.add(current.name)
for (const method of current.methods) {
if (!this.#methods.has(method.name)) {
this.#methods.set(method.name, method)
}
}
for (const dep of current.dependencies) {
const resolvedDep = this.#structures.get(dep.name) || dep
stack.push(resolvedDep)
}
}
}
getMethods() {
return this.#methods
}
}
export class Specification {
name
structures
#dependencies
constructor(name, structures) {
this.name = name
if (structures instanceof Map) {
this.structures = structures
} else {
this.structures = new Map()
for (const struct of structures) {
if (!(struct instanceof Structure)) {
throw new TypeError("Expected an array of Structure instances.")
}
this.structures.set(struct.name, struct)
}
}
this.#dependencies = new DependencyGraph(this.structures.values())
}
implementedBy(target) {
return new Implementor(target, this)
}
getOverrides(structs) {
return structs.map(s => this.structures.get(s.name))
}
getAllMethods() {
return this.#dependencies.getMethods()
}
}
export class Implementor {
target
structures = []
specification
constructor(target, specification = undefined) {
this.target = target
this.specification = specification
}
implements(...structures) {
for (const struct of structures) {
let structure
if (struct instanceof Structure) {
structure = struct
} else if (typeof struct === 'string') {
structure = this.specification.getStructure(struct)
if (!structure) {
console.warn(`Warning: Structure '${struct}' not found in specification '${this.specification.name}'.`)
continue
}
} else {
throw new TypeError(`Expected Structure instance or string name, got ${typeof struct}.`)
}
if (structure) {
this.structures.push(structure)
}
}
return this
}
specifiedBy(specification) {
this.specification = specification
return this
}
with(aliases) {
if (!(aliases instanceof AliasMap)) {
aliases = AliasMap.from(aliases)
}
let methods
if (this.structures.size === 0) {
console.warn(`${this.target.name} is implementing ${this.specification.name} with no specified structures`)
methods = this.specification.getMethods()
} else {
const overrides = this.specification.getOverrides(this.structures)
methods = new DependencyGraph(overrides).getMethods()
}
for (const [alias, impl] of aliases.schema.entries()) {
const method = methods.get(alias)
if (method) {
let bindMethod
if (typeof impl === 'function') {
bindMethod = impl
} else if (typeof impl === 'string') {
bindMethod = path(impl, this.target)
if (bindMethod == null || typeof bindMethod !== 'function') {
throw new TypeError(`Could not resolve string alias '${impl}' for specification method '${alias}'`)
}
} else {
throw new TypeError(`Alias '${alias}' has an invalid implementation value. Expected function or string.`)
}
if (bindMethod != null) {
method.bind(this.target, bindMethod)
} else {
console.warn(`Warning: No valid implementation found for alias '${alias}'.`)
}
} else {
console.warn(`Warning: Alias '${alias}' does not correspond to any method in the specification.`)
}
}
}
}
export class AliasMap {
schema
constructor(schema = new Map()) {
this.schema = schema
}
static from(iterable) {
if (iterable instanceof Map) {
return new AliasMap(iterable)
} else if (Array.isArray(iterable) || typeof iterable[Symbol.iterator] == 'function') {
return new AliasMap(new Map(iterable))
} else if (typeof iterable === 'object') {
return new AliasMap(new Map(Object.entries(iterable)))
} else {
throw new TypeError("can't make bind target from given schema")
}
}
}
export const implementor = (target, specification = null) => new Implementor(target, specification)

View file

View file

@ -0,0 +1,91 @@
import { Method, Structure } from './index.js'
const method = (name, isStatic = false) => new Method(name, name, { isStatic })
export const Setoid = new Structure('Setoid')
.hasMethod(method('equals'))
export const Ord = new Structure('Ord')
.hasMethod(method('lte'))
.dependsOn(Setoid)
export const Semigroupoid = new Structure('Semigroupoid')
.hasMethod(method('compose'))
export const Category = new Structure('Category')
.hasMethod(method('id', true))
.dependsOn(Semigroupoid)
export const Semigroup = new Structure('Semigroup')
.hasMethod(method('concat'))
export const Monoid = new Structure('Monoid')
.hasMethod(method('empty', true))
.dependsOn(Semigroup)
export const Group = new Structure('Group')
.hasMethod(method('invert'))
.dependsOn(Monoid)
export const Foldable = new Structure('Foldable')
.hasMethod(method('reduce'))
export const Functor = new Structure('Functor')
.hasMethod(method('map'))
export const Traversable = new Structure('Traversable')
.hasMethod(method('traverse'))
.dependsOn(Foldable, Functor)
export const Profunctor = new Structure('Profunctor')
.hasMethod(method('promap'))
.dependsOn(Functor)
export const Alt = new Structure('Alt')
.hasMethod(method('alt'))
.dependsOn(Functor)
export const Plus = new Structure('Plus')
.hasMethod(method('zero', true))
.dependsOn(Alt)
export const Apply = new Structure('Apply')
.hasMethod(method('ap'))
.dependsOn(Functor)
export const Applicative = new Structure('Applicative')
.hasMethod(method('of', true))
.dependsOn(Apply)
export const Chain = new Structure('Chain')
.hasMethod(method('chain'))
.dependsOn(Apply)
export const ChainRec = new Structure('ChainRec')
.hasMethod(method('chainRec'))
.dependsOn(Chain)
export const Alternative = new Structure('Alternative')
.dependsOn(Plus, Applicative)
export const Monad = new Structure('Monad')
.dependsOn(Applicative, Chain)
export const Bifunctor = new Structure('Bifunctor')
.hasMethod(method('bimap'))
.dependsOn(Functor)
export const Extend = new Structure('Extend')
.hasMethod(method('extend'))
.dependsOn(Functor)
export const Comonad = new Structure('Comonad')
.hasMethod(method('extract'))
.dependsOn(Extend)
export const Contravariant = new Structure('Contravariant')
.hasMethod(method('contramap'))
export const Filterable = new Structure('Filterable')
.hasMethod(method('filter'))

View file

@ -1,95 +0,0 @@
/**
* @template R
* @typedef {(...args: any[]) => R} Fn
*/
/**
* @template R
* @typedef {() => R} EmptyFn
*/
/**
* @template R
* @callback cata
* @param {Record<string, Fn<R>> & Partial<Record<'_', EmptyFn<R>>>} pattern
* @returns {R}
*
* @throws MatchError
*/
/**
* @typedef Variant
* @property {cata<any>} cata
*/
/**
* @typedef {(...values: any[]) => Variant} VariantConstructor
*/
/**
* @template {PropertyKey[]} Variant
* @typedef {{ [key in Variant[number]]: (...values: any) => Variant }} Variants
*/
/**
* @template {PropertyKey} T
* @template {PropertyKey[]} U
* @typedef {{ is: typeof is } & Variants<U>} Union
*/
const Tag = Symbol('Tag')
export class CatamorphismError extends Error {
/** @param {PropertyKey} name */
constructor(name) {
super(`unmatched arm in catamorphism: ${name.toString()}`)
}
}
/**
* @param {PropertyKey} type
* @param {PropertyKey} variant
* @returns {(...values: any[]) => Variant}
*/
const Variant = (type, variant) => (...values) => ({
[Tag]: type,
cata(pattern) {
if (variant in pattern) {
// NOTE: this is a workaround for typescript not recognizing
// that objects can be indexed with symbols
return pattern[ /** @type {string} */ (variant)](...values)
} else if ('_' in pattern) {
return pattern._()
} else {
throw new CatamorphismError(variant)
}
}
})
/**
* @template T, U
* @this {Union<T, U>}
* @param {any} other
* @returns {other is Variant<U>}
*/
function is(other) {
return Object.hasOwn(other, Tag) && other[Tag] === this[Tag]
}
/**
* @template {string} const T
* @template {Array<PropertyKey>} const U
* @param {T} typeName
* @param {...U} variantNames
* @returns {Union<T, U>}
*/
export const Union = (typeName, variantNames) => {
const typeTag = Symbol(typeName)
const tag = { [Tag]: typeTag, is }
const variants = Object.fromEntries(variantNames.map(v => [v, Variant(typeTag, v)]))
const result = Object.assign(tag, variants)
return /** @type {Union<T, U>} */ (result)
}

22
src/utils.js Normal file
View file

@ -0,0 +1,22 @@
export const path = (path, obj) => {
const parts = path.split('.')
let current = obj
let found = true
for (let i = 0; i < parts.length; i++) {
if (current && typeof current === 'object' && parts[i] in current) {
current = current[parts[i]]
} else {
found = false
break
}
}
return found
}
export const zip = (...xs) => {
const maxLength = Math.max(...xs.map(arr => arr.length))
return Array.from({ length: maxLength }).map((_, i) =>
xs.map(arr => arr[i])
)
}

View file

@ -1,7 +0,0 @@
#!/usr/bin/env node
import { TerminalRunner } from 'folktest'
import * as Tests from './units/index.js'
console.log(TerminalRunner(Tests).toString())

228
tests/option.test.js Normal file
View file

@ -0,0 +1,228 @@
import { Option, Some, None } from '../src/option.js'
import { UnwrapError } from '../src/error.js'
import { Result } from '../src/result.js'
import { describe, it, mock } from 'node:test'
import assert from 'node:assert/strict'
const some = v => new Some(v)
const eq = assert.deepStrictEqual.bind(assert)
describe('Option', () => {
it('Option.some should return a Some', async () => {
eq(Option.some(1), new Some(1))
// fantasy-land/of
eq(Option['fantasy-land/of'](1), new Some(1))
})
it('Some.bind(fn) should call fn', async () => {
const a = some(1)
let res = a.bind(x => some(x * 2))
eq(res, some(2))
// fantasy-land/chain
res = a['fantasy-land/chain'](x => some(x * 2))
eq(res, some(2))
})
it('None.bind(fn) should not execute fn', async () => {
const fn = mock.fn(v => some(v * 2))
let res = None.bind(fn)
eq(res, None)
eq(fn.mock.callCount(), 0)
// fantasy-land/chain
res = None['fantasy-land/chain'](fn)
eq(res, None)
eq(fn.mock.callCount(), 0)
})
it('Some.map(fn) should transform the inner value', () => {
const a = some(1)
let res = a.map(x => x + 1)
eq(res, some(2))
// fantasy-land/map
res = a['fantasy-land/map'](x => x + 1)
eq(res, some(2))
})
it('None.map(fn) should not execute fn', async () => {
const fn = mock.fn(v => some(v * 2))
let res = None.map(fn)
eq(res, None)
eq(fn.mock.callCount(), 0)
// fantasy-land/chain
res = None['fantasy-land/map'](fn)
eq(res, None)
eq(fn.mock.callCount(), 0)
})
it('Some.and(other)', () => {
const a = some(1)
const b = some(2)
assert.strictEqual(a.and(b), b)
})
it('None.and(other)', () => {
assert.strictEqual(None.and(some(1)), None)
})
it('Some.alt(other) should return itself', () => {
const self = some(1)
const other = some(2)
let res = self.alt(other)
assert.strictEqual(res, self)
// fantasy-land/alt
res = self['fantasy-land/alt'](other)
assert.strictEqual(res, self)
})
it('None.alt(other) should return other', () => {
const other = some(2)
let res = None.alt(other)
assert.strictEqual(res, other)
// fantasy-land/alt
res = None['fantasy-land/alt'](other)
assert.strictEqual(res, other)
})
it('Option.orElse()', () => {
const nothing = mock.fn(() => None)
const chocolate = mock.fn(() => some('chocolate'))
eq(some('vanilla').orElse(chocolate), some('vanilla'))
eq(chocolate.mock.callCount(), 0)
eq(None.orElse(chocolate), some('chocolate'))
eq(chocolate.mock.callCount(), 1)
eq(None.orElse(nothing), None)
eq(nothing.mock.callCount(), 1)
})
it('Some.filter(predicate)', () => {
const a = some(1)
assert.strictEqual(a.filter(x => x == 1), a)
assert.strictEqual(a.filter(x => x == 2), None)
// fantasy-land/filter
assert.strictEqual(a['fantasy-land/filter'](x => x == 1), a)
})
it('None.filter(predicate)', () => {
assert.strictEqual(None.filter(x => x == 1), None)
// fantasy-land/filter
assert.strictEqual(None['fantasy-land/filter'](x => x == 1), None)
})
it('Some.okOr(err)', () => {
eq(some(1).okOr(2), Result.ok(1))
})
it('None.okOr(err)', () => {
eq(None.okOr(2), Result.err(2))
})
it('Some.okOrElse(err)', () => {
eq(some(1).okOrElse(() => 2), Result.ok(1))
})
it('None.okOr(err)', () => {
eq(None.okOrElse(() => 2), Result.err(2))
})
it('Some.equals(other)', () => {
const a = some(1)
assert.ok(a.equals(some(1)))
assert(!a.equals(some(2)))
assert(!a.equals(None))
// fantasy-land/equals
assert.ok(a['fantasy-land/equals'](some(1)))
})
it('None.equals(other)', () => {
assert.ok(None.equals(None))
assert(!None.equals(some(1)))
// fantasy-land/equals
assert.ok(None['fantasy-land/equals'](None))
})
it('Some.lte(other)', () => {
const a = some(1)
assert.ok(a.lte(some(1)))
assert.ok(a.lte(some(2)))
assert.ok(!a.lte(some(0)))
assert(!a.lte(None))
// fantasy-land/lte
assert.ok(a['fantasy-land/lte'](some(1)))
})
it('None.lte(other)', () => {
assert(!None.lte(None))
assert(!None.lte(some(1)))
// fantasy-land/lte
assert.ok(!None['fantasy-land/lte'](None))
})
it('Some.flatten()', async () => {
eq(some(1).flatten(), some(1))
eq(some(some(1)).flatten(), some(1))
eq(some(some(some(1))).flatten(), some(some(1)))
eq(some(None).flatten(), None)
})
it('None.flatten()', async () => {
eq(None.flatten(), None)
})
it('Some.inspect(fn) should call fn', async () => {
const fn = mock.fn(_x => { })
some(1).inspect(fn)
eq(fn.mock.callCount(), 1)
})
it('None.inspect(fn) should not call fn', async () => {
const fn = mock.fn(_x => { })
None.inspect(fn)
eq(fn.mock.callCount(), 0)
})
it('Some.unwrap() returns inner value', async () => {
const a = some(1)
assert.doesNotThrow(a.unwrap.bind(a), UnwrapError)
eq(a.unwrap(), 1)
})
it('None.unwrap() throws UnwrapError', async () => {
assert.throws(None.unwrap, UnwrapError)
})
it('Some.unwrapOr(value) returns inner value', async () => {
eq(some(1).unwrapOr(2), 1)
})
it('None.unwrapOr(value) returns value', async () => {
eq(None.unwrapOr(2), 2)
})
it('Some.unwrapOrElse(fn) returns inner value', async () => {
const fn = mock.fn(() => 2)
const a = some(1)
eq(a.unwrapOrElse(fn), 1)
eq(fn.mock.callCount(), 0)
})
it('None.unwrapOrElse(fn) returns result of fn', async () => {
const fn = mock.fn(() => 2)
eq(None.unwrapOrElse(fn), 2)
eq(fn.mock.callCount(), 1)
})
})

276
tests/result.test.js Normal file
View file

@ -0,0 +1,276 @@
import { Result, Ok, Err } from '../src/result.js'
import { UnwrapError } from '../src/error.js'
import { Option } from '../src/option.js'
import { describe, it, mock } from 'node:test'
import assert from 'node:assert/strict'
const ok = v => new Ok(v)
const err = e => new Err(e)
const eq = assert.deepStrictEqual.bind(assert)
describe('Result', () => {
it('Result.ok should return an Ok value', async () => {
eq(Result.ok(1), new Ok(1))
// fantasy-land/of
eq(Result['fantasy-land/of'](1), new Ok(1))
})
it('Ok.bind(fn) should call fn', async () => {
const a = ok(1)
const fn = x => ok(x * 2)
let res = a.bind(fn)
eq(res, ok(2))
// fantasy-land/chain
res = a['fantasy-land/chain'](fn)
eq(res, ok(2))
// static-land chain
res = Result.chain(fn, a)
eq(res, ok(2))
})
it('Err.bind(fn) should not execute fn', async () => {
const fn = mock.fn(v => ok(v * 2))
const e = err(new Error('failed'))
let res = e.bind(fn)
eq(res, err(new Error('failed')))
eq(fn.mock.callCount(), 0)
// fantasy-land/chain
res = e['fantasy-land/chain'](fn)
eq(res, err(new Error('failed')))
eq(fn.mock.callCount(), 0)
// static-land chain
res = Result.chain(fn, e)
eq(res, err(new Error('failed')))
eq(fn.mock.callCount(), 0)
})
it('Ok.map(fn) should transform the inner value', () => {
const a = ok(1)
const fn = x => x + 1
let res = a.map(fn)
eq(res, ok(2))
// fantasy-land/map
res = a['fantasy-land/map'](fn)
eq(res, ok(2))
// static-land map
res = Result.map(fn, a)
eq(res, ok(2))
})
it('Err.map(fn) should not execute fn', async () => {
const fn = mock.fn(v => Result.ok(v * 2))
const e = err(new Error('failed'))
let res = e.map(fn)
eq(res, err(new Error('failed')))
eq(fn.mock.callCount(), 0)
// fantasy-land/chain
res = e['fantasy-land/map'](fn)
eq(res, err(new Error('failed')))
eq(fn.mock.callCount(), 0)
// static-land chain
res = Result.map(fn, e)
eq(res, err(new Error('failed')))
eq(fn.mock.callCount(), 0)
})
it('Ok.and(other)', () => {
const a = ok(1)
const b = ok(2)
assert.strictEqual(a.and(b), b)
})
it('Err.and(other)', () => {
const a = err(1)
const b = ok(2)
assert.strictEqual(a.and(b), a)
})
it('Ok.alt(other) should return itself', () => {
const self = ok(1)
const other = ok(2)
let res = self.alt(other)
assert.strictEqual(res, self)
// fantasy-land/alt
res = self['fantasy-land/alt'](other)
assert.strictEqual(res, self)
// static-land alt
res = Result.alt(other, self)
assert.strictEqual(res, self)
})
it('Err.alt(other) should return other', () => {
const self = err(new Error('failure'))
const other = ok(2)
let res = self.alt(other)
assert.strictEqual(res, other)
// fantasy-land/alt
res = self['fantasy-land/alt'](other)
assert.strictEqual(res, other)
// static-land alt
res = Result.alt(other, self)
assert.strictEqual(res, other)
})
it('Result.orElse(fn)', () => {
const sq = x => ok(x * x)
eq(ok(2).orElse(sq).orElse(sq), ok(2))
eq(ok(2).orElse(err).orElse(sq), ok(2))
eq(err(3).orElse(sq).orElse(err), ok(9))
eq(err(3).orElse(err).orElse(err), err(3))
})
it('Ok.ok()', () => {
eq(ok(1).ok(), Option.some(1))
})
it('Err.ok()', () => {
eq(err(1).ok(), Option.none())
})
it('Ok.err()', () => {
eq(ok(1).err(), Option.none())
})
it('Err.err()', () => {
eq(err(1).err(), Option.some(1))
})
it('Ok.equals(other)', () => {
const a = ok(1)
assert.ok(a.equals(ok(1)))
assert(!a.equals(ok(2)))
assert(!a.equals(err(1)))
// fantasy-land/equals
assert.ok(a['fantasy-land/equals'](ok(1)))
// static-land equals
assert.ok(Result.equals(ok(1), a))
})
it('Err.equals(other)', () => {
const a = err(1)
assert.ok(a.equals(err(1)))
assert(!a.equals(err(2)))
assert(!a.equals(ok(1)))
// fantasy-land/equals
assert.ok(a['fantasy-land/equals'](err(1)))
// static-land equals
assert.ok(Result.equals(err(1), a))
})
it('Ok.lte(other)', () => {
const a = ok(1)
assert.ok(a.lte(ok(1)))
assert.ok(a.lte(ok(2)))
assert.ok(!a.lte(ok(0)))
assert(!a.lte(err(1)))
// fantasy-land/lte
assert.ok(a['fantasy-land/lte'](ok(1)))
// static-land lte
assert.ok(Result.lte(ok(1), a))
})
it('Err.lte(other)', () => {
const a = err(1)
assert.ok(a.lte(err(1)))
assert.ok(a.lte(err(2)))
assert.ok(!a.lte(err(0)))
assert(!a.lte(ok(1)))
// fantasy-land/lte
assert.ok(a['fantasy-land/lte'](err(1)))
// static-land lte
assert.ok(Result.lte(err(1), a))
})
it('Ok.flatten()', async () => {
eq(ok(1).flatten(), ok(1))
eq(ok(ok(1)).flatten(), ok(1))
eq(ok(ok(ok(1))).flatten(), ok(ok(1)))
eq(ok(err(1)).flatten(), err(1))
})
it('Err.flatten()', async () => {
eq(err(1).flatten(), err(1))
eq(err(err(1)).flatten(), err(1))
})
it('Ok.inspect(fn) should call fn', async () => {
const fn = mock.fn(_x => { })
ok(1).inspect(fn)
eq(fn.mock.callCount(), 1)
})
it('Err.inspect(fn) should not call fn', async () => {
const fn = mock.fn(_x => { })
err(1).inspect(fn)
eq(fn.mock.callCount(), 0)
})
it('Ok.unwrap() returns inner value', async () => {
const a = ok(1)
assert.doesNotThrow(a.unwrap.bind(a), UnwrapError)
eq(a.unwrap(), 1)
})
it('Err.unwrap() throws UnwrapError', async () => {
assert.throws(err(1).unwrap, UnwrapError)
})
it('Ok.unwrapErr() throws UnwrapError', async () => {
const a = ok(1)
assert.throws(a.unwrapErr, UnwrapError)
})
it('Err.unwrapErr() returns inner error', async () => {
const a = err(1)
assert.doesNotThrow(a.unwrapErr.bind(a), UnwrapError)
eq(a.unwrapErr(), 1)
})
it('Ok.unwrapOr(value) returns inner value', async () => {
eq(ok(1).unwrapOr(2), 1)
})
it('Err.unwrapOr(value) returns value', async () => {
eq(err(1).unwrapOr(2), 2)
})
it('Ok.unwrapOrElse(fn) returns inner value', async () => {
const fn = mock.fn(() => 2)
const a = ok(1)
eq(a.unwrapOrElse(fn), 1)
eq(fn.mock.callCount(), 0)
})
it('Err.unwrapOrElse(fn) returns result of fn', async () => {
const fn = mock.fn(() => 2)
const a = err(1)
eq(a.unwrapOrElse(fn), 2)
eq(fn.mock.callCount(), 1)
})
})

View file

@ -1,2 +0,0 @@
export { Tests as Monad } from './monad.js'

View file

@ -1,48 +0,0 @@
// @ts-nocheck
import { it, assert } from 'folktest'
import { IO, List, Option, Reader, Result, Some } from '../../src/index.js'
import { Applicative, Chain, Ord, Setoid } from '../../src/algebra/interfaces.js'
const Algebra = [Option, Result, List, IO, Reader]
const inc = F => x => F.of(x + 1)
const dbl = F => x => F.of(x * 2)
const impl = i => m => i.implementedBy(m)
export const Tests = [
it('unit is a left identity', () => {
Algebra.filter(impl(Setoid, Applicative, Chain)).forEach(algebra => {
const f = inc(algebra)
assert(
algebra.of(1).chain(f).equals(f(1)),
`${algebra.name} is not a left identity`
)
})
}),
it('unit is a right identity', () => {
Algebra.filter(impl(Setoid, Applicative, Chain)).forEach(algebra => {
const m = algebra.of(1)
assert(
m.chain(algebra.of).equals(m),
`${algebra.name} is not a right identity`
)
})
}),
it('unit is associative', () => {
Algebra.filter(impl(Setoid, Applicative, Chain)).forEach(algebra => {
const a = algebra.of(1)
const f = inc(algebra)
const g = dbl(algebra)
const x = a.chain(f).chain(g)
const y = a.chain(x => f(x).chain(g))
assert(x.equals(y), `${algebra.name} is not associative`)
})
}),
]