├── .github └── workflows │ ├── format.yml │ ├── publish.yml │ ├── pull_request.yml │ ├── push.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── applicable.ts ├── array.ts ├── async.ts ├── async_either.ts ├── async_iterable.ts ├── benchmarks ├── array.bench.ts ├── fn.bench.ts └── stream.bench.ts ├── bimappable.ts ├── boolean.ts ├── combinable.ts ├── comparable.ts ├── composable.ts ├── datum.ts ├── datum_either.ts ├── decoder.ts ├── deno.json ├── deno.lock ├── effect.ts ├── either.ts ├── examples ├── arrow.ts ├── async_clone.ts ├── async_either.ts ├── comparable.ts ├── failable.ts ├── fetch_archives.ts ├── flow.ts ├── freer.ts ├── fx.ts ├── hkts.ts ├── lazy.ts ├── most.ts ├── natural.ts ├── observable.ts ├── optic.ts ├── play.ts ├── profunctor_optics.ts ├── schema.ts ├── store.ts ├── tagged_error.ts ├── transformers.ts ├── traverse.ts └── tree.ts ├── failable.ts ├── filterable.ts ├── flake.lock ├── flake.nix ├── flatmappable.ts ├── fn.ts ├── fn_either.ts ├── foldable.ts ├── free.ts ├── ideas ├── README.md ├── callbag.ts └── dux.ts ├── identity.ts ├── initializable.ts ├── iterable.ts ├── json_schema.ts ├── kind.ts ├── map.ts ├── mappable.ts ├── newtype.ts ├── nil.ts ├── number.ts ├── optic.ts ├── option.ts ├── pair.ts ├── parser.ts ├── predicate.ts ├── premappable.ts ├── promise.ts ├── record.ts ├── refinement.ts ├── schemable.ts ├── scripts ├── build-npm.ts ├── docs.sh └── exports.sh ├── set.ts ├── showable.ts ├── sortable.ts ├── state.ts ├── state_either.ts ├── stream.ts ├── string.ts ├── sync.ts ├── sync_either.ts ├── testing ├── applicable.test.ts ├── array.test.ts ├── async.test.ts ├── async_either.test.ts ├── async_iterable.test.ts ├── boolean.test.ts ├── combinable.test.ts ├── comparable.test.ts ├── composable.test.ts ├── datum.test.ts ├── datum_either.test.ts ├── decoder.test.ts ├── effect.test.ts ├── either.test.ts ├── failable.test.ts ├── flatmappable.test.ts ├── fn.test.ts ├── fn_either.test.ts ├── foldable.test.ts ├── free.test.ts ├── identity.test.ts ├── initializable.test.ts ├── iterable.test.ts ├── json_schema.test.ts ├── map.test.ts ├── mappable.test.ts ├── newtype.test.ts ├── nil.test.ts ├── number.test.ts ├── optic.test.ts ├── option.test.ts ├── pair.test.ts ├── parser.test.ts ├── predicate.test.ts ├── promise.test.ts ├── record.test.ts ├── refinement.test.ts ├── schemable.test.ts ├── set.test.ts ├── showable.test.ts ├── sortable.test.ts ├── state.test.ts ├── state_either.test.ts ├── stream.test.ts ├── string.test.ts ├── sync.test.ts ├── sync_either.test.ts ├── these.test.ts └── tree.test.ts ├── these.ts ├── traversable.ts ├── tree.ts └── wrappable.ts /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Check Format 2 | 3 | on: [workflow_call] 4 | 5 | jobs: 6 | format: 7 | strategy: 8 | matrix: 9 | deno: ["v2.2.12"] 10 | os: [ubuntu-latest] 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - name: Setup repo 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Deno 19 | uses: denoland/setup-deno@v1 20 | with: 21 | deno-version: ${{ matrix.deno }} 22 | 23 | - name: Run Deno Check Formatting 24 | run: deno fmt --check 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: [workflow_call] 4 | 5 | jobs: 6 | publish: 7 | permissions: 8 | contents: read 9 | id-token: write 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Setup repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Deno 18 | uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: "v2.2.12" 21 | 22 | - name: Publish package 23 | run: deno publish 24 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | call-format: 7 | uses: ./.github/workflows/format.yml 8 | 9 | call-test: 10 | uses: ./.github/workflows/test.yml 11 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | call-format: 9 | uses: ./.github/workflows/format.yml 10 | 11 | call-test: 12 | uses: ./.github/workflows/test.yml 13 | secrets: inherit 14 | 15 | call-publish: 16 | needs: [call-format, call-test] 17 | uses: ./.github/workflows/publish.yml 18 | permissions: 19 | contents: read 20 | id-token: write 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [workflow_call] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | deno: ["v2.2.12"] 10 | os: [macOS-latest, windows-latest, ubuntu-latest] 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - name: Setup repo 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Deno 19 | uses: denoland/setup-deno@v1 20 | with: 21 | deno-version: ${{ matrix.deno }} 22 | 23 | - name: Run Deno Tests 24 | run: deno test --parallel --coverage=coverage 25 | 26 | - name: Generate Coverage 27 | run: deno coverage --unstable ./coverage --lcov > ./coverage/lcov.info 28 | 29 | - name: Coveralls 30 | uses: coverallsapp/github-action@master 31 | with: 32 | flag-name: run-${{ matrix.deno }}-${{ matrix.os }} 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | base-path: / 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .vim-lsp-settings 3 | draft 4 | *.swp 5 | coverage 6 | out 7 | result 8 | build 9 | **/.envrc 10 | **/.direnv 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Giulio Canti 4 | Copyright (c) 2018 Tom Crockett 5 | Copyright (c) 2019 Brandon Blaylock 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /applicable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Applicable is a structure that allows a function to be applied inside of the 3 | * associated concrete structure. For example, `Option` may hold a value of 4 | * `(a: A) => B` inside of it. An Applicable for Option would allow one to 5 | * apply the `A` in an `Option` to the function `(a: A) => B` in an 6 | * `Option<(a: A) => B>`, resulting in an `Option`. 7 | * 8 | * @module Applicable 9 | * @since 2.0.0 10 | */ 11 | 12 | import type { $, Hold, Kind } from "./kind.ts"; 13 | import type { Combinable } from "./combinable.ts"; 14 | import type { Mappable } from "./mappable.ts"; 15 | import type { Wrappable } from "./wrappable.ts"; 16 | 17 | /** 18 | * The Applicable interface. This interface includes the methods apply, map, and 19 | * wrap. 20 | * 21 | * @since 2.0.0 22 | */ 23 | export interface Applicable< 24 | U extends Kind, 25 | > extends Mappable, Wrappable, Hold { 26 | readonly apply: < 27 | A, 28 | B = never, 29 | C = never, 30 | D = unknown, 31 | E = unknown, 32 | >( 33 | ta: $, 34 | ) => < 35 | I, 36 | J = never, 37 | K = never, 38 | L = unknown, 39 | >( 40 | tfai: $ I, J, K], [L], [E]>, 41 | ) => $; 42 | } 43 | 44 | /** 45 | * Create a Combinable instance for an Applicable structure given a Combinable for the inner type. 46 | * 47 | * @example 48 | * ```ts 49 | * import * as A from "./applicable.ts"; 50 | * import * as O from "./option.ts"; 51 | * import * as N from "./number.ts"; 52 | * 53 | * const combinableOption = A.getApplicableCombinable(O.ApplicableOption)(N.CombinableNumberSum); 54 | * 55 | * const result = combinableOption.combine(O.some(2))(O.some(3)); // Some(5) 56 | * ``` 57 | * 58 | * @since 2.0.0 59 | */ 60 | export function getApplicableCombinable( 61 | { apply, map }: Applicable, 62 | ): ( 63 | combinable: Combinable, 64 | ) => Combinable<$> { 65 | return ( 66 | { combine }: Combinable, 67 | ): Combinable<$> => { 68 | const _map = map(combine); 69 | return { 70 | combine: (second) => (first) => apply(first)(_map(second)), 71 | }; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /benchmarks/array.bench.ts: -------------------------------------------------------------------------------- 1 | import * as A from "../array.ts"; 2 | import * as N from "../number.ts"; 3 | import { pipe } from "../fn.ts"; 4 | 5 | const count = 1_000_000; 6 | const sorted = A.range(count); 7 | const binarySearch = A.binarySearch(N.SortableNumber); 8 | const monoSearch = A.monoSearch(N.SortableNumber); 9 | const searches = pipe(A.range(count), A.map(() => Math.random() * count)); 10 | const expensiveOperation = (n: number) => 11 | ((n ** Math.floor(n)) / (n ** (Math.floor(n) - 1))) + 12 | ((n ** Math.floor(n - 2)) / (n ** Math.floor(n - 3))); 13 | const reducer = (acc: number, value: number) => acc + expensiveOperation(value); 14 | 15 | Deno.bench("array native findIndex", { group: "binarySearch" }, () => { 16 | searches.forEach((value) => sorted.findIndex((n) => n <= value)); 17 | }); 18 | 19 | Deno.bench("array binarySearch", { group: "binarySearch" }, () => { 20 | searches.forEach((value) => binarySearch(value, sorted)); 21 | }); 22 | 23 | Deno.bench("array monoSearch", { group: "binarySearch" }, () => { 24 | searches.forEach((value) => monoSearch(value, sorted)); 25 | }); 26 | 27 | Deno.bench("array map", { group: "map" }, () => { 28 | pipe(searches, A.map(expensiveOperation)); 29 | }); 30 | 31 | Deno.bench("array native map", { group: "map" }, () => { 32 | searches.map(expensiveOperation); 33 | }); 34 | 35 | Deno.bench("array fold", { group: "fold" }, () => { 36 | pipe(searches, A.fold(reducer, 0)); 37 | }); 38 | 39 | Deno.bench("array native reduce", { group: "fold" }, () => { 40 | searches.reduce(reducer, 0); 41 | }); 42 | -------------------------------------------------------------------------------- /benchmarks/stream.bench.ts: -------------------------------------------------------------------------------- 1 | import * as S from "../stream.ts"; 2 | import * as CB from "../ideas/callbag.ts"; 3 | import * as M from "npm:@most/core@1.6.1"; 4 | import * as MS from "npm:@most/scheduler@1.3.0"; 5 | import * as R from "npm:rxjs@7.8.1"; 6 | import { pipe } from "../fn.ts"; 7 | 8 | const count = 1_000_000; 9 | const add = (a: number, b: number) => a + b; 10 | const passthrough = (_: number, value: number) => value; 11 | const runRx = (obs: R.Observable): Promise => 12 | new Promise((resolve, reject) => { 13 | obs.subscribe({ complete: resolve, error: reject }); 14 | }); 15 | function mostRange(count: number) { 16 | return M.newStream((snk) => { 17 | let ended = false; 18 | let index = -1; 19 | while (++index < count) { 20 | if (ended) { 21 | break; 22 | } 23 | snk.event(0, index); 24 | } 25 | if (!ended) { 26 | snk.end(0); 27 | } 28 | return { 29 | dispose: () => { 30 | ended = true; 31 | }, 32 | }; 33 | }); 34 | } 35 | 36 | Deno.bench("stream scan", { group: "scan" }, async () => { 37 | await pipe( 38 | S.range(count), 39 | S.scan(add, 0), 40 | S.scan(passthrough, 0), 41 | S.runPromise(S.DefaultEnv), 42 | ); 43 | }); 44 | 45 | Deno.bench("callbag scan", { group: "scan" }, async () => 46 | await pipe( 47 | CB.range(count), 48 | CB.scan(add, 0), 49 | CB.scan(passthrough, 0), 50 | CB.runPromise({ queueMicrotask }), 51 | )); 52 | 53 | Deno.bench("most scan", { group: "scan" }, async () => 54 | await pipe( 55 | mostRange(count), 56 | M.scan(add, 0), 57 | M.scan(passthrough, 0), 58 | (stream) => 59 | M.runEffects( 60 | stream, 61 | MS.newDefaultScheduler(), 62 | ), 63 | )); 64 | 65 | Deno.bench("rxjs scan", { group: "scan" }, async () => { 66 | await pipe( 67 | R.range(0, count), 68 | R.scan(add, 0), 69 | R.scan(passthrough, 0), 70 | runRx, 71 | ); 72 | }); 73 | 74 | const JOIN_COUNT = 1_000; 75 | 76 | Deno.bench("stream join", { group: "join" }, async () => { 77 | await pipe( 78 | S.range(JOIN_COUNT), 79 | S.flatmap(() => S.range(JOIN_COUNT)), 80 | S.runPromise(S.DefaultEnv), 81 | ); 82 | }); 83 | 84 | Deno.bench("most join", { group: "join" }, async () => { 85 | await pipe( 86 | mostRange(JOIN_COUNT), 87 | M.chain(() => mostRange(JOIN_COUNT)), 88 | (stream) => 89 | M.runEffects( 90 | stream, 91 | MS.newDefaultScheduler(), 92 | ), 93 | ); 94 | }); 95 | 96 | Deno.bench("rxjs join", { group: "join" }, async () => { 97 | await pipe( 98 | R.range(0, JOIN_COUNT), 99 | R.mergeMap(() => R.range(0, JOIN_COUNT)), 100 | runRx, 101 | ); 102 | }); 103 | -------------------------------------------------------------------------------- /bimappable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bimappable is a structure that allows a function to be applied inside of the 3 | * associated concrete structure but is specific to a second held value of that 4 | * structure.. 5 | * 6 | * @module Bimappable 7 | * @since 2.0.0 8 | */ 9 | 10 | import type { $, Hold, Kind } from "./kind.ts"; 11 | import type { Mappable } from "./mappable.ts"; 12 | 13 | /** 14 | * The Bimappable interface. Bimapple includes the methods map and mapSecond. 15 | * 16 | * @since 2.0.0 17 | */ 18 | export interface Bimappable extends Mappable, Hold { 19 | readonly mapSecond: ( 20 | fbj: (value: B) => J, 21 | ) => ( 22 | ta: $, 23 | ) => $; 24 | } 25 | -------------------------------------------------------------------------------- /boolean.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The boolean module contains combinators for working with boolean. 3 | * 4 | * @module number 5 | * @since 2.0.0 6 | */ 7 | 8 | import type { Ordering, Sortable } from "./sortable.ts"; 9 | import type { Combinable } from "./combinable.ts"; 10 | import type { Initializable } from "./initializable.ts"; 11 | import type { Comparable } from "./comparable.ts"; 12 | import type { Showable } from "./showable.ts"; 13 | 14 | import { fromSort } from "./sortable.ts"; 15 | 16 | /** 17 | * A function that always returns true. 18 | * 19 | * @example 20 | * ```ts 21 | * import { constTrue } from "./boolean.ts"; 22 | * 23 | * const result = constTrue(); // true 24 | * ``` 25 | * 26 | * @since 2.0.0 27 | */ 28 | export const constTrue = () => true; 29 | 30 | /** 31 | * A function that always returns false. 32 | * 33 | * @example 34 | * ```ts 35 | * import { constFalse } from "./boolean.ts"; 36 | * 37 | * const result = constFalse(); // false 38 | * ``` 39 | * 40 | * @since 2.0.0 41 | */ 42 | export const constFalse = () => false; 43 | 44 | /** 45 | * A type guard, indicating that a value is a boolean. 46 | * 47 | * @since 2.0.0 48 | */ 49 | export function isBoolean(a: unknown): a is boolean { 50 | return typeof a === "boolean"; 51 | } 52 | 53 | /** 54 | * Create a match function over boolean 55 | * 56 | * @example 57 | * ```ts 58 | * import { match } from "./boolean.ts"; 59 | * import * as O from "./option.ts"; 60 | * 61 | * const match1 = match(() => "Tina", () => "Turner"); 62 | * const match2 = match(() => O.some("Hi"), () => O.none); 63 | * 64 | * const result1 = match1(true); // "Tina" 65 | * const result2 = match1(false); // "Turner" 66 | * const result3 = match2(true); // Some("Hi") 67 | * const result4 = match2(false); // None 68 | * ``` 69 | * 70 | * @since 2.0.0 71 | */ 72 | export function match( 73 | onTrue: () => A, 74 | onFalse: () => B, 75 | ): (a: boolean) => A | B { 76 | return (a) => a ? onTrue() : onFalse(); 77 | } 78 | 79 | /** 80 | * Negate a given boolean. 81 | * 82 | * @example 83 | * ```ts 84 | * import { not } from "./boolean.ts"; 85 | * 86 | * const result1 = not(true); // false 87 | * const result2 = not(false); // true 88 | * ``` 89 | * 90 | * @since 2.0.0 91 | */ 92 | export function not(ua: boolean): boolean { 93 | return !ua; 94 | } 95 | 96 | /** 97 | * Test the equality of two booleans. 98 | * 99 | * @example 100 | * ```ts 101 | * import { compare } from "./boolean.ts"; 102 | * 103 | * const result1 = compare(true)(false); // false 104 | * const result2 = compare(true)(true); // true 105 | * ``` 106 | * 107 | * @since 2.0.0 108 | */ 109 | export function compare(second: boolean): (first: boolean) => boolean { 110 | return (first) => first === second; 111 | } 112 | 113 | /** 114 | * Compares two booleans, returning an Ordering. True is greater than 115 | * False in this ordering, generally, but the specifics are always 116 | * decided by the runtime. 117 | * 118 | * @example 119 | * ```ts 120 | * import { sort } from "./boolean.ts"; 121 | * 122 | * const result1 = sort(true, true); // 0 123 | * const result2 = sort(true, false); // 1 124 | * const result3 = sort(false, true); // -1 125 | * ``` 126 | * 127 | * @since 2.0.0 128 | */ 129 | export function sort(first: boolean, second: boolean): Ordering { 130 | return first < second ? -1 : second < first ? 1 : 0; 131 | } 132 | 133 | /** 134 | * A curried form of logical Or. 135 | * 136 | * @example 137 | * ```ts 138 | * import { or } from "./boolean.ts"; 139 | * 140 | * const result1 = or(true)(true); // true 141 | * const result2 = or(true)(false); // true 142 | * const result3 = or(false)(false); // false 143 | * ``` 144 | * 145 | * @since 2.0.0 146 | */ 147 | export function or(second: boolean): (first: boolean) => boolean { 148 | return (first) => first || second; 149 | } 150 | 151 | /** 152 | * A curried form of logical And. 153 | * 154 | * @example 155 | * ```ts 156 | * import { and } from "./boolean.ts"; 157 | * 158 | * const result1 = and(true)(true); // true 159 | * const result2 = and(true)(false); // false 160 | * const result3 = and(false)(false); // false 161 | * ``` 162 | * 163 | * @since 2.0.0 164 | */ 165 | export function and(second: boolean): (first: boolean) => boolean { 166 | return (first) => first && second; 167 | } 168 | 169 | /** 170 | * The canonical implementation of Sortable for boolean. It contains 171 | * the method lt, lte, equals, gte, gt, min, max, clamp, between, 172 | * and compare. 173 | * 174 | * @since 2.0.0 175 | */ 176 | export const SortableBoolean: Sortable = fromSort(sort); 177 | 178 | /** 179 | * The canonical implementation of Comparable for boolean. It contains 180 | * the method equals. 181 | * 182 | * @since 2.0.0 183 | */ 184 | export const ComparableBoolean: Comparable = { compare }; 185 | 186 | /** 187 | * The canonical implementation of Combinable for boolean that 188 | * combines using the logical and operator. It contains the 189 | * method combine. 190 | * 191 | * @since 2.0.0 192 | */ 193 | export const CombinableBooleanAll: Combinable = { 194 | combine: and, 195 | }; 196 | 197 | /** 198 | * The canonical implementation of Combinable for boolean that 199 | * combines using the logical or operator. It contains the 200 | * method combine. 201 | * 202 | * @since 2.0.0 203 | */ 204 | export const CombinableBooleanAny: Combinable = { 205 | combine: or, 206 | }; 207 | 208 | /** 209 | * The canonical implementation of Initializable for boolean that 210 | * combines using the logical and operator. It contains the 211 | * method combine. 212 | * 213 | * @since 2.0.0 214 | */ 215 | export const InitializableBooleanAll: Initializable = { 216 | combine: and, 217 | init: constTrue, 218 | }; 219 | 220 | /** 221 | * The canonical implementation of Initializable for boolean that 222 | * combines using the logical or operator. It contains the 223 | * method combine. 224 | * 225 | * @since 2.0.0 226 | */ 227 | export const InitializableBooleanAny: Initializable = { 228 | combine: or, 229 | init: constFalse, 230 | }; 231 | 232 | /** 233 | * The canoncial implementation of Showable for boolean. It 234 | * uses JSON.stringify to turn a boolean into a string. 235 | * It contains the method show. 236 | */ 237 | export const ShowableBoolean: Showable = { 238 | show: JSON.stringify, 239 | }; 240 | -------------------------------------------------------------------------------- /composable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Composable is a structure that allows two algebraic structures to be composed 3 | * one into the other. It is a supertype of Combinable but operates on a Kind 4 | * instead of a concrete type. 5 | * 6 | * @module Composable 7 | * @since 2.0.0 8 | */ 9 | 10 | import type { $, Hold, Kind } from "./kind.ts"; 11 | 12 | /** 13 | * Composable is a structure that allows the composition of two algebraic 14 | * structures that have in and out fields. It also allows for the initialization 15 | * of algebraic structures, in effect identity. In other functional libraries 16 | * this is called a Category. 17 | * 18 | * @since 2.0.0 19 | */ 20 | export interface Composable extends Hold { 21 | readonly id: () => $; 22 | readonly compose: ( 23 | second: $, 24 | ) => ( 25 | first: $, 26 | ) => $; 27 | } 28 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@baetheus/fun", 3 | "version": "3.0.0-rc.3", 4 | "exports": { 5 | "./applicable": "./applicable.ts", 6 | "./array": "./array.ts", 7 | "./async": "./async.ts", 8 | "./async_either": "./async_either.ts", 9 | "./async_iterable": "./async_iterable.ts", 10 | "./bimappable": "./bimappable.ts", 11 | "./boolean": "./boolean.ts", 12 | "./combinable": "./combinable.ts", 13 | "./comparable": "./comparable.ts", 14 | "./composable": "./composable.ts", 15 | "./datum": "./datum.ts", 16 | "./datum_either": "./datum_either.ts", 17 | "./decoder": "./decoder.ts", 18 | "./effect": "./effect.ts", 19 | "./either": "./either.ts", 20 | "./failable": "./failable.ts", 21 | "./filterable": "./filterable.ts", 22 | "./flatmappable": "./flatmappable.ts", 23 | "./fn": "./fn.ts", 24 | "./fn_either": "./fn_either.ts", 25 | "./foldable": "./foldable.ts", 26 | "./free": "./free.ts", 27 | "./identity": "./identity.ts", 28 | "./initializable": "./initializable.ts", 29 | "./iterable": "./iterable.ts", 30 | "./json_schema": "./json_schema.ts", 31 | "./kind": "./kind.ts", 32 | "./map": "./map.ts", 33 | "./mappable": "./mappable.ts", 34 | "./newtype": "./newtype.ts", 35 | "./nil": "./nil.ts", 36 | "./number": "./number.ts", 37 | "./optic": "./optic.ts", 38 | "./option": "./option.ts", 39 | "./pair": "./pair.ts", 40 | "./parser": "./parser.ts", 41 | "./predicate": "./predicate.ts", 42 | "./premappable": "./premappable.ts", 43 | "./promise": "./promise.ts", 44 | "./record": "./record.ts", 45 | "./refinement": "./refinement.ts", 46 | "./schemable": "./schemable.ts", 47 | "./set": "./set.ts", 48 | "./showable": "./showable.ts", 49 | "./sortable": "./sortable.ts", 50 | "./state": "./state.ts", 51 | "./state_either": "./state_either.ts", 52 | "./stream": "./stream.ts", 53 | "./string": "./string.ts", 54 | "./sync": "./sync.ts", 55 | "./sync_either": "./sync_either.ts", 56 | "./these": "./these.ts", 57 | "./traversable": "./traversable.ts", 58 | "./tree": "./tree.ts", 59 | "./wrappable": "./wrappable.ts", 60 | "./ideas/callbag": "./ideas/callbag.ts", 61 | "./ideas/dux": "./ideas/dux.ts" 62 | }, 63 | "include": [ 64 | "LICENSE", 65 | "README.md", 66 | "deno.json" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /examples/arrow.ts: -------------------------------------------------------------------------------- 1 | import type { $, Kind } from "../kind.ts"; 2 | 3 | export type Arrow< 4 | U extends Kind, 5 | A, 6 | I, 7 | J = never, 8 | K = never, 9 | L = unknown, 10 | M = unknown, 11 | > = (a: A) => $; 12 | 13 | export type LiftFn = < 14 | A, 15 | I, 16 | J = never, 17 | K = never, 18 | L = unknown, 19 | M = unknown, 20 | >(fai: (a: A) => I) => Arrow; 21 | -------------------------------------------------------------------------------- /examples/async_clone.ts: -------------------------------------------------------------------------------- 1 | import { tee } from "https://raw.githubusercontent.com/denoland/deno_std/0.136.0/async/tee.ts"; 2 | 3 | import * as A from "../async_iterable.ts"; 4 | import { pipe } from "../fn.ts"; 5 | 6 | // Creates a stateful AsyncIterable 7 | const makeTest = (value: number) => ({ 8 | value, 9 | async *[Symbol.asyncIterator]() { 10 | while (true) { 11 | yield this.value; 12 | this.value++; 13 | } 14 | }, 15 | }); 16 | 17 | // Iterates over an asyncIterable logging (with a name) 18 | // every entry 19 | async function run(name: string, ta: AsyncIterable) { 20 | for await (const a of ta) { 21 | console.log(name, a); 22 | } 23 | } 24 | 25 | // A stateful asyncIterable 26 | const test = makeTest(0); 27 | 28 | // Create a "stateless" asyncIterable 29 | const cloned = pipe( 30 | A.clone(test), 31 | A.take(5), 32 | ); 33 | 34 | // Take a few off the top of the orig 35 | await run("[orig-pre]", pipe(test, A.take(5))); 36 | 37 | // Run three copies of the stateless asyncIterables simultaineously 38 | run("[one]", cloned); 39 | run("[two]", cloned); 40 | 41 | await run("[twe]", cloned); 42 | 43 | // Run the original (it should pick up at 5 44 | await run("[orig]", pipe(test, A.take(5))); 45 | 46 | // Let's try the same with tee 47 | const test2 = makeTest(0); 48 | const [branch1, branch2] = tee(test2); 49 | 50 | await run("[tee-orig-pre]", pipe(test2, A.take(5))); 51 | 52 | run("[tee-branch1]", pipe(branch1, A.take(5))); 53 | await run("[tee-branch2]", pipe(branch2, A.take(5))); 54 | 55 | run("[tee-orig]", pipe(test2, A.take(5))); 56 | 57 | // Let's see about a bug 58 | const test3 = makeTest(0); 59 | const syncTest = A.clone(test3); 60 | const iter = syncTest[Symbol.asyncIterator](); 61 | console.log(await Promise.all([iter.next(), iter.next()])); 62 | -------------------------------------------------------------------------------- /examples/async_either.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import * as TE from "../async_either.ts"; 4 | import * as A from "../async.ts"; 5 | import { flow, identity } from "../fn.ts"; 6 | 7 | const hello = flow( 8 | TE.match(() => "World", identity), 9 | A.map((name) => `Hello ${name}!`), 10 | ); 11 | 12 | assertEquals(await hello(TE.right("Functional!"))(), "Hello Functional!!"); 13 | assertEquals(await hello(TE.left(Error))(), "Hello World!"); 14 | -------------------------------------------------------------------------------- /examples/comparable.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "../fn.ts"; 2 | import * as C from "../comparable.ts"; 3 | 4 | const Struct = pipe( 5 | C.struct({ one: C.boolean }), 6 | C.intersect(C.partial({ two: C.number })), 7 | ); 8 | type Struct = C.TypeOf; 9 | 10 | const tests: [Struct, Struct][] = [[{ one: true, two: 1 }, { 11 | one: true, 12 | two: 2, 13 | }]]; 14 | 15 | tests.forEach(([first, second], index) => { 16 | console.log(`Test ${index}`, { 17 | first, 18 | second, 19 | compare: Struct.compare(second)(first), 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/failable.ts: -------------------------------------------------------------------------------- 1 | import type { Kind } from "../kind.ts"; 2 | import type { Failable } from "../failable.ts"; 3 | 4 | export function createTryAll(F: Failable) { 5 | } 6 | -------------------------------------------------------------------------------- /examples/fetch_archives.ts: -------------------------------------------------------------------------------- 1 | import * as D from "../decoder.ts"; 2 | import * as E from "../either.ts"; 3 | import * as S from "../schemable.ts"; 4 | import * as AE from "../async_either.ts"; 5 | import * as O from "../optic.ts"; 6 | import * as J from "../json_schema.ts"; 7 | import { flow, pipe } from "../fn.ts"; 8 | 9 | // Let's start by defining some error types and making some constructors 10 | type FetchError = { type: "FetchError"; error: unknown }; 11 | const fetchError = (error: unknown): FetchError => ({ 12 | type: "FetchError", 13 | error, 14 | }); 15 | 16 | type DecodeError = { type: "DecodeError"; error: string }; 17 | const decodeError = (error: string): DecodeError => ({ 18 | type: "DecodeError", 19 | error, 20 | }); 21 | 22 | type MyErrors = FetchError | DecodeError; 23 | 24 | const foldErrors = ( 25 | onFetchError: (e: unknown) => O, 26 | onDecodeError: (e: string) => O, 27 | ) => 28 | (e: MyErrors) => { 29 | switch (e.type) { 30 | case "FetchError": 31 | return onFetchError(e.error); 32 | case "DecodeError": 33 | return onDecodeError(e.error); 34 | } 35 | }; 36 | 37 | /** 38 | * Let's make a helper function for fetch. This one takes the same 39 | * inputs as fetch, but does the following: 40 | * 41 | * * Wraps fetch in a TaskEither instance 42 | * * Parses the response to json 43 | * * Takes any errors and types them as FetchError 44 | */ 45 | const fetchAsyncEither = AE.tryCatch(fetch, fetchError); 46 | 47 | /** 48 | * Let's make another helper function that takes a Response 49 | * and parses it as JSON. The default json() method on response 50 | * returns a Promise which is not constrained enough for our 51 | * usage, so we type cast it to Promise 52 | */ 53 | const jsonResponse = AE.tryCatch( 54 | (res: Response): Promise => res.json(), 55 | fetchError, 56 | ); 57 | 58 | /** 59 | * Here is an example of a Decoder. The intersect combinator is generally useful 60 | * for "merging" two decoders together. In this case type contains the required 61 | * properties on a Document and partial contains the optional ones. 62 | */ 63 | const Document = S.schema((d) => 64 | pipe( 65 | d.struct({ 66 | title: d.string(), 67 | downloads: d.number(), 68 | format: d.array(d.string()), 69 | identifier: d.string(), 70 | item_size: d.number(), 71 | mediatype: d.string(), 72 | }), 73 | d.intersect( 74 | d.partial({ 75 | collection: d.array(d.string()), // It's decoders all the way down. 76 | backup_location: d.string(), 77 | language: d.string(), 78 | date: d.string(), 79 | description: pipe(d.string(), d.union(d.array(d.string()))), 80 | creator: pipe(d.string(), d.union(d.array(d.string()))), 81 | month: d.number(), 82 | week: d.number(), 83 | year: pipe(d.string(), d.union(d.number())), 84 | }), 85 | ), 86 | ) 87 | ); 88 | // TypeOf extracts the type from the decoder so 89 | // we don't have to do double the work keeping decoders 90 | // in sync with type declarations 91 | type Document = S.TypeOf; 92 | 93 | // A response from archive contains many documents and 94 | // some metadata 95 | const ArchiveResponse = S.schema((s) => 96 | s.struct({ 97 | numFound: s.number(), 98 | start: s.number(), 99 | docs: s.array(Document(s)), 100 | }) 101 | ); 102 | type ArchiveResponse = S.TypeOf; 103 | 104 | // The happy path response from archive gives this response 105 | const QueryResponse = S.schema((s) => 106 | s.struct({ 107 | response: ArchiveResponse(s), // Notice that Decoders are composable even when you make your own 108 | }) 109 | ); 110 | type QueryResponse = S.TypeOf; 111 | 112 | // A helper function that builds a searchURL for us 113 | const searchUrl = (term: string) => 114 | [ 115 | "https://archive.org/advancedsearch.php?q=", 116 | encodeURIComponent(term), 117 | "&output=json", 118 | ].join(""); 119 | 120 | // Decode a queryResponse and lift a result into an AsyncEither 121 | const decodeQueryResponse = flow( 122 | QueryResponse(D.SchemableDecoder), // Try to decode an unknown into a QueryResponse 123 | E.mapSecond(flow(D.draw, decodeError)), // Wrap the error in a DecodeError 124 | AE.fromEither, // Lift the Either response into an AsyncEither 125 | ); 126 | 127 | /** 128 | * Now we have the building blocks to build the fetching part of our flow 129 | */ 130 | const fetchFromArchive = flow( 131 | searchUrl, // First build a search URL 132 | fetchAsyncEither, // Then apply it to fetch, wrapping the response 133 | AE.flatmap(jsonResponse), // Then decode the response as json if successful 134 | AE.flatmap(decodeQueryResponse), // Then decode the result as a QueryResponse 135 | ); 136 | 137 | // If we just wanted to print the response we could do so like this 138 | // fetchFromArchive("Chimamanda")().then(E.fold(console.error, console.log)); 139 | 140 | // Instead, let's say we are only interested in the titles, dates, and 141 | // descriptions. We can build an "optic" that digs into the Query response 142 | // for the data we want 143 | 144 | const getData = pipe( 145 | O.id(), // Start with a lens on the QueryResponse 146 | O.prop("response"), // Focus on the response field 147 | O.prop("docs"), // Then on the docs field 148 | O.array, 149 | O.props("title", "date", "description"), // We only want to focus on title, date, and description 150 | ); 151 | 152 | // The last part is to run the query and map the response to the getData function 153 | pipe( 154 | fetchFromArchive("Chimamanda"), // Fetch and Parse a request for items with Chimamanda in them 155 | AE.map((n) => getData.view(n)), // Take any "good" response and extract an array of titles, dates, and descriptions 156 | // This line actually runs our program and outputs the results 157 | // to either stderr or stdout depending on the result 158 | AE.match( 159 | foldErrors((e) => console.error("FetchError", e), console.error), 160 | console.log, 161 | ), 162 | )(); 163 | 164 | // While we're at it let's print out the JsonSchema for the response! 165 | console.log( 166 | "Following is the JsonSchema for the QueryResponse form archive.org", 167 | ); 168 | 169 | const jsonSchemaTemplate = QueryResponse(J.SchemableJsonBuilder); // Create a JsonSchema template 170 | const jsonSchema = J.print(jsonSchemaTemplate); // JsonSchema objects 171 | 172 | console.log(JSON.stringify(jsonSchema, null, 2)); // Print it to console! 173 | 174 | /** 175 | * Summary 176 | * * We validated the response from a fetch call 177 | * * We differentiated between FetchErrors and DecodeErrors 178 | * * We derived the QueryResponse type from the validator 179 | * * We used functional lenses to focus deep into the 180 | * QueryResponse to get only the titles, dates, and 181 | * descriptions we care about 182 | * * We output a clear validation error when decoded 183 | * data is incorrect. 184 | */ 185 | -------------------------------------------------------------------------------- /examples/flow.ts: -------------------------------------------------------------------------------- 1 | import { flow } from "../fn.ts"; 2 | import * as P from "../promise.ts"; 3 | 4 | const asyncAddOne = (n: number) => P.of(n + 1); 5 | 6 | const test = flow( 7 | (n: number) => P.of(n), 8 | P.then(asyncAddOne), 9 | ); // (n: number) => Promise 10 | -------------------------------------------------------------------------------- /examples/freer.ts: -------------------------------------------------------------------------------- 1 | import { flow, identity, pipe, todo } from "https://deno.land/x/fun/fn.ts"; 2 | 3 | export type Substitutions = { 4 | readonly ["covariant"]: unknown[]; 5 | readonly ["contravariant"]: unknown[]; 6 | readonly ["invariant"]: unknown[]; 7 | }; 8 | 9 | export interface Kind extends Substitutions { 10 | readonly kind?: unknown; 11 | } 12 | 13 | export type Substitute = T extends 14 | { readonly kind: unknown } ? (T & S)["kind"] 15 | : { 16 | readonly T: T; 17 | readonly ["covariant"]: () => S["covariant"]; 18 | readonly ["contravariant"]: (_: S["contravariant"]) => void; 19 | readonly ["invariant"]: (_: S["invariant"]) => S["invariant"]; 20 | }; 21 | 22 | export type $< 23 | T, 24 | Out extends unknown[], 25 | In extends unknown[] = [never], 26 | InOut extends unknown[] = [never], 27 | > = Substitute< 28 | T, 29 | { ["covariant"]: Out; ["contravariant"]: In; ["invariant"]: InOut } 30 | >; 31 | 32 | /** 33 | * First, defint state for testing. 34 | * 35 | * newtype State s a = State{unState :: s -> (a,s)} 36 | 37 | get :: State s s 38 | get = State $ \s -> (s,s) 39 | 40 | put :: s -> State s () 41 | put s = State $ \_ -> ((),s) 42 | 43 | runState :: State s a -> s -> (a,s) 44 | runState = unState 45 | */ 46 | 47 | export type State = (s: S) => [A, S]; 48 | 49 | export function get(): State { 50 | return (s) => [s, s]; 51 | } 52 | 53 | export function put(s: S): State { 54 | return () => [void 0, s]; 55 | } 56 | 57 | export interface KindState extends Kind { 58 | readonly kind: State; 59 | } 60 | 61 | // === Free === 62 | // data Free f a where 63 | // Pure :: a -> Free f a 64 | // Impure :: f (Free f a) -> Free f a 65 | 66 | // === Freer === 67 | // data FFree g a where 68 | // FPure :: a -> FFree g a 69 | // FImpure :: g x -> (x -> FFree g a) -> FFree g a 70 | 71 | export type Pure<_G, A> = { 72 | readonly tag: "Pure"; 73 | readonly value: A; 74 | }; 75 | 76 | export type Impure = { 77 | readonly tag: "Impure"; 78 | readonly from: $; 79 | readonly to: (d: D) => Freer; 80 | }; 81 | 82 | // deno-lint-ignore no-explicit-any 83 | export type Freer = Pure | Impure; 84 | 85 | export function pure(value: A): Freer { 86 | return { tag: "Pure", value }; 87 | } 88 | 89 | export function impure( 90 | from: $, 91 | to: (d: D) => Freer, 92 | ): Freer { 93 | return { tag: "Impure", from, to }; 94 | } 95 | 96 | export function isPure(ua: Freer): ua is Pure { 97 | return ua.tag === "Pure"; 98 | } 99 | 100 | export function of(value: A): Freer { 101 | return pure(value); 102 | } 103 | 104 | export function map(fai: (a: A) => I) { 105 | return (ua: Freer): Freer => 106 | isPure(ua) 107 | ? pure(fai(ua.value)) 108 | : impure(ua.from, (a) => map(fai)(ua.to(a))); 109 | } 110 | 111 | export function chain(faui: (a: A) => Freer) { 112 | return (ua: Freer): Freer => 113 | isPure(ua) ? faui(ua.value) : impure(ua.from, (a) => chain(faui)(ua.to(a))); 114 | } 115 | 116 | export function ap(ufai: Freer I>) { 117 | return (ua: Freer): Freer => 118 | pipe(ufai, chain((fai: (a: A) => I) => pipe(ua, map(fai)))); 119 | } 120 | 121 | export function join(uua: Freer>): Freer { 122 | return pipe(uua, chain(identity)); 123 | } 124 | 125 | // etaF :: g a -> FFree g a 126 | // etaF fa = FImpure fa FPure 127 | 128 | export function eta(from: $): Freer { 129 | return impure(from, (a) => pure(a)); 130 | } 131 | 132 | /** 133 | * Define State in terms of Freer 134 | * 135 | * type FFState s = FFree (State s) 136 | */ 137 | 138 | export type FState = Freer, A>; 139 | 140 | export function fget(): FState { 141 | return eta(get()); 142 | } 143 | 144 | export function fput(s: S): FState { 145 | return eta(put(s)); 146 | } 147 | 148 | // Interpreter 149 | // runFFState :: FFState s a -> s -> (a,s) 150 | // runFFState (FPure x) s = (x,s) 151 | // runFFState (FImpure m q) s = let (x,s') = unState m s in runFFState (q x) s' 152 | 153 | export function runState(fstate: FState): State { 154 | switch (fstate.tag) { 155 | case "Pure": 156 | return (s) => [fstate.value, s]; 157 | case "Impure": 158 | return flow( 159 | fstate.from, 160 | ([a, s]) => runState(fstate.to(a))(s), 161 | ); 162 | } 163 | } 164 | 165 | const computation = pipe( 166 | fget(), 167 | chain((n) => fput(n + 1)), 168 | ); 169 | 170 | const result1 = pipe(0, runState(computation)); 171 | const result2 = pipe(2, runState(computation)); 172 | 173 | console.log({ result1, result2 }); 174 | 175 | /** 176 | * Define Option in terms of Freer 177 | */ 178 | 179 | export type None = { readonly tag: "None" }; 180 | export type Some = { readonly tag: "Some"; readonly value: A }; 181 | export type Option = None | Some; 182 | 183 | export interface KindOption extends Kind { 184 | readonly kind: Option; 185 | } 186 | 187 | export type FOption = Freer; 188 | 189 | export function none(): Option { 190 | return { tag: "None" }; 191 | } 192 | 193 | export function some(value: A): Option { 194 | return { tag: "Some", value }; 195 | } 196 | 197 | export function fnone(): FOption { 198 | return eta(none()); 199 | } 200 | 201 | export function fsome(value: A): FOption { 202 | return eta(some(value)); 203 | } 204 | 205 | export function runOption(ua: FOption): Option { 206 | switch (ua.tag) { 207 | case "Pure": 208 | return some(ua.value); 209 | case "Impure": { 210 | switch (ua.from.tag) { 211 | case "None": 212 | return ua.from; 213 | case "Some": 214 | return runOption(ua.to(ua.from.value)); 215 | } 216 | } 217 | } 218 | } 219 | 220 | const op1 = pipe( 221 | fsome("Hello"), 222 | chain((s) => fsome(s + s)), 223 | map((s) => s.length), 224 | map((s) => s + 1), 225 | ); 226 | 227 | const opr1 = runOption(op1); 228 | 229 | console.log({ opr1 }); 230 | 231 | const json = (a: A): string => JSON.stringify(a, null, 2); 232 | 233 | export function logOption(ua: FOption, acc = "Log Option"): string { 234 | switch (ua.tag) { 235 | case "Pure": 236 | return `${acc}\n${json(ua.value)}`; 237 | case "Impure": { 238 | const { from, to } = ua; 239 | switch (from.tag) { 240 | case "None": 241 | return `${acc}\n${json(from)}`; 242 | case "Some": 243 | return logOption(to(from.value), `${acc}\n${json(from)}`); 244 | } 245 | } 246 | } 247 | } 248 | 249 | const opr2 = logOption(op1); 250 | 251 | console.log(opr2); 252 | -------------------------------------------------------------------------------- /examples/hkts.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any ban-types 2 | import { pipe } from "../fn.ts"; 3 | 4 | /** 5 | * This is here just to show a different path for 6 | * higher kinded types in typescript. This approach 7 | * is technically very flexible and requires very 8 | * little type magic to get working. 9 | * 10 | * The downside is that it cases all named types 11 | * into their consituent types, which breaks 12 | * in places where structural typing breaks 13 | * and provides a less readable development 14 | * experience. 15 | */ 16 | 17 | // --- 18 | // Higher Kinded Types a la pelotom 19 | // --- 20 | 21 | declare const index: unique symbol; 22 | 23 | export interface _ { 24 | [index]: N; 25 | } 26 | export type _0 = _<0>; 27 | export type _1 = _<1>; 28 | export type _2 = _<2>; 29 | export type _3 = _<3>; 30 | export type _4 = _<4>; 31 | export type _5 = _<5>; 32 | export type _6 = _<6>; 33 | export type _7 = _<7>; 34 | export type _8 = _<8>; 35 | export type _9 = _<9>; 36 | 37 | declare const Fix: unique symbol; 38 | 39 | export interface Fix { 40 | [Fix]: T; 41 | } 42 | 43 | export type Primitives = 44 | | null 45 | | undefined 46 | | boolean 47 | | number 48 | | bigint 49 | | string 50 | | symbol; 51 | 52 | export type $ = T extends Fix ? F 53 | : T extends _ ? S[N] 54 | : T extends any[] ? { [K in keyof T]: $ } 55 | : T extends (...as: infer AS) => infer R ? (...as: $) => $ 56 | : T extends Promise ? Promise<$> 57 | : T extends object ? { [K in keyof T]: $ } 58 | : T extends Primitives ? T 59 | : never; 60 | 61 | // --- 62 | // Type Classes 63 | // --- 64 | 65 | export type FunctorFn = ( 66 | fai: (a: A) => I, 67 | ) => (ta: $) => $; 68 | 69 | export type ApplyFn = ( 70 | tfai: $ I]>, 71 | ) => (ta: $) => $; 72 | 73 | export type ApplicativeFn = (a: A) => $; 74 | 75 | export type TraversableFn = ( 76 | A: Applicative, 77 | ) => (faui: (a: A) => $) => (ta: $) => $]>; 78 | 79 | export type SemigroupFn = (left: T) => (right: T) => T; 80 | 81 | export type Functor = { map: FunctorFn }; 82 | export type Apply = Functor & { ap: ApplyFn }; 83 | export type Applicative = Functor & Apply & { 84 | of: ApplicativeFn; 85 | }; 86 | export type Traversable = Functor & { traverse: TraversableFn }; 87 | export type Semigroup = { concat: SemigroupFn }; 88 | 89 | // --- 90 | // Algebraic! 91 | // --- 92 | 93 | export type None = { tag: "None" }; 94 | export type Some = { tag: "Some"; value: A }; 95 | export type Option = None | Some; 96 | 97 | export const none: Option = { tag: "None" }; 98 | export const some = (value: A): Option => ({ tag: "Some", value }); 99 | 100 | export const map = (fai: (a: A) => I) => (ta: Option): Option => 101 | ta.tag === "Some" ? some(fai(ta.value)) : none; 102 | 103 | export const ap = 104 | (tfai: Option<(a: A) => I>) => (ta: Option): Option => 105 | tfai.tag === "Some" && ta.tag === "Some" 106 | ? some(tfai.value(ta.value)) 107 | : none; 108 | 109 | export const Applicative: Applicative> = { of: some, map, ap }; 110 | 111 | export const traverse = 112 | (A: Applicative) => 113 | (faui: (a: A) => $) => 114 | (ta: Option): $]> => 115 | ta.tag === "Some" ? pipe(faui(ta.value), A.map(some)) : A.of(none); 116 | 117 | export const Traversable: Traversable> = { map, traverse }; 118 | 119 | // Even with a simple Option this type gets ugly 120 | export const traverseOption = Traversable.traverse(Applicative); 121 | 122 | export const ApplicativePromise: Applicative> = { 123 | of: Promise.resolve, 124 | map: (fai) => (ta) => ta.then(fai), 125 | ap: (tfai) => (ta) => ta.then((a) => tfai.then((fai) => fai(a))), 126 | }; 127 | 128 | // Using traverse with the nice types and a primitive like number looks good 129 | export const traversePromise = traverse(ApplicativePromise); 130 | 131 | export type Left = { tag: "Left"; left: B }; 132 | export type Right = { tag: "Right"; right: A }; 133 | export type Either = Left | Right; 134 | export const left = (left: B): Either => ({ tag: "Left", left }); 135 | export const right = (right: A): Either => ({ 136 | tag: "Right", 137 | right, 138 | }); 139 | 140 | export const mapEither: FunctorFn> = (fai) => (ua) => 141 | ua.tag === "Right" ? right(fai(ua.right)) : ua; 142 | export const mapEitherT1 = pipe( 143 | right(1), 144 | mapEither((n) => n + 1), 145 | ); 146 | export const mapEitherT2 = pipe( 147 | left(1), 148 | mapEither((n: number) => n + 1), 149 | ); 150 | 151 | export const ApplicativeEither: Applicative> = { 152 | of: right, 153 | ap: (tfai: Either I>) => (ta: Either) => 154 | ta.tag === "Left" 155 | ? ta 156 | : (tfai.tag === "Left" ? tfai : right(tfai.right(ta.right))), 157 | map: (fai) => (ta) => ta.tag === "Right" ? right(fai(ta.right)) : ta, 158 | }; 159 | 160 | export const traverseEither = traverse(ApplicativeEither); 161 | 162 | export const SemigroupNumber: Semigroup = { 163 | concat: (right) => (left) => left + right, 164 | }; 165 | -------------------------------------------------------------------------------- /examples/most.ts: -------------------------------------------------------------------------------- 1 | import * as M from "../contrib/most.ts"; 2 | import { pipe } from "../fn.ts"; 3 | 4 | const stream = pipe( 5 | pipe(M.periodic(1000), M.scan((a) => a + 1, 0)), 6 | M.bindTo("seconds"), 7 | M.bind("timestamps", () => M.wrap(Date.now())), 8 | M.tap((a) => console.log(a)), 9 | M.take(10), 10 | ); 11 | 12 | // Strangely, this emits the first two events quickly. 13 | await M.runEffects(stream)(M.newDefaultScheduler()); 14 | -------------------------------------------------------------------------------- /examples/natural.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import type { $, Kind, TypeClass } from "../kind.ts"; 4 | 5 | import * as E from "../either.ts"; 6 | import * as O from "../option.ts"; 7 | import * as I from "../identity.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | /** 11 | * Here is *a* definition of NaturalTransformation using fun substitution. 12 | * For a deep understanding the wikipedia article is pretty good: 13 | * https://en.wikipedia.org/wiki/Natural_transformation 14 | * 15 | * However, while we are using category there here it's important to 16 | * note that our choices for categories are small and our "Functors" 17 | * are really Endofunctors (e.g. meaning that a Functor for Option maps 18 | * from the Option category back to the Option category). 19 | * 20 | * In a way you can think of NaturalTransformation as a Functor of 21 | * Functors *except* that a Functor can be used with any morphism (function) 22 | * where as a NaturalTransformation only maps from one specific category 23 | * to another. This is like saying a NaturalTransformation will map 24 | * Option to Either and not from ANY ADT to ANY other ADT (which it would 25 | * have to if it were actually a Functor of Functors). 26 | */ 27 | export interface Nat extends TypeClass<[U, V]> { 28 | transform: ( 29 | ua: $, 30 | ) => $; 31 | } 32 | 33 | /** 34 | * Now lets define some Natural Transformations. The rule we have to follow is: 35 | * 36 | * NaturalTransform . Functor === Functor . NaturalTransform 37 | * 38 | * This is like saying (U = Either, V = Option): 39 | * 40 | * pipe( 41 | * right("Hello World"), 42 | * Either.map(str => str.length), 43 | * NaturalTransformation.transform, 44 | * ) 45 | * 46 | * Must equal: 47 | * 48 | * pipe( 49 | * right("Hello World"), 50 | * NaturalTransform.transform, 51 | * Option.map(str => str.length), 52 | * ) 53 | */ 54 | 55 | export const NatEitherOption: Nat = { 56 | transform: (either) => E.isLeft(either) ? O.none : O.some(either.right), 57 | }; 58 | 59 | export const NatOptionEither: Nat> = { 60 | transform: (option) => 61 | O.isNone(option) ? E.left(void 0) : E.right(option.value), 62 | }; 63 | 64 | /** 65 | * Identity can be transformed into any category! 66 | */ 67 | export function getIdentityNat( 68 | of: (a: A) => $, 69 | ): Nat { 70 | return { transform: (a) => of(a) }; 71 | } 72 | 73 | export const NatIdentityOption = getIdentityNat(O.of); 74 | 75 | export const NatIdentityEither = getIdentityNat(E.of); 76 | 77 | const strLength = (str: string) => str.length; 78 | 79 | function test(option: O.Option) { 80 | const result1 = pipe( 81 | option, 82 | O.map(strLength), 83 | NatOptionEither.transform, 84 | ); 85 | 86 | const result2 = pipe( 87 | option, 88 | NatOptionEither.transform, 89 | E.map(strLength), 90 | ); 91 | 92 | assertEquals(result1, result2); 93 | 94 | console.log({ option, result1, result2 }); 95 | } 96 | 97 | test(O.some("Hello World")); 98 | test(O.none); 99 | -------------------------------------------------------------------------------- /examples/observable.ts: -------------------------------------------------------------------------------- 1 | import { OrdNumber } from "../number.ts"; 2 | import { contramap } from "../ord.ts"; 3 | import { binarySearch } from "../array.ts"; 4 | import { pipe } from "../fn.ts"; 5 | 6 | export type Time = number; // milliseconds 7 | export type Delay = number; // milliseconds 8 | export type Period = number; // milliseconds 9 | export type Offset = number; // milliseconds 10 | 11 | export interface Task { 12 | run(time: Time): void; 13 | dispose(): void; 14 | } 15 | 16 | export interface ScheduledTask { 17 | readonly task: Task; 18 | readonly time: Time; 19 | run(): void; 20 | dispose(): void; 21 | } 22 | 23 | export interface TimeSlot { 24 | time: Time; 25 | tasks: ScheduledTask[]; 26 | } 27 | 28 | export interface Timeline { 29 | readonly events: TimeSlot[]; 30 | } 31 | 32 | export interface Scheduler { 33 | currentTime(): Time; 34 | scheduleTask( 35 | offset: Offset, 36 | delay: Delay, 37 | period: Period, 38 | task: Task, 39 | ): ScheduledTask; 40 | relative(offset: Offset): Scheduler; 41 | cancel(task: ScheduledTask): void; 42 | } 43 | 44 | export interface Disposable { 45 | dispose(): void; 46 | } 47 | 48 | export interface Clock { 49 | /** 50 | * Returns the current time in milliseconds 51 | */ 52 | now(): Time; 53 | } 54 | 55 | export interface Sink { 56 | event(time: Time, value: A): void; 57 | end(time: Time): void; 58 | error(time: Time, err: Error): void; 59 | } 60 | 61 | export interface Stream { 62 | run(sink: Sink, scheduler: Scheduler): Disposable; 63 | } 64 | 65 | export type Handle = number; 66 | 67 | export interface Timer { 68 | now(): Time; 69 | setTimer(f: () => unknown, delayTime: Delay): Handle; 70 | clearTimer(timerHandle: Handle): void; 71 | } 72 | 73 | export type TaskRunner = (st: ScheduledTask) => unknown; 74 | 75 | /** 76 | * Clock 77 | * 78 | * Do not implement high resolution clock, stick with performance.now. 79 | */ 80 | 81 | export function relativeClock(clock: Clock, from: Time): Clock { 82 | return { now: () => clock.now() - from }; 83 | } 84 | 85 | export function performanceClock(): Clock { 86 | return relativeClock(performance, performance.now()); 87 | } 88 | 89 | /** 90 | * Timeline 91 | * 92 | * Do not implement removeAll 93 | */ 94 | 95 | // add(scheduledTask: ScheduledTask): void; 96 | // remove(scheduledTask: ScheduledTask): boolean; 97 | // isEmpty(): boolean; 98 | // nextArrival(): Time; 99 | // runTasks(time: Time, runTask: TaskRunner): void; 100 | 101 | export function timeslot(time: Time, tasks: ScheduledTask[]): TimeSlot { 102 | return { time, tasks }; 103 | } 104 | 105 | const OrdTimeSlot = pipe( 106 | OrdNumber, 107 | contramap((ts: TimeSlot) => ts.time), 108 | ); 109 | 110 | const searchTimeline = binarySearch(OrdTimeSlot); 111 | 112 | export function timeline(...events: TimeSlot[]): Timeline { 113 | return { events }; 114 | } 115 | 116 | // nextArrival(): number { 117 | // return this.isEmpty() ? Infinity : this.tasks[0].time 118 | // } 119 | 120 | // isEmpty(): boolean { 121 | // return this.tasks.length === 0 122 | // } 123 | 124 | // add(st: ScheduledTaskImpl): void { 125 | // insertByTime(st, this.tasks) 126 | // } 127 | 128 | // remove(st: ScheduledTaskImpl): boolean { 129 | // const i = binarySearch(getTime(st), this.tasks) 130 | 131 | // if (i >= 0 && i < this.tasks.length) { 132 | // const events = this.tasks[i].events 133 | // const at = findIndex(st, events) 134 | // if (at >= 0) { 135 | // events.splice(at, 1) 136 | // if (events.length === 0) { 137 | // this.tasks.splice(i, 1) 138 | // } 139 | // return true 140 | // } 141 | // } 142 | 143 | // return false 144 | // } 145 | 146 | // runTasks(t: Time, runTask: (task: ScheduledTaskImpl) => void): void { 147 | // const tasks = this.tasks 148 | // const l = tasks.length 149 | // let i = 0 150 | 151 | // while (i < l && tasks[i].time <= t) { 152 | // ++i 153 | // } 154 | 155 | // this.tasks = tasks.slice(i) 156 | 157 | // // Run all ready tasks 158 | // for (let j = 0; j < i; ++j) { 159 | // this.tasks = runReadyTasks(runTask, tasks[j].events, this.tasks) 160 | // } 161 | // } 162 | 163 | function getScheduledTaskTime(scheduledTask: ScheduledTask): Time { 164 | return Math.floor(scheduledTask.time); 165 | } 166 | 167 | export function addTask(task: ScheduledTask, events: TimeSlot[]) { 168 | const length = events.length; 169 | const time = getScheduledTaskTime(task); 170 | 171 | if (length === 0) { 172 | events.push(timeslot(time, [task])); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /examples/play.ts: -------------------------------------------------------------------------------- 1 | import { createBind, createTap } from "../flatmappable.ts"; 2 | import { createBindTo } from "../mappable.ts"; 3 | import { FlatmappableOption, MappableOption } from "../option.ts"; 4 | 5 | import { pipe } from "../fn.ts"; 6 | import * as O from "../option.ts"; 7 | 8 | const bind = createBind(FlatmappableOption); 9 | const tap = createTap(FlatmappableOption); 10 | const bindTo = createBindTo(MappableOption); 11 | 12 | const test = pipe( 13 | O.wrap(1), 14 | bindTo("one"), 15 | tap((n) => console.log(n)), 16 | bind("two", ({ one }) => O.wrap(one + one)), 17 | ); 18 | -------------------------------------------------------------------------------- /examples/profunctor_optics.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | // 3 | import type { $, Hold, In, Kind, Out } from "../kind.ts"; 4 | import type { Pair } from "../pair.ts"; 5 | import type { Mappable } from "../mappable.ts"; 6 | 7 | import * as O from "../option.ts"; 8 | import * as F from "../fn.ts"; 9 | import * as P from "../pair.ts"; 10 | import { flow, pipe, todo } from "../fn.ts"; 11 | 12 | export interface Profunctor { 13 | readonly dimap: ( 14 | fld: (l: L) => D, 15 | fai: (a: A) => I, 16 | ) => (ua: $) => $; 17 | } 18 | 19 | export interface Forget extends Hold { 20 | readonly runForget: (a: A) => R; 21 | } 22 | 23 | export interface KindForget extends Kind { 24 | readonly kind: Forget, In, Out>; 25 | } 26 | 27 | export const ProfunctorForget: Profunctor = { 28 | dimap: (fld, _fai) => (ua) => ({ 29 | runForget: flow(fld, ua.runForget), 30 | }), 31 | }; 32 | 33 | export interface Star { 34 | readonly runStar: ( 35 | x: X, 36 | ) => $; 37 | } 38 | 39 | export interface KindStar extends Kind { 40 | readonly kind: Star, Out>; 41 | } 42 | 43 | export function profunctorStar( 44 | M: Mappable, 45 | ): Profunctor> { 46 | return { 47 | dimap: (fld, fai) => (ua) => ({ 48 | // This should work but we need to do some type spelunking 49 | runStar: (flow(fld, ua.runStar, M.map(fai))) as any, 50 | }), 51 | }; 52 | } 53 | 54 | export interface Strong extends Profunctor { 55 | readonly first: < 56 | A, 57 | X = never, 58 | B = never, 59 | C = never, 60 | D = unknown, 61 | E = unknown, 62 | >( 63 | ua: $, 64 | ) => $, B, C], [Pair], [E]>; 65 | readonly second: < 66 | A, 67 | X = never, 68 | B = never, 69 | C = never, 70 | D = unknown, 71 | E = unknown, 72 | >( 73 | ua: $, 74 | ) => $, B, C], [Pair], [E]>; 75 | } 76 | 77 | export function strong({ dimap }: Profunctor): Strong { 78 | return { 79 | dimap, 80 | first: dimap(P.getFirst, (a) => P.pair(a, null as any)), 81 | second: dimap(P.getSecond, (a) => P.pair(null as any, a)), 82 | }; 83 | } 84 | 85 | export const ProfunctorStarFn: Profunctor> = profunctorStar( 86 | F.MappableFn, 87 | ); 88 | export const StrongStarFn: Strong> = strong( 89 | ProfunctorStarFn, 90 | ); 91 | 92 | // Hmm 93 | -------------------------------------------------------------------------------- /examples/schema.ts: -------------------------------------------------------------------------------- 1 | import * as FC from "npm:fast-check@3.14.0"; 2 | import { schema } from "../schemable.ts"; 3 | import { SchemableDecoder } from "../decoder.ts"; 4 | import { print, SchemableJsonBuilder } from "../json_schema.ts"; 5 | import { getSchemableArbitrary } from "../contrib/fast-check.ts"; 6 | import { pipe } from "../fn.ts"; 7 | 8 | const Vector = schema((s) => s.tuple(s.number(), s.number(), s.number())); 9 | 10 | const Asteroid = schema((s) => 11 | s.struct({ 12 | type: s.literal("asteroid"), 13 | location: Vector(s), 14 | mass: s.number(), 15 | }) 16 | ); 17 | 18 | const Planet = schema((s) => 19 | s.struct({ 20 | type: s.literal("planet"), 21 | location: Vector(s), 22 | mass: s.number(), 23 | population: s.number(), 24 | habitable: s.boolean(), 25 | }) 26 | ); 27 | 28 | const Rank = schema((s) => 29 | pipe( 30 | s.literal("captain"), 31 | s.union(s.literal("first mate")), 32 | s.union(s.literal("officer")), 33 | s.union(s.literal("ensign")), 34 | ) 35 | ); 36 | 37 | const CrewMember = schema((s) => 38 | s.struct({ 39 | name: s.string(), 40 | age: s.number(), 41 | rank: Rank(s), 42 | home: Planet(s), 43 | }) 44 | ); 45 | 46 | const Ship = schema((s) => 47 | s.struct({ 48 | type: s.literal("ship"), 49 | location: Vector(s), 50 | mass: s.number(), 51 | name: s.string(), 52 | crew: s.array(CrewMember(s)), 53 | }) 54 | ); 55 | 56 | const SpaceObject = schema((s) => 57 | pipe(Asteroid(s), s.union(Planet(s)), s.union(Ship(s))) 58 | ); 59 | 60 | const decoder = SpaceObject(SchemableDecoder); 61 | const arbitrary = SpaceObject(getSchemableArbitrary(FC)); 62 | const json_schema = print(SpaceObject(SchemableJsonBuilder)); 63 | 64 | const rands = FC.sample(arbitrary, 10); 65 | const checks = rands.map(decoder); 66 | 67 | console.log({ json_schema, rands, checks }); 68 | 69 | const intersect = schema((s) => 70 | pipe( 71 | s.struct({ one: s.number() }), 72 | s.intersect(s.partial({ two: s.string() })), 73 | ) 74 | ); 75 | 76 | const iarbitrary = intersect(getSchemableArbitrary(FC)); 77 | console.log("Intersect", FC.sample(iarbitrary, 20)); 78 | -------------------------------------------------------------------------------- /examples/store.ts: -------------------------------------------------------------------------------- 1 | import type { ReadonlyRecord } from "../record.ts"; 2 | import type { DatumEither } from "../datum_either.ts"; 3 | 4 | import * as D from "../ideas/dux.ts"; 5 | import * as O from "../optic.ts"; 6 | import * as P from "../promise.ts"; 7 | import * as S from "../stream.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | const BOOKS: ReadonlyRecord = { 11 | "a1": { id: "a1", author: "Tom Robbins", title: "Jitterbug Perfume" }, 12 | "a2": { 13 | id: "a2", 14 | author: "Elizabeth Moon", 15 | title: "The Deed of Paksenarrion", 16 | }, 17 | }; 18 | 19 | type Book = { 20 | readonly id: string; 21 | readonly author: string; 22 | readonly title: string; 23 | }; 24 | 25 | type MyState = { 26 | readonly books: ReadonlyRecord>; 27 | }; 28 | 29 | const INITIAL_STATE: MyState = { books: {} }; 30 | 31 | export const actionCreator = D.actionGroup("STORE"); 32 | 33 | export const getBookAction = actionCreator.async( 34 | "GET_BOOK", 35 | ); 36 | 37 | export const getBookReducer = D.asyncCollectionRecord( 38 | getBookAction, 39 | pipe(O.id(), O.prop("books")), 40 | (id) => 41 | pipe( 42 | O.id>>(), 43 | O.atKey(id), 44 | ), 45 | ); 46 | 47 | export const getBookEffect = D.onAction( 48 | getBookAction.pending, 49 | (id) => { 50 | return pipe( 51 | P.wait(Math.random() * 1000), 52 | P.map(() => 53 | id in BOOKS 54 | ? getBookAction.success({ params: id, result: BOOKS[id] }) 55 | : getBookAction.failure({ 56 | params: id, 57 | error: `Book with id ${id} does not exist`, 58 | }) 59 | ), 60 | ); 61 | }, 62 | ); 63 | 64 | const store = D.createStore(INITIAL_STATE, getBookReducer, getBookEffect); 65 | 66 | // State connection 1 67 | pipe( 68 | store.state, 69 | S.forEach((state) => console.log("OUTPUT 1", state)), 70 | ); 71 | 72 | // Optic to look at book at id a1 73 | const bookA1 = pipe( 74 | O.id(), 75 | O.prop("books"), 76 | O.atKey("a1"), 77 | O.some, 78 | O.success, 79 | O.props("title", "author"), 80 | ); 81 | 82 | // State connection 2: selecting book a1 83 | pipe( 84 | store.state, 85 | S.map(bookA1.view), 86 | S.forEach((state) => console.log("OUTPUT 2", state)), 87 | ); 88 | 89 | // After a second, get book a1 90 | setTimeout(() => { 91 | store.dispatch(getBookAction.pending("a1")); 92 | }, 1000); 93 | -------------------------------------------------------------------------------- /examples/tagged_error.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import * as RSS from "https://deno.land/x/rss@0.5.6/mod.ts"; 3 | import * as AE from "../async_either.ts"; 4 | import { pipe } from "../fn.ts"; 5 | 6 | // --- 7 | // Try implementing a reusable error type 8 | // --- 9 | 10 | export type Err = { 11 | readonly tag: "Error"; 12 | readonly type: T; 13 | readonly context: A; 14 | readonly error: unknown; 15 | }; 16 | 17 | export type AnyErr = Err; 18 | 19 | export function err( 20 | type: T, 21 | ): (error: unknown, context: A) => Err { 22 | return (error, context) => ({ tag: "Error", type, context, error }); 23 | } 24 | 25 | // --- 26 | // Use a little creative typing to extract tag and context pairs and 27 | // build a dynamically typed fold function 28 | // --- 29 | 30 | type ExtractTags = T extends Err ? Tag : never; 31 | 32 | type MatchTag = Tag extends string 33 | ? Errors extends Err ? Err : never 34 | : never; 35 | 36 | type MapFunc = T extends Err 37 | ? (error: unknown, context: V) => B 38 | : never; 39 | 40 | type ToRecord = { [K in ExtractTags]: MapFunc, B> }; 41 | 42 | export function foldErr( 43 | fns: ToRecord, 44 | ): (ta: T) => B { 45 | return (ta) => (fns[ta.type as keyof ToRecord])(ta.error, ta.context); 46 | } 47 | 48 | /** 49 | * Wrapping the errors for tryCatch tend to all look like 50 | * Err, so why not just tag the errors and reuse 51 | * the taggedError constructor. 52 | */ 53 | export const tagTryCatch = ( 54 | tag: T, 55 | f: (...as: A) => O | PromiseLike, 56 | ) => AE.tryCatch(f, err(tag)); 57 | 58 | // --- 59 | // Wrap external functions with tagged errors 60 | // --- 61 | 62 | export const parseFeed = tagTryCatch("RssError", RSS.parseFeed); 63 | export const safeFetch = tagTryCatch("FetchError", fetch); 64 | export const getText = tagTryCatch("TextError", (res: Response) => res.text()); 65 | export const stringify = tagTryCatch( 66 | "StringifyError", 67 | (a: A) => JSON.stringify(a, null, 2), 68 | ); 69 | 70 | // --- 71 | // Get some xml, parse it as rss, and log it 72 | // --- 73 | 74 | function logError(annotation: string): (err: unknown, args: A) => void { 75 | return (err, args) => { 76 | console.error(annotation); 77 | console.error({ err, args }); 78 | }; 79 | } 80 | 81 | export const run = pipe( 82 | // Start with a fetch 83 | safeFetch("https://hnrss.org/frontpage-blarg"), 84 | AE.bindTo("response"), 85 | // The default flatmap widens the left type, picking up the 86 | // additional Err types 87 | AE.bind("text", ({ response }) => getText(response)), 88 | // Parse the body text 89 | AE.bind("parsed", ({ text }) => parseFeed(text)), 90 | // Stringify feed values 91 | AE.bind("result", ({ parsed }) => stringify(parsed)), 92 | // Output the date 93 | AE.match( 94 | // Use the taggedError fold to extract all unioned tags 95 | foldErr({ 96 | "FetchError": logError("Failed during fetch"), 97 | "RssError": logError("Failed during RSS Parsing"), 98 | "TextError": logError("Failed while parsing text body"), 99 | "StringifyError": logError("Failed while stringifying result"), 100 | }), 101 | console.log, 102 | ), 103 | ); 104 | 105 | // Run the program 106 | await run(); 107 | -------------------------------------------------------------------------------- /examples/traverse.ts: -------------------------------------------------------------------------------- 1 | import * as A from "../array.ts"; 2 | import * as AS from "../async.ts"; 3 | import * as I from "../iterable.ts"; 4 | import * as T from "../tree.ts"; 5 | import { pipe } from "../fn.ts"; 6 | 7 | const traversePar = A.traverse(AS.FlatmappableAsync); 8 | const traverseSeq = A.traverse(AS.FlatmappableAsyncSeq); 9 | 10 | const addNumberToIndex = (a: number, i: number) => 11 | pipe(AS.wrap(a + i), AS.delay(100 * a)); 12 | 13 | const sumPar = traversePar(addNumberToIndex); 14 | const sumSeq = traverseSeq(addNumberToIndex); 15 | 16 | const asyncPar = sumPar([1, 2, 3, 4, 5]); 17 | const asyncSeq = sumSeq([1, 2, 3, 4, 5]); 18 | 19 | // Parallel takes as long as the longest delay, ~500ms 20 | asyncPar().then((result) => console.log("Parallel", result)); 21 | 22 | // Sequential takes as long as the sum of delays, ~1500ms 23 | asyncSeq().then((result) => console.log("Sequential", result)); 24 | 25 | const traverseTreeIterable = T.traverse(I.FlatmappableIterable); 26 | -------------------------------------------------------------------------------- /examples/tree.ts: -------------------------------------------------------------------------------- 1 | import type { $, Kind, Out } from "../kind.ts"; 2 | import type { Option } from "../option.ts"; 3 | 4 | import * as O from "../option.ts"; 5 | import * as A from "../array.ts"; 6 | import { pipe } from "../fn.ts"; 7 | 8 | export type Tree = { 9 | readonly value: Option; 10 | readonly forest: Forest; 11 | }; 12 | 13 | export type Forest = ReadonlyArray>; 14 | 15 | // deno-lint-ignore no-explicit-any 16 | export type AnyTree = Tree; 17 | 18 | export type Type = U extends Tree ? A : never; 19 | 20 | export interface KindTree extends Kind { 21 | readonly kind: Tree>; 22 | } 23 | 24 | export function tree(value: Option, forest: Forest = []): Tree { 25 | return { value, forest }; 26 | } 27 | 28 | export function init(forest: Forest = []): Tree { 29 | return { value: O.none, forest }; 30 | } 31 | 32 | export function wrap(value: A, forest: Forest = []): Tree { 33 | return { value: O.some(value), forest }; 34 | } 35 | 36 | export function map(fai: (a: A) => I): (ua: Tree) => Tree { 37 | const mapValue = O.map(fai); 38 | return (ua) => tree(mapValue(ua.value), pipe(ua.forest, A.map(map(fai)))); 39 | } 40 | -------------------------------------------------------------------------------- /failable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Failable is a compound structure. It represents the idea of failure. As such 3 | * it includes methods for creating a `failed` data, providing alternative 4 | * data, and recovering from a failed state (effectively flatMapping from the 5 | * failed value). 6 | * 7 | * @module Failable 8 | * @since 2.0.0 9 | */ 10 | 11 | import type { $, Hold, Kind } from "./kind.ts"; 12 | import type { Flatmappable } from "./flatmappable.ts"; 13 | 14 | import { flow } from "./fn.ts"; 15 | 16 | /** 17 | * A Failable structure is a Flatmappable that allows for alternative instances, 18 | * failures, and recovery. 19 | * 20 | * @since 2.0.0 21 | */ 22 | export interface Failable extends Flatmappable, Hold { 23 | readonly alt: ( 24 | second: $, 25 | ) => (first: $) => $; 26 | readonly fail: ( 27 | value: B, 28 | ) => $; 29 | readonly recover: ( 30 | fbti: (b: B) => $, 31 | ) => ( 32 | ua: $, 33 | ) => $; 34 | } 35 | 36 | /** 37 | * The return type of createTap for Failable. Useful for reducing type 38 | * inference in the docs. 39 | * 40 | * @since 2.0.0 41 | */ 42 | export type Tap = ( 43 | onSuccess: (value: A) => void, 44 | onFailure: (value: B) => void, 45 | ) => ( 46 | ua: $, 47 | ) => $; 48 | 49 | /** 50 | * Create a tap function for a structure with instances of Wrappable and 51 | * Flatmappable. A tap function allows one to break out of the functional 52 | * codeflow. It is generally not advised to use tap for code flow but to 53 | * consider an escape hatch to do things like tracing or logging. 54 | * 55 | * @example 56 | * ```ts 57 | * import * as E from "./either.ts"; 58 | * import { createTap } from "./failable.ts"; 59 | * import { pipe } from "./fn.ts"; 60 | * 61 | * const tap = createTap(E.FailableEither); 62 | * 63 | * const result = pipe( 64 | * E.right(42), 65 | * tap( 66 | * value => console.log(`Success: ${value}`), 67 | * () => console.log("Failure") 68 | * ) 69 | * ); 70 | * 71 | * // Output: "Success: 42" 72 | * console.log(result); // Some(42) 73 | * ``` 74 | * 75 | * @since 2.0.0 76 | */ 77 | export function createTap( 78 | { wrap, fail, flatmap, recover }: Failable, 79 | ): ( 80 | onSuccess: (value: A) => void, 81 | onFailure: (value: B) => void, 82 | ) => ( 83 | ua: $, 84 | ) => $ { 85 | return (onSuccess, onFailure) => 86 | flow( 87 | flatmap((a) => { 88 | onSuccess(a); 89 | return wrap(a); 90 | }), 91 | recover((b) => { 92 | onFailure(b); 93 | return fail(b); 94 | }), 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /filterable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filterable is a structure that allows one to remove or refine a data 3 | * structure. 4 | * 5 | * @module Filterable 6 | * @since 2.0.0 7 | */ 8 | 9 | import type { $, Hold, Kind } from "./kind.ts"; 10 | import type { Either } from "./either.ts"; 11 | import type { Option } from "./option.ts"; 12 | import type { Pair } from "./pair.ts"; 13 | import type { Predicate } from "./predicate.ts"; 14 | import type { Refinement } from "./refinement.ts"; 15 | 16 | /** 17 | * A Filterable structure allows one to filter over the values contained in the 18 | * structure. This includes standard filter, filterMap, partition, and 19 | * partitionMap. 20 | * 21 | * @example 22 | * ```ts 23 | * import type { Filterable } from "./filterable.ts"; 24 | * import * as A from "./array.ts"; 25 | * import { pipe } from "./fn.ts"; 26 | * 27 | * // Example with Array filterable 28 | * const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 29 | * const evens = pipe( 30 | * numbers, 31 | * A.FilterableArray.filter(n => n % 2 === 0) 32 | * ); 33 | * console.log(evens); // [2, 4, 6, 8, 10] 34 | * ``` 35 | * 36 | * @since 2.0.0 37 | */ 38 | export interface Filterable extends Hold { 39 | readonly filter: { 40 | ( 41 | refinement: Refinement, 42 | ): ( 43 | ta: $, 44 | ) => $; 45 | ( 46 | predicate: Predicate, 47 | ): ( 48 | ta: $, 49 | ) => $; 50 | }; 51 | readonly filterMap: ( 52 | fai: (a: A) => Option, 53 | ) => ( 54 | ua: $, 55 | ) => $; 56 | readonly partition: { 57 | ( 58 | refinement: Refinement, 59 | ): ( 60 | ta: $, 61 | ) => Pair<$, $>; 62 | ( 63 | predicate: Predicate, 64 | ): ( 65 | ta: $, 66 | ) => Pair<$, $>; 67 | }; 68 | readonly partitionMap: ( 69 | fai: (a: A) => Either, 70 | ) => ( 71 | ua: $, 72 | ) => Pair<$, $>; 73 | } 74 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1754076744, 24 | "narHash": "sha256-de4d1IPxOUU/OcAn+GC+FrZmz+ydnbULMMFaElazUtI=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "09c2786786f3249ead0ad158b3151e3571c6dd6b", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "release-25.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A functional typescript library for deno."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/release-25.05"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: let 11 | pkgs = import nixpkgs { inherit system; }; 12 | mkScript = pkgs.writeShellScriptBin; 13 | 14 | shell = with pkgs; mkShell { 15 | packages = [ 16 | # Insert packages here 17 | deno 18 | 19 | # Insert shell aliases here 20 | (mkScript "coverage" '' 21 | #!/usr/bin/env sh 22 | 23 | if [ -z "$\{COVERAGE_DIR}" ]; then 24 | export COVERAGE_DIR="coverage" 25 | fi 26 | 27 | exiting() { 28 | echo "Ctrl-C trapped, clearing coverage"; 29 | rm -rf ./$COVERAGE_DIR; 30 | exit 0; 31 | } 32 | trap exiting SIGINT 33 | 34 | rm -rf $COVERAGE_DIR 35 | deno fmt 36 | deno test --doc --parallel --trace-leaks --coverage=$COVERAGE_DIR 37 | deno coverage $COVERAGE_DIR 38 | rm -rf ./$COVERAGE_DIR 39 | '') 40 | ]; 41 | 42 | shellHook = '' 43 | export COVERAGE_DIR="coverage" 44 | ''; 45 | }; 46 | in { 47 | devShells.default = shell; 48 | }); 49 | } 50 | 51 | -------------------------------------------------------------------------------- /flatmappable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Flatmappable is a structure that allows a function that returns the concrete 3 | * structure to be applied to the value inside of the same type of concrete 4 | * structure. The resultant nested structure is then flattened. 5 | * 6 | * @module Flatmappable 7 | * @since 2.0.0 8 | */ 9 | 10 | import type { $, Hold, Kind } from "./kind.ts"; 11 | import type { Applicable } from "./applicable.ts"; 12 | 13 | /** 14 | * A Flatmappable structure. 15 | * 16 | * @since 2.0.0 17 | */ 18 | export interface Flatmappable< 19 | U extends Kind, 20 | > extends Applicable, Hold { 21 | readonly flatmap: ( 22 | fati: (a: A) => $, 23 | ) => ( 24 | ta: $, 25 | ) => $; 26 | } 27 | 28 | /** 29 | * The return type for the createTap function on Flatmappable. Useful to reduce 30 | * type inference in documentation. 31 | * 32 | * @since 2.0.0 33 | */ 34 | export type Tap = ( 35 | fn: (value: A) => void, 36 | ) => ( 37 | ua: $, 38 | ) => $; 39 | 40 | /** 41 | * Create a tap function for a structure with instances of Wrappable and 42 | * Flatmappable. A tap function allows one to break out of the functional 43 | * codeflow. It is generally not advised to use tap for code flow but to 44 | * consider an escape hatch to do things like tracing or logging. 45 | * 46 | * @example 47 | * ```ts 48 | * import { createTap } from "./flatmappable.ts"; 49 | * import * as O from "./option.ts"; 50 | * import { pipe } from "./fn.ts"; 51 | * 52 | * const tap = createTap(O.FlatmappableOption); 53 | * const option = O.some(5); 54 | * 55 | * const result = pipe( 56 | * option, 57 | * tap(value => console.log(`Processing: ${value}`)) 58 | * ); 59 | * 60 | * // Output: "Processing: 5" 61 | * console.log(result); // Some(5) 62 | * ``` 63 | * 64 | * @since 2.0.0 65 | */ 66 | export function createTap( 67 | { wrap, flatmap }: Flatmappable, 68 | ): ( 69 | fn: (value: A) => void, 70 | ) => ( 71 | ua: $, 72 | ) => $ { 73 | return (fn) => 74 | flatmap((a) => { 75 | fn(a); 76 | return wrap(a); 77 | }); 78 | } 79 | 80 | // /** 81 | // * The return type for the createBind function on Flatmappable. Useful to reduce 82 | // * type inference in documentation. 83 | // * 84 | // * @since 2.0.0 85 | // */ 86 | export type Bind = < 87 | N extends string, 88 | A, 89 | I, 90 | J = never, 91 | K = never, 92 | L = unknown, 93 | M = unknown, 94 | >( 95 | name: Exclude, 96 | faui: (a: A) => $, 97 | ) => ( 98 | ua: $, 99 | ) => $< 100 | U, 101 | [{ readonly [K in keyof A | N]: K extends keyof A ? A[K] : I }, B | J, C | K], 102 | [D & L], 103 | [M] 104 | >; 105 | 106 | // /** 107 | // * Create a bind function for a structure with instances of Mappable and 108 | // * Flatmappable. A bind function allows one to flatmap into named fields in a 109 | // * struct, collecting values from the result of the flatmap in the order that 110 | // * they are completed. 111 | // * 112 | // * @example 113 | // * ```ts 114 | // * import { createBind } from "./flatmappable.ts"; 115 | // * import * as O from "./option.ts"; 116 | // * import { pipe } from "./fn.ts"; 117 | // * 118 | // * const bind = createBind(O.FlatmappableOption); 119 | // * const option = O.some({ name: "John" }); 120 | // * 121 | // * const result = pipe( 122 | // * option, 123 | // * bind("age", user => O.some(30)) 124 | // * ); 125 | // * 126 | // * console.log(result); // Some({ name: "John", age: 30 }) 127 | // * ``` 128 | // * 129 | // * @since 2.0.0 130 | // */ 131 | export function createBind( 132 | { flatmap, map }: Flatmappable, 133 | ): Bind { 134 | return < 135 | N extends string, 136 | A, 137 | I, 138 | J = never, 139 | K = never, 140 | L = unknown, 141 | M = unknown, 142 | >(name: Exclude, faui: (a: A) => $) => 143 | flatmap((a: A) => { 144 | const mapper = map((i: I) => { 145 | type Bound = { 146 | readonly [K in keyof A | N]: K extends keyof A ? A[K] : I; 147 | }; 148 | return Object.assign({}, a, { [name]: i }) as unknown as Bound; 149 | }); 150 | return mapper(faui(a)); 151 | }); 152 | } 153 | 154 | // /** 155 | // * Create a Flatmappable instance from wrap and flatmap functions. 156 | // * 157 | // * @example 158 | // * ```ts 159 | // * import type { Kind, Out } from "./kind.ts"; 160 | // * import { createFlatmappable } from "./flatmappable.ts"; 161 | // * import { pipe } from "./fn.ts"; 162 | // * 163 | // * // Create a Kind for Promise 164 | // * interface KindPromise extends Kind { 165 | // * readonly kind: Promise>; 166 | // * }; 167 | // * 168 | // * // Create an of and chain function for Promise 169 | // * const wrap = (a: A): Promise => Promise.resolve(a); 170 | // * const flatmap = (faui: (a: A) => Promise) => 171 | // * (ua: Promise): Promise => ua.then(faui); 172 | // * 173 | // * // Derive a Flatmappable for Promise 174 | // * const M = createFlatmappable({ wrap, flatmap }); 175 | // * 176 | // * const result = await pipe( 177 | // * M.wrap((n: number) => (m: number) => n + m), 178 | // * M.apply(M.wrap(1)), 179 | // * M.apply(M.wrap(1)), 180 | // * ); // 2 181 | // * ``` 182 | // * 183 | // * @experimental 184 | // * 185 | // * @since 2.0.0 186 | // */ 187 | export function createFlatmappable( 188 | { wrap, flatmap }: Pick, "wrap" | "flatmap">, 189 | ): Flatmappable { 190 | const result: Flatmappable = { 191 | wrap, 192 | apply: ((ua) => flatmap((fai) => result.map(fai)(ua))) as Flatmappable< 193 | U 194 | >["apply"], 195 | map: ((fai) => flatmap((a) => wrap(fai(a)))) as Flatmappable["map"], 196 | flatmap, 197 | }; 198 | return result; 199 | } 200 | -------------------------------------------------------------------------------- /foldable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Foldable is a structure that allows a function to all values inside of a 3 | * structure. The reduction is accumulative and ordered. 4 | * 5 | * @module Foldable 6 | * @since 2.0.0 7 | */ 8 | 9 | import type { $, Hold, Kind } from "./kind.ts"; 10 | import type { Initializable } from "./initializable.ts"; 11 | 12 | /** 13 | * A Foldable structure has the method fold. 14 | * 15 | * @example 16 | * ```ts 17 | * import * as A from "./array.ts"; 18 | * import { pipe } from "./fn.ts"; 19 | * 20 | * const numbers = [1, 2, 3, 4, 5]; 21 | * const sum = pipe( 22 | * numbers, 23 | * A.FoldableArray.fold( 24 | * (acc, value) => acc + value, 25 | * 0 26 | * ) 27 | * ); 28 | * 29 | * console.log(sum); // 15 30 | * ``` 31 | * 32 | * @since 2.0.0 33 | */ 34 | export interface Foldable extends Hold { 35 | readonly fold: ( 36 | reducer: (accumulator: O, value: A) => O, 37 | accumulator: O, 38 | ) => ( 39 | ua: $, 40 | ) => O; 41 | } 42 | 43 | /** 44 | * Collect all values from a Foldable structure using the provided Initializable. 45 | * 46 | * @example 47 | * ```ts 48 | * import { collect } from "./foldable.ts"; 49 | * import * as A from "./array.ts"; 50 | * import * as N from "./number.ts"; 51 | * 52 | * const numbers = [1, 2, 3, 4, 5]; 53 | * const sum = collect( 54 | * A.FoldableArray, 55 | * N.InitializableNumberSum 56 | * )(numbers); 57 | * 58 | * console.log(sum); // 15 59 | * ``` 60 | * 61 | * @experimental 62 | * @since 2.0.0 63 | */ 64 | export function collect( 65 | { fold }: Foldable, 66 | { combine, init }: Initializable, 67 | ): ( 68 | ua: $, 69 | ) => A { 70 | return (ua) => 71 | fold((first, second) => combine(second)(first), init())(ua); 72 | } 73 | -------------------------------------------------------------------------------- /ideas/README.md: -------------------------------------------------------------------------------- 1 | # Fun Ideas 2 | 3 | This directory contains experimental ideas that fun may or may not implement. It 4 | is not distributed via jsr or deno.land/x but can be referenced directly via 5 | github branches. Occasionally, an idea will be moved into the root directory 6 | with an @experimental tag in the module documentation. These modules are not api 7 | stable and may change from time to time until stabilized. 8 | 9 | If you wish to add a new module to fun it's best to open a pull request with the 10 | module in this directory so that merging it doesn't impact production use. 11 | -------------------------------------------------------------------------------- /identity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the Identity algebraic data type. Identity is one of the 3 | * simplest data types in that it represents a type AS IS, allowing one to 4 | * create algebraic structures for the inner type. This is most useful for 5 | * constructing arrow optics. 6 | * 7 | * @module Identity 8 | * @since 2.0.0 9 | */ 10 | import type { Kind, Out } from "./kind.ts"; 11 | import type { Applicable } from "./applicable.ts"; 12 | import type { Mappable } from "./mappable.ts"; 13 | import type { Flatmappable } from "./flatmappable.ts"; 14 | import type { Wrappable } from "./wrappable.ts"; 15 | 16 | /** 17 | * Represents the Identity type constructor. 18 | * @since 2.0.0 19 | */ 20 | export type Identity = A; 21 | 22 | /** 23 | * Specifies Identity as a Higher Kinded Type, with covariant 24 | * parameter A corresponding to the 0th index of any substitutions. 25 | * 26 | * @since 2.0.0 27 | */ 28 | export interface KindIdentity extends Kind { 29 | readonly kind: Identity>; 30 | } 31 | 32 | /** 33 | * Wraps a value into an Identity type. 34 | * This function allows any value to be lifted into the context of an Identity, 35 | * making it possible to interact with other functions that operate on the Identity type. 36 | * 37 | * @example 38 | * ```ts 39 | * import { wrap } from "./identity.ts"; 40 | * 41 | * // numberIdentity is Identity with value 5 42 | * const numberIdentity = wrap(5); 43 | * 44 | * // stringIdentity is Identity with value "hello" 45 | * const stringIdentity = wrap("hello"); 46 | * ``` 47 | * 48 | * @param a - The value to wrap. 49 | * @returns The wrapped value as an Identity. 50 | * @since 2.0.0 51 | */ 52 | export function wrap(a: A): Identity { 53 | return a; 54 | } 55 | 56 | /** 57 | * Apply a function to the value in an Identity. 58 | * 59 | * @example 60 | * ```ts 61 | * import { map, wrap } from "./identity.ts"; 62 | * import { pipe } from "./fn.ts"; 63 | * 64 | * const identity = wrap(5); 65 | * const doubled = pipe( 66 | * identity, 67 | * map(n => n * 2) 68 | * ); 69 | * 70 | * console.log(doubled); // 10 71 | * ``` 72 | * 73 | * @since 2.0.0 74 | */ 75 | export function map(fai: (a: A) => I): (ta: Identity) => Identity { 76 | return fai; 77 | } 78 | 79 | /** 80 | * Apply a function wrapped in an Identity to a value wrapped in an Identity. 81 | * 82 | * @example 83 | * ```ts 84 | * import { apply, wrap } from "./identity.ts"; 85 | * import { pipe } from "./fn.ts"; 86 | * 87 | * const identityFn = wrap((n: number) => n * 2); 88 | * const identityValue = wrap(5); 89 | * const result = pipe( 90 | * identityFn, 91 | * apply(identityValue) 92 | * ); 93 | * 94 | * console.log(result); // 10 95 | * ``` 96 | * 97 | * @since 2.0.0 98 | */ 99 | export function apply( 100 | ua: Identity, 101 | ): (ufai: Identity<(a: A) => I>) => Identity { 102 | return (ufai) => ufai(ua); 103 | } 104 | 105 | /** 106 | * Chain Identity computations together. 107 | * 108 | * @example 109 | * ```ts 110 | * import { flatmap, wrap } from "./identity.ts"; 111 | * import { pipe } from "./fn.ts"; 112 | * 113 | * const identity = wrap(5); 114 | * const chained = pipe( 115 | * identity, 116 | * flatmap(n => wrap(n * 2)) 117 | * ); 118 | * 119 | * console.log(chained); // 10 120 | * ``` 121 | * 122 | * @since 2.0.0 123 | */ 124 | export function flatmap( 125 | fati: (a: A) => Identity, 126 | ): (ta: Identity) => Identity { 127 | return fati; 128 | } 129 | 130 | /** 131 | * The canonical implementation of Applicable for Identity. 132 | * 133 | * @since 2.0.0 134 | */ 135 | export const ApplicableIdentity: Applicable = { 136 | apply, 137 | map, 138 | wrap, 139 | }; 140 | 141 | /** 142 | * The canonical implementation of Mappable for Identity. 143 | * 144 | * @since 2.0.0 145 | */ 146 | export const MappableIdentity: Mappable = { map }; 147 | 148 | /** 149 | * The canonical implementation of Flatmappable for Identity. 150 | * 151 | * @since 2.0.0 152 | */ 153 | export const FlatmappableIdentity: Flatmappable = { 154 | apply, 155 | map, 156 | flatmap, 157 | wrap, 158 | }; 159 | 160 | /** 161 | * @since 2.0.0 162 | */ 163 | export const WrappableIdentity: Wrappable = { wrap }; 164 | -------------------------------------------------------------------------------- /initializable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the type and utilities for the Initializable algebraic 3 | * data structure. A type that implements Initializable has a concept of "empty" 4 | * represented by the init method and a concept to combine represented by the 5 | * combine method. In other functional libraries and languages Initializable is 6 | * called Monoid. 7 | * 8 | * @module Initializable 9 | * @since 2.0.0 10 | */ 11 | 12 | import type { Combinable } from "./combinable.ts"; 13 | 14 | import * as C from "./combinable.ts"; 15 | 16 | type ReadonlyRecord = Readonly>; 17 | 18 | // deno-lint-ignore no-explicit-any 19 | type AnyReadonlyRecord = ReadonlyRecord; 20 | 21 | /** 22 | * A Initializable structure has the method init. 23 | * 24 | * @since 2.0.0 25 | */ 26 | export interface Initializable extends Combinable { 27 | readonly init: () => A; 28 | } 29 | 30 | /** 31 | * A type for Initializable over any fixed value, useful as an extension target 32 | * for functions that take any Initializable and do not need to unwrap the 33 | * type. 34 | * 35 | * @since 2.0.0 36 | */ 37 | // deno-lint-ignore no-explicit-any 38 | export type AnyInitializable = Initializable; 39 | 40 | /** 41 | * A type level unwrapor, used to pull the inner type from a Combinable. 42 | * 43 | * @since 2.0.0 44 | */ 45 | export type TypeOf = T extends Initializable ? A : never; 46 | 47 | /** 48 | * Create an Initializable fixed to a concrete value A. This operates like init 49 | * from Combinable in other functional libraries. 50 | * 51 | * @since 2.0.0 52 | */ 53 | export function constant(value: A): Initializable { 54 | return { init: () => value, combine: () => () => value }; 55 | } 56 | 57 | /** 58 | * Create an Initializable fixed to a struct using nested Initializables to 59 | * create the init values within. 60 | * 61 | * @since 2.0.0 62 | */ 63 | export function struct( 64 | initializables: { [K in keyof O]: Initializable }, 65 | ): Initializable<{ readonly [K in keyof O]: O[K] }> { 66 | type Entries = [keyof O, typeof initializables[keyof O]][]; 67 | return ({ 68 | init: () => { 69 | const r = {} as Record; 70 | for (const [key, { init }] of Object.entries(initializables) as Entries) { 71 | r[key] = init(); 72 | } 73 | return r as { [K in keyof O]: O[K] }; 74 | }, 75 | ...C.struct(initializables), 76 | }); 77 | } 78 | 79 | /** 80 | * Create an Initializable fixed to a tuple using nested Initializables to 81 | * create the init values within. 82 | * 83 | * @since 2.0.0 84 | */ 85 | export function tuple( 86 | ...initializables: T 87 | ): Initializable<{ readonly [K in keyof T]: TypeOf }> { 88 | return { 89 | init: () => initializables.map(({ init }) => init()), 90 | ...C.tuple(...initializables), 91 | } as Initializable<{ readonly [K in keyof T]: TypeOf }>; 92 | } 93 | 94 | /** 95 | * Create a dual Initializable from an existing initializable. This effectively 96 | * switches the order of application of the original Initializable. 97 | * 98 | * @example 99 | * ```ts 100 | * import { dual, getCombineAll, intercalcate } from "./initializable.ts"; 101 | * import { InitializableString } from "./string.ts"; 102 | * import { pipe } from "./fn.ts"; 103 | * 104 | * const reverse = dual(InitializableString); 105 | * const reverseAll = pipe( 106 | * reverse, 107 | * intercalcate(" "), 108 | * getCombineAll, 109 | * ); 110 | * 111 | * const result = reverseAll("Hello", "World"); // "World Hello" 112 | * ``` 113 | * 114 | * @since 2.0.0 115 | */ 116 | export function dual(M: Initializable): Initializable { 117 | return ({ 118 | combine: C.dual(M).combine, 119 | init: M.init, 120 | }); 121 | } 122 | 123 | /** 124 | * Create a initializable that works like Array.join, 125 | * inserting middle between every two values 126 | * that are combineenated. This can have some interesting 127 | * results. 128 | * 129 | * @example 130 | * ```ts 131 | * import * as M from "./initializable.ts"; 132 | * import * as S from "./string.ts"; 133 | * import { pipe } from "./fn.ts"; 134 | * 135 | * const { combine: toList } = pipe( 136 | * S.InitializableString, 137 | * M.intercalcate(", "), 138 | * ); 139 | * 140 | * const list = pipe( 141 | * "apples", 142 | * toList("oranges"), 143 | * toList("and bananas"), 144 | * ); // list === "apples, oranges, and bananas" 145 | * ``` 146 | * 147 | * @since 2.0.0 148 | */ 149 | export function intercalcate( 150 | middle: A, 151 | ): (I: Initializable) => Initializable { 152 | return ({ combine, init }) => ({ 153 | combine: (second) => combine(combine(second)(middle)), 154 | init, 155 | }); 156 | } 157 | /** 158 | * Given an Initializable, create a function that will 159 | * iterate through an array of values and combine 160 | * them. This is not much more than Array.fold(combine). 161 | * 162 | * @example 163 | * ```ts 164 | * import * as I from "./initializable.ts"; 165 | * import * as N from "./number.ts"; 166 | * 167 | * const sumAll = I.getCombineAll(N.InitializableNumberSum); 168 | * 169 | * const result = sumAll(1, 30, 80, 1000, 52, 42); // sum === 1205 170 | * ``` 171 | * 172 | * @since 2.0.0 173 | */ 174 | export function getCombineAll( 175 | { combine, init }: Initializable, 176 | ): (...as: ReadonlyArray) => A { 177 | const _combine = (first: A, second: A) => combine(second)(first); 178 | return (...as) => as.reduce(_combine, init()); 179 | } 180 | -------------------------------------------------------------------------------- /kind.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Kind is a collection of types used for doing something called Type 3 | * Substitution. The core idea here is that sometimes there is a function that 4 | * is generic at two levels, at the concrete type as well as at the type leve. 5 | * A simple example of this is a *forEach* function. In javascript the built in 6 | * *Array* and *Set* data structures both have a built in *forEach* method. If 7 | * we look at Array and Set we can see that the creation functions are similar. 8 | * 9 | * * `const arr = new Array()` returns Array; 10 | * * `const set = new Set()` returns Set; 11 | * 12 | * If we look at the type signitures on forEach for `arr` and `set` we also 13 | * notice a similar pattern. 14 | * 15 | * * `type A = (typeof arr)['forEach']` returns `(callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void` 16 | * * `type B = (typeof set)['forEach']` returns `(callbackfn: (value: number, value2: number, set: Set) => void, thisArg?: any) => void` 17 | * 18 | * Both forEach methods have the same concrete value of `number` but differ in 19 | * that they operate over an Array or a Set. Here is a table to illustrate this 20 | * pattern a bit more clearly. 21 | * 22 | * | Structure | Outer Type | Inner Type | forEach Type | 23 | * | --------- | ---------- | ------------------ | ------------------------------------------------- | 24 | * | Array | Array | A | (value: A, index: number, struct: A[]) => void | 25 | * | Set | Set | A | (value: A, index: number, struct: Set) => void | 26 | * | Map | Map | K (key), V (value) | (value: V, key: K, struct: Map) => void | 27 | * 28 | * In general we can see that the forEach function could have a more generic 29 | * type signiture like this: 30 | * 31 | * ``` 32 | * type ForEach = { 33 | * forEach: (struct: Outer) => (value: V, key: K, struct: Outer) => void; 34 | * } 35 | * ``` 36 | * 37 | * Unfortunately, while these types of patterns are abundant within TypeScript 38 | * (and most programming languages). The ability to pass generic types around 39 | * and fill type holes is not built into TypeScript. However, with some type 40 | * magic we can create our own type level substitutions using methods pionered 41 | * by [gcanti](https://github.com/gcanti) in *fp-ts*. 42 | * 43 | * @module Kind 44 | * @since 2.0.0 45 | */ 46 | 47 | /** 48 | * The Substitutions type splits the different type level substitutions into 49 | * tuples based on variance. 50 | */ 51 | export type Substitutions = { 52 | // Covariant Substitutions: () => A 53 | readonly ["out"]: unknown[]; 54 | // Premappable Substitutions: (A) => void 55 | readonly ["in"]: unknown[]; 56 | // Invariant Substitutions: (A) => A 57 | readonly ["inout"]: unknown[]; 58 | }; 59 | 60 | /** 61 | * Kind is an interface that can be extended 62 | * to retrieve inner types using "this". 63 | */ 64 | export interface Kind extends Substitutions { 65 | readonly kind?: unknown; 66 | } 67 | 68 | /** 69 | * Substitute is a substitution type, taking a Kind implementation T and 70 | * substituting it with types passed in S. 71 | */ 72 | export type Substitute = T extends 73 | { readonly kind: unknown } 74 | // If the kind property on T is required (no ?) then proceed with substitution 75 | ? (T & S)["kind"] 76 | // Otherwise carry T and the substitutions forward, preserving variance. 77 | : { 78 | readonly T: T; 79 | readonly ["out"]: () => S["out"]; 80 | readonly ["in"]: (_: S["in"]) => void; 81 | readonly ["inout"]: (_: S["inout"]) => S["inout"]; 82 | }; 83 | 84 | /** 85 | * $ is an alias of Substitute, lifting out, in, and inout 86 | * substitutions to positional type parameters. 87 | */ 88 | export type $< 89 | T extends Kind, 90 | Out extends unknown[], 91 | In extends unknown[] = [never], 92 | InOut extends unknown[] = [never], 93 | > = Substitute; 94 | 95 | /** 96 | * Access the Covariant substitution type at index N 97 | */ 98 | export type Out = 99 | T["out"][N]; 100 | 101 | /** 102 | * Access the Premappable substitution type at index N 103 | */ 104 | export type In = T["in"][N]; 105 | 106 | /** 107 | * Access the Invariant substitution type at index N 108 | */ 109 | export type InOut = 110 | T["inout"][N]; 111 | 112 | /** 113 | * Fix a concrete type as a non-substituting Kind. This allows one to define 114 | * algebraic structures over things like number, string, etc. 115 | */ 116 | export interface Fix extends Kind { 117 | readonly kind: A; 118 | } 119 | 120 | /** 121 | * Create a scoped symbol for use with Hold. 122 | */ 123 | const HoldSymbol = Symbol("Hold"); 124 | 125 | /** 126 | * The Hold interface allows one to trick the typescript compiler into holding 127 | * onto type information that it would otherwise discard. This is useful when 128 | * creating an interface that merges multiple type classes (see Flatmappable). 129 | */ 130 | export interface Hold { 131 | readonly [HoldSymbol]?: A; 132 | } 133 | 134 | /** 135 | * Some Flatmappable Transform kinds only have one out type, in those cases we 136 | * use this Nest type to make the transform kind cleaner. 137 | */ 138 | export type Nest = $< 139 | U, 140 | [Out, Out, Out], 141 | [In], 142 | [InOut] 143 | >; 144 | 145 | /** 146 | * Spread the keys of a struct union into a single struct. 147 | */ 148 | export type Spread = { [K in keyof A]: A[K] } extends infer B ? B : never; 149 | 150 | /** 151 | * An extension type to be used to constrain in input to an outer container with 152 | * any concrete types. 153 | */ 154 | // deno-lint-ignore no-explicit-any 155 | export type AnySub = $; 156 | 157 | /** 158 | * A type level utility that turns a type union into a type intersection. This 159 | * type is dangerous and can have unexpected results so extra runtime testing is 160 | * necessary when used. 161 | */ 162 | // deno-lint-ignore no-explicit-any 163 | export type Intersect = (U extends any ? (k: U) => void : never) extends 164 | ((k: infer I) => void) ? I : never; 165 | -------------------------------------------------------------------------------- /mappable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mappable is a structure that allows a function to be applied inside of the 3 | * associated concrete structure. 4 | * 5 | * @module Mappable 6 | * @since 2.0.0 7 | */ 8 | 9 | import type { $, Hold, Kind } from "./kind.ts"; 10 | 11 | /** 12 | * A Mappable structure has the method map. 13 | * 14 | * @since 2.0.0 15 | */ 16 | export interface Mappable extends Hold { 17 | readonly map: ( 18 | fai: (value: A) => I, 19 | ) => ( 20 | ta: $, 21 | ) => $; 22 | } 23 | 24 | /** 25 | * The return type for the createBindTo function on Mappable. Useful to reduce 26 | * type inference in documentation. 27 | * 28 | * @since 2.0.0 29 | */ 30 | export type BindTo = ( 31 | name: N, 32 | ) => ( 33 | ua: $, 34 | ) => $; 35 | 36 | /** 37 | * Create a bindTo function from a structure with an instance of Mappable. A 38 | * bindTo function takes the inner value of the structure and maps it to the 39 | * value of a struct with the given name. It is useful for lifting the value 40 | * such that it can then be used with a `bind` function, effectively allowing 41 | * for `Do`-like notation in typescript. 42 | * 43 | * @example 44 | * ```ts 45 | * import { createBindTo } from "./mappable.ts"; 46 | * import * as O from "./option.ts"; 47 | * import { pipe } from "./fn.ts"; 48 | * 49 | * const bindTo = createBindTo(O.MappableOption); 50 | * const option = O.some(5); 51 | * 52 | * const result = pipe( 53 | * option, 54 | * bindTo("value") 55 | * ); 56 | * 57 | * console.log(result); // Some({ value: 5 }) 58 | * ``` 59 | * 60 | * @since 2.0.0 61 | */ 62 | export function createBindTo( 63 | { map }: Mappable, 64 | ): ( 65 | name: N, 66 | ) => ( 67 | ua: $, 68 | ) => $ { 69 | // deno-lint-ignore no-explicit-any 70 | return (name) => map((value) => ({ [name]: value }) as any); 71 | } 72 | -------------------------------------------------------------------------------- /predicate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The Predicate type represents unary functions that return boolean values. 3 | * Typically, these functions indicate the existence of some quality for the 4 | * values passed as inputs. Some simple examples of Predicates are postive for 5 | * numbers, non-null values, etc. 6 | * 7 | * @module Predicate 8 | * @since 2.0.0 9 | */ 10 | 11 | import type { In, Kind } from "./kind.ts"; 12 | import type { Combinable } from "./combinable.ts"; 13 | import type { Initializable } from "./initializable.ts"; 14 | 15 | import { flow } from "./fn.ts"; 16 | 17 | /** 18 | * The Predicate type is a function that takes some 19 | * value of type A and returns boolean, indicating 20 | * that a property is true or false for the value A. 21 | * 22 | * @example 23 | * ```ts 24 | * import type { Predicate } from "./predicate.ts"; 25 | * import * as O from "./option.ts"; 26 | * 27 | * function fromPredicate(predicate: Predicate) { 28 | * return (a: A): O.Option => predicate(a) 29 | * ? O.some(a) : O.none; 30 | * } 31 | * 32 | * function isPositive(n: number): boolean { 33 | * return n > 0; 34 | * } 35 | * 36 | * const isPos = fromPredicate(isPositive); 37 | * 38 | * const resultSome = isPos(1); // Some(1) 39 | * const resultNone = isPos(-1); // None 40 | * ``` 41 | * 42 | * @since 2.0.0 43 | */ 44 | export type Predicate = (a: A) => boolean; 45 | 46 | /** 47 | * Specifies Predicate as a Higher Kinded Type, with 48 | * contravariant parameter A corresponding to the 0th 49 | * index of any Substitutions. 50 | * 51 | * @since 2.0.0 52 | */ 53 | export interface KindPredicate extends Kind { 54 | readonly kind: Predicate>; 55 | } 56 | 57 | /** 58 | * Create a Predicate using a Predicate and a function that takes 59 | * a type L and returns a type D. This maps over the input value of 60 | * the predicate. 61 | * 62 | * @example 63 | * ```ts 64 | * import { premap } from "./predicate.ts"; 65 | * import { pipe } from "./fn.ts"; 66 | * 67 | * const isGreaterThan3 = (n: number) => n > 3; 68 | * const isLongerThan3 = pipe( 69 | * isGreaterThan3, 70 | * premap((s: string) => s.length), 71 | * ); 72 | * 73 | * const result1 = isLongerThan3("Hello"); // true 74 | * const result2 = isLongerThan3("Hi"); // false 75 | * ``` 76 | * 77 | * @since 2.0.0 78 | */ 79 | export function premap( 80 | fia: (i: I) => A, 81 | ): (ua: Predicate) => Predicate { 82 | return (ua) => flow(fia, ua); 83 | } 84 | 85 | /** 86 | * Negates the result of an existing Predicate. 87 | * 88 | * @example 89 | * ```ts 90 | * import { not } from "./predicate.ts"; 91 | * 92 | * const isPositive = (n: number) => n > 0; 93 | * const isZeroOrNegative = not(isPositive); 94 | * 95 | * const result1 = isZeroOrNegative(1); // false 96 | * const result2 = isZeroOrNegative(0); // true 97 | * const result3 = isZeroOrNegative(-1); // true 98 | * ``` 99 | * 100 | * @since 2.0.0 101 | */ 102 | export function not(predicate: Predicate): Predicate { 103 | return (a) => !predicate(a); 104 | } 105 | 106 | /** 107 | * Creates the union of two predicates, returning true if either 108 | * predicate returns true. 109 | * 110 | * @example 111 | * ```ts 112 | * import { or } from "./predicate.ts"; 113 | * import { string, number } from "./refinement.ts"; 114 | * import { pipe } from "./fn.ts"; 115 | * 116 | * // A Refinement is also a Predicate 117 | * const stringOrNumber = pipe( 118 | * string, 119 | * or(number), 120 | * ); 121 | * 122 | * const result1 = stringOrNumber("Hello"); // true 123 | * const result2 = stringOrNumber(1); // true 124 | * const result3 = stringOrNumber({}); // false 125 | * ``` 126 | * 127 | * @since 2.0.0 128 | */ 129 | export function or( 130 | second: Predicate, 131 | ): (first: Predicate) => Predicate { 132 | return (first) => (a) => first(a) || second(a); 133 | } 134 | 135 | /** 136 | * Creates the intersection of two predicates, returning true if both 137 | * predicates return true. 138 | * 139 | * @example 140 | * ```ts 141 | * import { and } from "./predicate.ts"; 142 | * import { pipe } from "./fn.ts"; 143 | * 144 | * const isPositive = (n: number) => n > 0; 145 | * const isInteger = (n: number) => Number.isInteger(n); 146 | * 147 | * const isPositiveInteger = pipe( 148 | * isPositive, 149 | * and(isInteger), 150 | * ); 151 | * 152 | * const result1 = isPositiveInteger(1); // true 153 | * const result2 = isPositiveInteger(100); // true 154 | * const result3 = isPositiveInteger(-1); // false 155 | * ``` 156 | * 157 | * @since 2.0.0 158 | */ 159 | export function and( 160 | second: Predicate, 161 | ): (first: Predicate) => Predicate { 162 | return (first) => (a) => first(a) && second(a); 163 | } 164 | 165 | /** 166 | * @since 2.0.0 167 | */ 168 | export function getCombinableAny(): Combinable> { 169 | return { combine: or }; 170 | } 171 | 172 | /** 173 | * @since 2.0.0 174 | */ 175 | export function getCombinableAll(): Combinable> { 176 | return { combine: and }; 177 | } 178 | 179 | /** 180 | * Get a Initializable> for any type A that combines using the 181 | * Predicate or function. 182 | * 183 | * @example 184 | * ```ts 185 | * import { getInitializableAny } from "./predicate.ts"; 186 | * import { pipe } from "./fn.ts"; 187 | * 188 | * const { combine } = getInitializableAny(); 189 | * 190 | * const lessThanZero = (n: number) => n < 0; 191 | * const greaterThanFifty = (n: number) => n > 50; 192 | * 193 | * const notBetweenZeroAndFifty = pipe( 194 | * lessThanZero, 195 | * combine(greaterThanFifty), 196 | * ); 197 | * 198 | * const result1 = notBetweenZeroAndFifty(10); // false 199 | * const result2 = notBetweenZeroAndFifty(-10); // true 200 | * const result3 = notBetweenZeroAndFifty(100); // true 201 | * ``` 202 | * 203 | * @since 2.0.0 204 | */ 205 | export function getInitializableAny(): Initializable> { 206 | return { 207 | combine: or, 208 | init: () => () => false, 209 | }; 210 | } 211 | 212 | /** 213 | * Get a Initializable> for any type A that combines using the 214 | * Predicate and function. 215 | * 216 | * @example 217 | * ```ts 218 | * import { getInitializableAll } from "./predicate.ts"; 219 | * import { pipe } from "./fn.ts"; 220 | * 221 | * const { combine } = getInitializableAll(); 222 | * 223 | * const greaterThanZero = (n: number) => n > 0; 224 | * const lessThanFifty = (n: number) => n < 50; 225 | * 226 | * const betweenZeroAndFifty = pipe( 227 | * greaterThanZero, 228 | * combine(lessThanFifty) 229 | * ); 230 | * 231 | * const result1 = betweenZeroAndFifty(10); // true 232 | * const result2 = betweenZeroAndFifty(-10); // false 233 | * const result3 = betweenZeroAndFifty(100); // false 234 | * ``` 235 | * 236 | * @since 2.0.0 237 | */ 238 | export function getInitializableAll(): Initializable> { 239 | return { 240 | combine: and, 241 | init: () => () => true, 242 | }; 243 | } 244 | -------------------------------------------------------------------------------- /premappable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Premappable is a structure that allows a function to be applied 3 | * contravariantly inside of the associated concrete structure. 4 | * 5 | * @module Premappable 6 | * @since 2.0.0 7 | */ 8 | 9 | import type { $, Hold, Kind } from "./kind.ts"; 10 | import type { Mappable } from "./mappable.ts"; 11 | 12 | /** 13 | * A Premappable structure has the method premap. 14 | * 15 | * @since 2.0.0 16 | */ 17 | export interface Premappable extends Mappable, Hold { 18 | readonly premap: ( 19 | fia: (l: L) => D, 20 | ) => (ua: $) => $; 21 | readonly dimap: ( 22 | fld: (l: L) => D, 23 | fai: (a: A) => I, 24 | ) => ( 25 | ua: $, 26 | ) => $; 27 | } 28 | -------------------------------------------------------------------------------- /scripts/build-npm.ts: -------------------------------------------------------------------------------- 1 | import * as DNT from "https://deno.land/x/dnt@0.38.1/mod.ts"; 2 | import { parse } from "https://deno.land/x/semver@v1.4.0/mod.ts"; 3 | import * as AE from "../async_either.ts"; 4 | import * as D from "../decoder.ts"; 5 | import * as E from "../either.ts"; 6 | import * as I from "../iterable.ts"; 7 | import * as O from "../option.ts"; 8 | import * as S from "../string.ts"; 9 | import { flow, pipe } from "../fn.ts"; 10 | 11 | // Environment 12 | const semver = pipe( 13 | D.string, 14 | D.compose((i) => { 15 | const semVer = parse(i); 16 | return semVer === null 17 | ? D.failure(i, "Semantic Version") 18 | : D.success(semVer); 19 | }), 20 | ); 21 | 22 | const Env = D.struct({ 23 | VERSION: semver, 24 | }); 25 | 26 | type Env = D.TypeOut; 27 | 28 | /** 29 | * Consts 30 | */ 31 | 32 | const BUILD_DIR = "./build"; 33 | const ENTRYPOINTS = pipe( 34 | Deno.readDirSync("./"), 35 | I.map(({ name }) => name), 36 | I.filterMap(O.fromPredicate(S.endsWith(".ts"))), 37 | I.collect, 38 | ) as string[]; 39 | 40 | // Errors 41 | type BuildError = { message: string; context: Record }; 42 | 43 | const buildError = ( 44 | message: string, 45 | context: Record = {}, 46 | ): BuildError => ({ message, context }); 47 | 48 | const printBuildError = ({ message, context }: BuildError) => { 49 | let msg = `BUILD ERROR: ${message}\n`; 50 | msg += Object 51 | .entries(context) 52 | .map(([key, value]) => { 53 | const val = typeof value === "string" 54 | ? value 55 | : value instanceof Error 56 | ? value 57 | : typeof value === "object" && value !== null && 58 | Object.hasOwn(value, "toString") && 59 | typeof value.toString === "function" 60 | ? value.toString() 61 | : JSON.stringify(value, null, 2); 62 | return `Context - ${key}\n${val}`; 63 | }) 64 | .join("\n"); 65 | return msg; 66 | }; 67 | 68 | // Functions 69 | const createBuildOptions = ( 70 | { VERSION }: Env, 71 | ): DNT.BuildOptions => ({ 72 | entryPoints: ENTRYPOINTS, 73 | outDir: BUILD_DIR, 74 | typeCheck: false, 75 | test: true, 76 | shims: { 77 | deno: true, 78 | }, 79 | package: { 80 | name: "@nll/fun", 81 | version: VERSION.toString(), 82 | description: "A utility library for functional programming in TypeScript", 83 | keywords: ["functional programming", "typescript", "fp"], 84 | license: "MIT", 85 | bugs: { 86 | url: "https://github.com/baetheus/fun/issues", 87 | email: "brandon@null.pub", 88 | }, 89 | author: { 90 | "name": "Brandon Blaylock", 91 | "email": "brandon@null.pub", 92 | "url": "blaylock.dev", 93 | }, 94 | repository: "github:baetheus/fun", 95 | }, 96 | postBuild() { 97 | Deno.copyFileSync("LICENSE", `${BUILD_DIR}/LICENSE`); 98 | Deno.copyFileSync("README.md", `${BUILD_DIR}/README.md`); 99 | }, 100 | }); 101 | 102 | const getEnv = AE.tryCatch( 103 | Deno.env.toObject, 104 | (err, args) => buildError("Unable to get environment.", { err, args }), 105 | )(); 106 | 107 | const parseEnv = flow( 108 | Env, 109 | E.mapSecond((err) => 110 | buildError("Unable to parse environment.", { err: D.draw(err) }) 111 | ), 112 | AE.fromEither, 113 | ); 114 | 115 | const emptyDir = AE.tryCatch( 116 | DNT.emptyDir, 117 | (err, args) => buildError("Unable to empty build directory.", { err, args }), 118 | ); 119 | 120 | const build = AE.tryCatch( 121 | DNT.build, 122 | (err, args) => buildError("Unable to build node package.", { err, args }), 123 | ); 124 | 125 | const printComplete = (env: Env) => 126 | `BUILD COMPLETE 127 | ${JSON.stringify(env, null, 2)}`; 128 | 129 | export const run = pipe( 130 | getEnv, 131 | AE.flatmap(parseEnv), 132 | AE.flatmapFirst(() => emptyDir(BUILD_DIR)), 133 | AE.flatmapFirst((env) => build(createBuildOptions(env))), 134 | AE.match( 135 | flow(printBuildError, console.error), 136 | flow(printComplete, console.log), 137 | ), 138 | ); 139 | 140 | await run(); 141 | -------------------------------------------------------------------------------- /scripts/docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | for i in *.ts; do echo "- [$i](https://doc.deno.land/https://raw.githubusercontent.com/baetheus/fun/main/$i)"; done 4 | -------------------------------------------------------------------------------- /scripts/exports.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | for i in *.ts ideas/*.ts; do echo "\"./${i:s/\.ts//}\": \"./$i\","; done 4 | -------------------------------------------------------------------------------- /showable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The Showable module contains utilities for working with the Showable algebraic 3 | * data type. Showable is a structure that indicates that an instance can be 4 | * converted to a string representation. 5 | * 6 | * Showable provides a standardized way to convert any value to a human-readable 7 | * string format. This is particularly useful for debugging, logging, and 8 | * displaying values in user interfaces. The module includes utilities for 9 | * creating Showable instances for complex data structures like objects and tuples. 10 | * 11 | * @module Showable 12 | * @since 2.0.0 13 | */ 14 | 15 | import type { Hold } from "./kind.ts"; 16 | 17 | /** 18 | * A Showable structure has the method show, which converts a value to a string 19 | * representation. This interface extends Hold to ensure type safety and 20 | * consistency with other algebraic structures in the library. 21 | * 22 | * @example 23 | * ```ts 24 | * import * as S from "./showable.ts"; 25 | * 26 | * const ShowableNumber: S.Showable = { 27 | * show: (n: number) => n.toString() 28 | * }; 29 | * 30 | * const result = ShowableNumber.show(42); // "42" 31 | * ``` 32 | * 33 | * @since 2.0.0 34 | */ 35 | export interface Showable extends Hold { 36 | readonly show: (value: U) => string; 37 | } 38 | 39 | /** 40 | * Create a Showable instance for an object type by providing Showable instances 41 | * for each of its properties. The resulting show function will format the object 42 | * as a string with property names and their string representations. 43 | * 44 | * @example 45 | * ```ts 46 | * import * as S from "./showable.ts"; 47 | * import * as N from "./number.ts"; 48 | * import * as Str from "./string.ts"; 49 | * 50 | * type Person = { 51 | * name: string; 52 | * age: number; 53 | * }; 54 | * 55 | * const ShowablePerson = S.struct({ 56 | * name: Str.ShowableString, 57 | * age: N.ShowableNumber 58 | * }); 59 | * 60 | * const person: Person = { name: "Alice", age: 30 }; 61 | * const result = ShowablePerson.show(person); 62 | * // "{ name: Alice, age: 30 }" 63 | * 64 | * const emptyPerson: Person = { name: "", age: 0 }; 65 | * const emptyResult = ShowablePerson.show(emptyPerson); 66 | * // "{ name: , age: 0 }" 67 | * ``` 68 | * 69 | * @since 2.10.0 70 | */ 71 | export function struct( 72 | shows: { [K in keyof A]: Showable }, 73 | ): Showable<{ readonly [K in keyof A]: A[K] }> { 74 | const entries = Object.entries(shows) as [ 75 | keyof A & string, 76 | Showable, 77 | ][]; 78 | return { 79 | show: (struct) => { 80 | const inner = entries 81 | .map(([key, { show }]) => `${key}: ${show(struct[key])}`) 82 | .join(", "); 83 | return inner.length > 0 ? `{ ${inner} }` : "{}"; 84 | }, 85 | }; 86 | } 87 | 88 | /** 89 | * Create a Showable instance for a tuple type by providing Showable instances 90 | * for each element in the tuple. The resulting show function will format the 91 | * tuple as a string with square brackets and comma-separated values. 92 | * 93 | * @example 94 | * ```ts 95 | * import * as S from "./showable.ts"; 96 | * import * as N from "./number.ts"; 97 | * import * as Str from "./string.ts"; 98 | * 99 | * const ShowableTuple = S.tuple<[string, number, boolean]>( 100 | * Str.ShowableString, 101 | * N.ShowableNumber, 102 | * { show: (b: boolean) => b.toString() } 103 | * ); 104 | * 105 | * const tuple: [string, number, boolean] = ["hello", 42, true]; 106 | * const result = ShowableTuple.show(tuple); 107 | * // "[hello, 42, true]" 108 | * 109 | * const emptyTuple: [string, number, boolean] = ["", 0, false]; 110 | * const emptyResult = ShowableTuple.show(emptyTuple); 111 | * // "[, 0, false]" 112 | * ``` 113 | * 114 | * @since 2.10.0 115 | */ 116 | export const tuple = >( 117 | ...shows: { [K in keyof A]: Showable } 118 | ): Showable> => ({ 119 | show: (tuple) => `[${tuple.map((a, i) => shows[i].show(a)).join(", ")}]`, 120 | }); 121 | -------------------------------------------------------------------------------- /testing/applicable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as A from "../applicable.ts"; 4 | import * as O from "../option.ts"; 5 | import * as N from "../number.ts"; 6 | import * as C from "../combinable.ts"; 7 | 8 | Deno.test("Applicable getApplicableCombinable", () => { 9 | const getCombinableNumber = A.getApplicableCombinable(O.ApplicableOption); 10 | const CombinableOptionNumber = getCombinableNumber(N.InitializableNumberSum); 11 | const combineAll = C.getCombineAll(CombinableOptionNumber); 12 | 13 | assertEquals(combineAll(O.none), O.none); 14 | assertEquals(combineAll(O.some(1)), O.some(1)); 15 | assertEquals(combineAll(O.none, O.none), O.none); 16 | assertEquals(combineAll(O.none, O.some(1)), O.none); 17 | assertEquals(combineAll(O.some(1), O.none), O.none); 18 | assertEquals(combineAll(O.some(1), O.some(1)), O.some(2)); 19 | }); 20 | -------------------------------------------------------------------------------- /testing/async.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as A from "../async.ts"; 4 | import * as N from "../number.ts"; 5 | import { pipe } from "../fn.ts"; 6 | 7 | const add = (n: number) => n + 1; 8 | 9 | function throwSync(n: number): number { 10 | if (n % 2 === 0) { 11 | throw new Error(`Number '${n}' is divisible by 2`); 12 | } 13 | return n; 14 | } 15 | 16 | function throwAsync(n: number): Promise { 17 | if (n % 2 === 0) { 18 | return Promise.reject(`Number '${n}' is divisible by 2`); 19 | } 20 | return Promise.resolve(n); 21 | } 22 | 23 | Deno.test("Async of", async () => { 24 | assertEquals(await A.wrap(0)(), 0); 25 | }); 26 | 27 | Deno.test("Async delay", async () => { 28 | assertEquals(await pipe(A.wrap(0), A.delay(200))(), 0); 29 | }); 30 | 31 | Deno.test("Async fromSync", async () => { 32 | assertEquals(await A.fromSync(() => 0)(), 0); 33 | }); 34 | 35 | Deno.test("Async tryCatch", async () => { 36 | assertEquals(await A.tryCatch(throwSync, () => 0)(1)(), 1); 37 | assertEquals(await A.tryCatch(throwSync, () => 0)(2)(), 0); 38 | assertEquals(await A.tryCatch(throwAsync, () => 0)(1)(), 1); 39 | assertEquals(await A.tryCatch(throwAsync, () => 0)(2)(), 0); 40 | }); 41 | 42 | Deno.test("Async of", async () => { 43 | assertEquals(await A.wrap(1)(), 1); 44 | }); 45 | 46 | Deno.test("Async apply", async () => { 47 | assertEquals( 48 | await pipe(A.wrap((n: number) => n + 1), A.apply(A.wrap(1)))(), 49 | 2, 50 | ); 51 | }); 52 | 53 | Deno.test("Async map", async () => { 54 | assertEquals(await pipe(A.wrap(1), A.map(add))(), 2); 55 | }); 56 | 57 | Deno.test("Async flatmap", async () => { 58 | assertEquals(await pipe(A.wrap(1), A.flatmap((n) => A.wrap(n + 1)))(), 2); 59 | }); 60 | 61 | Deno.test("Async apSeq", async () => { 62 | assertEquals( 63 | await pipe(A.wrap((n: number) => n + 1), A.applySequential(A.wrap(1)))(), 64 | 2, 65 | ); 66 | }); 67 | 68 | Deno.test("Async getCombinableAsync", async () => { 69 | const { combine } = A.getCombinableAsync(N.InitializableNumberSum); 70 | assertEquals( 71 | await pipe( 72 | A.wrap(1), 73 | combine(A.wrap(2)), 74 | )(), 75 | 3, 76 | ); 77 | }); 78 | 79 | Deno.test("Async getInitializableAsync", async () => { 80 | const { init, combine } = A.getInitializableAsync(N.InitializableNumberSum); 81 | assertEquals(await init()(), 0); 82 | assertEquals( 83 | await pipe( 84 | A.wrap(1), 85 | combine(A.wrap(2)), 86 | )(), 87 | 3, 88 | ); 89 | }); 90 | 91 | Deno.test("Async tap", async () => { 92 | let out: null | number = null; 93 | await pipe( 94 | A.wrap(1), 95 | A.tap((n) => out = n), 96 | )(); 97 | assertEquals(out, 1); 98 | }); 99 | 100 | Deno.test("Async bind", async () => { 101 | assertEquals( 102 | await pipe( 103 | A.wrap({ a: 1 }), 104 | A.bind("b", ({ a }) => A.wrap(a + 1)), 105 | )(), 106 | { a: 1, b: 2 }, 107 | ); 108 | }); 109 | 110 | Deno.test("Async bindTo", async () => { 111 | assertEquals( 112 | await pipe( 113 | A.wrap(1), 114 | A.bindTo("a"), 115 | )(), 116 | { a: 1 }, 117 | ); 118 | }); 119 | -------------------------------------------------------------------------------- /testing/async_iterable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as AI from "../async_iterable.ts"; 4 | import * as I from "../iterable.ts"; 5 | import * as O from "../option.ts"; 6 | import * as E from "../either.ts"; 7 | import { resolve } from "../promise.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | Deno.test("AsyncIterable asyncIterable", async () => { 11 | const iterable = AI.asyncIterable(async function* () { 12 | let value = 0; 13 | while (value < 5) { 14 | yield ++value; 15 | } 16 | }); 17 | 18 | assertEquals(await AI.collect(iterable), [1, 2, 3, 4, 5]); 19 | }); 20 | 21 | Deno.test("AsyncIterable fromIterable", async () => { 22 | const iterable = I.range(3); 23 | const asyncIterable = AI.fromIterable(iterable); 24 | 25 | assertEquals(await AI.collect(asyncIterable), [0, 1, 2]); 26 | }); 27 | 28 | Deno.test("AsyncIterable fromPromise", async () => { 29 | const asyncIterable = AI.fromPromise(Promise.resolve(1)); 30 | assertEquals(await pipe(AI.collect(asyncIterable)), [1]); 31 | }); 32 | 33 | Deno.test("AsyncIterable range", async () => { 34 | assertEquals(await pipe(AI.range(3), AI.collect), [0, 1, 2]); 35 | }); 36 | 37 | Deno.test("AsyncIterable clone", async () => { 38 | const first = AI.range(3); 39 | const second = AI.clone(first); 40 | 41 | assertEquals(await AI.collect(first), [0, 1, 2]); 42 | assertEquals(await AI.collect(second), [0, 1, 2]); 43 | }); 44 | 45 | Deno.test("AsyncIterable wrap", async () => { 46 | assertEquals(await pipe(AI.wrap(1), AI.collect), [1]); 47 | }); 48 | 49 | Deno.test("AsyncIterable apply", async () => { 50 | assertEquals( 51 | await pipe( 52 | AI.wrap((n: number) => n + 1), 53 | AI.apply(AI.range(3)), 54 | AI.collect, 55 | ), 56 | [1, 2, 3], 57 | ); 58 | }); 59 | 60 | Deno.test("AsyncIterable map", async () => { 61 | assertEquals( 62 | await pipe( 63 | AI.range(3), 64 | AI.map((n) => n + 1), 65 | AI.collect, 66 | ), 67 | [1, 2, 3], 68 | ); 69 | }); 70 | 71 | Deno.test("AsyncIterable flatmap", async () => { 72 | assertEquals( 73 | await pipe( 74 | AI.range(3), 75 | AI.map((n) => n + 1), 76 | AI.flatmap(AI.range), 77 | AI.collect, 78 | ), 79 | [0, 0, 1, 0, 1, 2], 80 | ); 81 | }); 82 | 83 | Deno.test("AsyncIterable forEach", async () => { 84 | const result = new Array(); 85 | await pipe( 86 | AI.range(3), 87 | AI.forEach( 88 | async (n) => await resolve(result.push(n)), 89 | async () => await resolve(result.push(100)), 90 | ), 91 | ); 92 | assertEquals(result, [0, 1, 2, 100]); 93 | 94 | const result2 = new Array(); 95 | await pipe( 96 | AI.range(3), 97 | AI.forEach( 98 | async (n) => await resolve(result2.push(n)), 99 | ), 100 | ); 101 | assertEquals(result2, [0, 1, 2]); 102 | }); 103 | 104 | Deno.test("AsyncIterable delay", async () => { 105 | assertEquals( 106 | await pipe( 107 | AI.range(3), 108 | AI.delay(100), 109 | AI.collect, 110 | ), 111 | [0, 1, 2], 112 | ); 113 | }); 114 | 115 | Deno.test("AsyncIterable filter", async () => { 116 | assertEquals( 117 | await pipe( 118 | AI.range(4), 119 | AI.filter((n) => n > 1), 120 | AI.collect, 121 | ), 122 | [2, 3], 123 | ); 124 | }); 125 | 126 | Deno.test("AsyncIterable filterMap", async () => { 127 | assertEquals( 128 | await pipe( 129 | AI.range(4), 130 | AI.filterMap(O.fromPredicate((n) => n > 1)), 131 | AI.collect, 132 | ), 133 | [2, 3], 134 | ); 135 | }); 136 | 137 | Deno.test("AsyncIterable partition", async () => { 138 | const [first, second] = pipe( 139 | AI.range(4), 140 | AI.partition((n) => n > 1), 141 | ); 142 | 143 | assertEquals(await AI.collect(first), [2, 3]); 144 | assertEquals(await AI.collect(second), [0, 1]); 145 | }); 146 | 147 | Deno.test("AsyncIterable partitionMap", async () => { 148 | const [first, second] = pipe( 149 | AI.range(4), 150 | AI.partitionMap((n) => n > 1 ? E.right(n + 10) : E.left(n)), 151 | ); 152 | 153 | assertEquals(await AI.collect(first), [12, 13]); 154 | assertEquals(await AI.collect(second), [0, 1]); 155 | }); 156 | 157 | Deno.test("AsyncIterable fold", async () => { 158 | assertEquals( 159 | await pipe( 160 | AI.range(3), 161 | AI.fold((value, sum) => value + sum, 0), 162 | ), 163 | 3, 164 | ); 165 | }); 166 | 167 | Deno.test("AsyncIterable takeUntil", async () => { 168 | assertEquals( 169 | await pipe( 170 | AI.range(), 171 | AI.takeUntil((n) => n > 3), 172 | AI.collect, 173 | ), 174 | [0, 1, 2, 3], 175 | ); 176 | }); 177 | 178 | Deno.test("AsyncIterable takeWhile", async () => { 179 | assertEquals( 180 | await pipe( 181 | AI.range(), 182 | AI.takeWhile((n) => n < 3), 183 | AI.collect, 184 | ), 185 | [0, 1, 2], 186 | ); 187 | }); 188 | 189 | Deno.test("AsyncIterable scan", async () => { 190 | assertEquals( 191 | await pipe( 192 | AI.range(5), 193 | AI.scan((value, sum) => value + sum, 0), 194 | AI.collect, 195 | ), 196 | [0, 1, 3, 6, 10], 197 | ); 198 | }); 199 | 200 | Deno.test("AsyncIterable tap", async () => { 201 | let value = 0; 202 | await pipe( 203 | AI.range(3), 204 | AI.tap((n) => value = n), 205 | AI.collect, 206 | ); 207 | assertEquals(value, 2); 208 | }); 209 | 210 | Deno.test("AsyncIterable repeat", async () => { 211 | assertEquals( 212 | await pipe( 213 | AI.range(3), 214 | AI.repeat(2), 215 | AI.collect, 216 | ), 217 | [0, 1, 2, 0, 1, 2], 218 | ); 219 | }); 220 | 221 | Deno.test("AsyncIterable take", async () => { 222 | assertEquals( 223 | await pipe( 224 | AI.range(), 225 | AI.take(3), 226 | AI.collect, 227 | ), 228 | [0, 1, 2], 229 | ); 230 | }); 231 | -------------------------------------------------------------------------------- /testing/boolean.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as B from "../boolean.ts"; 4 | 5 | Deno.test("Boolean constTrue", () => { 6 | assertEquals(B.constTrue(), true); 7 | }); 8 | 9 | Deno.test("Boolean constFalse", () => { 10 | assertEquals(B.constFalse(), false); 11 | }); 12 | 13 | Deno.test("Boolean isBoolean", () => { 14 | assertEquals(B.isBoolean(1), false); 15 | assertEquals(B.isBoolean(true), true); 16 | assertEquals(B.isBoolean(false), true); 17 | }); 18 | 19 | Deno.test("Boolean match", () => { 20 | const match = B.match(() => 1, () => 2); 21 | assertEquals(match(true), 1); 22 | assertEquals(match(false), 2); 23 | }); 24 | 25 | Deno.test("Boolean not", () => { 26 | assertEquals(B.not(true), false); 27 | assertEquals(B.not(false), true); 28 | }); 29 | 30 | Deno.test("Boolean compare", () => { 31 | assertEquals(B.compare(true)(true), true); 32 | assertEquals(B.compare(true)(false), false); 33 | assertEquals(B.compare(false)(true), false); 34 | assertEquals(B.compare(false)(false), true); 35 | }); 36 | 37 | Deno.test("Boolean sort", () => { 38 | assertEquals(B.sort(true, true), 0); 39 | assertEquals(B.sort(true, false), 1); 40 | assertEquals(B.sort(false, true), -1); 41 | assertEquals(B.sort(false, false), 0); 42 | }); 43 | 44 | Deno.test("Boolean or", () => { 45 | assertEquals(B.or(true)(true), true); 46 | assertEquals(B.or(true)(false), true); 47 | assertEquals(B.or(false)(true), true); 48 | assertEquals(B.or(false)(false), false); 49 | }); 50 | 51 | Deno.test("Boolean and", () => { 52 | assertEquals(B.and(true)(true), true); 53 | assertEquals(B.and(true)(false), false); 54 | assertEquals(B.and(false)(true), false); 55 | assertEquals(B.and(false)(false), false); 56 | }); 57 | -------------------------------------------------------------------------------- /testing/combinable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import * as SG from "../combinable.ts"; 4 | import * as S from "../string.ts"; 5 | import * as N from "../number.ts"; 6 | import * as B from "../boolean.ts"; 7 | import { pipe } from "../fn.ts"; 8 | 9 | // const toArray: (a: SG.Free) => number[] = SG.Free.combineAll( 10 | // (value: number) => [value], 11 | // (left, right) => toArray(left).combine(toArray(right)), 12 | // ); 13 | // 14 | // Deno.test("Combinable Free", () => { 15 | // const ta = SG.Free.wrap(1); 16 | // const tb = SG.Free.combine(ta)(ta); 17 | // assertEquals(ta, { tag: "Of", value: 1 }); 18 | // assertEquals(tb, { tag: "Concat", left: ta, right: ta }); 19 | // assertEquals(toArray(ta), [1]); 20 | // assertEquals(toArray(tb), [1, 1]); 21 | // }); 22 | // 23 | // Deno.test("Combinable semigroupAll", () => { 24 | // const combine = B.CombinableBooleanAll.combine; 25 | // const combineTrue = combine(true); 26 | // const combineFalse = combine(false); 27 | // 28 | // assertEquals(typeof combine, "function"); 29 | // assertEquals(typeof combineTrue, "function"); 30 | // 31 | // assertEquals(typeof combineFalse, "function"); 32 | // assertEquals(combineTrue(true), true); 33 | // assertEquals(combineTrue(false), false); 34 | // assertEquals(combineFalse(true), false); 35 | // assertEquals(combineFalse(false), false); 36 | // }); 37 | // 38 | // Deno.test("Combinable semigroupAny", () => { 39 | // const combine = SG.semigroupAny.combine; 40 | // const combineTrue = combine(true); 41 | // const combineFalse = combine(false); 42 | // 43 | // assertEquals(typeof combine, "function"); 44 | // assertEquals(typeof combineTrue, "function"); 45 | // assertEquals(typeof combineFalse, "function"); 46 | // 47 | // assertEquals(combineTrue(true), true); 48 | // assertEquals(combineTrue(false), true); 49 | // assertEquals(combineFalse(true), true); 50 | // assertEquals(combineFalse(false), false); 51 | // }); 52 | // 53 | // Deno.test("Combinable semigroupSum", () => { 54 | // const combine = N.InitializableNumberSum.combine; 55 | // const combineZero = combine(0); 56 | // const combineOne = combine(1); 57 | // 58 | // assertEquals(typeof combine, "function"); 59 | // assertEquals(typeof combineZero, "function"); 60 | // assertEquals(typeof combineOne, "function"); 61 | // 62 | // assertEquals(combineOne(1), 2); 63 | // assertEquals(combineZero(1), 1); 64 | // assertEquals(combineOne(0), 1); 65 | // assertEquals(combineZero(0), 0); 66 | // }); 67 | // 68 | // Deno.test("Combinable semigroupProduct", () => { 69 | // const combine = SG.semigroupProduct.combine; 70 | // const combineOne = combine(1); 71 | // const combineTwo = combine(2); 72 | // 73 | // assertEquals(typeof combine, "function"); 74 | // assertEquals(typeof combineOne, "function"); 75 | // assertEquals(typeof combineTwo, "function"); 76 | // 77 | // assertEquals(combineOne(2), 2); 78 | // assertEquals(combineTwo(2), 4); 79 | // }); 80 | // 81 | // Deno.test("Combinable semigroupString", () => { 82 | // const combine = S.Combinable.combine; 83 | // const combineEmpty = combine(""); 84 | // const combineHello = combine("Hello"); 85 | // 86 | // assertEquals(typeof combine, "function"); 87 | // assertEquals(typeof combineEmpty, "function"); 88 | // assertEquals(typeof combineHello, "function"); 89 | // 90 | // assertEquals(combineEmpty("World"), "World"); 91 | // assertEquals(combineHello("World"), "HelloWorld"); 92 | // }); 93 | // 94 | // Deno.test("Combinable semigroupVoid", () => { 95 | // const combine = SG.semigroupVoid.combine; 96 | // const combineUndefined = combine(undefined); 97 | // 98 | // assertEquals(typeof combine, "function"); 99 | // assertEquals(typeof combineUndefined, "function"); 100 | // 101 | // assertEquals(combineUndefined(undefined), undefined); 102 | // }); 103 | // 104 | // Deno.test("Combinable getFreeCombinable", () => { 105 | // const { combine } = SG.getFreeCombinable(); 106 | // const ta = SG.Free.wrap(1); 107 | // const tb = SG.Free.wrap(2); 108 | // const tc = combine(ta)(tb); 109 | // const td = combine(tc)(tb); 110 | // assertEquals(toArray(td), [1, 2, 2]); 111 | // }); 112 | 113 | Deno.test("Combinable first", () => { 114 | const { combine } = SG.first(); 115 | assertEquals(combine(1)(2), 2); 116 | }); 117 | 118 | Deno.test("Combinable last", () => { 119 | const { combine } = SG.last(); 120 | assertEquals(combine(1)(2), 1); 121 | }); 122 | 123 | Deno.test("Combinable tuple", () => { 124 | const { combine } = SG.tuple( 125 | N.InitializableNumberSum, 126 | pipe(S.InitializableString, SG.intercalcate(" ")), 127 | ); 128 | assertEquals(pipe([1, "Hello"], combine([2, "World"])), [3, "Hello World"]); 129 | }); 130 | 131 | Deno.test("Combinable dual", () => { 132 | const { combine } = SG.dual(SG.first()); 133 | assertEquals(combine(1)(2), 1); 134 | }); 135 | 136 | Deno.test("Combinable struct", () => { 137 | const { combine } = SG.struct({ 138 | a: N.InitializableNumberSum, 139 | b: B.InitializableBooleanAll, 140 | }); 141 | assertEquals(combine({ a: 1, b: true })({ a: 2, b: false }), { 142 | a: 3, 143 | b: false, 144 | }); 145 | }); 146 | 147 | Deno.test("Combinable min", () => { 148 | const { combine } = SG.min(N.SortableNumber); 149 | assertEquals(combine(0)(0), 0); 150 | assertEquals(combine(0)(1), 0); 151 | assertEquals(combine(1)(0), 0); 152 | assertEquals(combine(1)(1), 1); 153 | }); 154 | 155 | Deno.test("Combinable max", () => { 156 | const { combine } = SG.max(N.SortableNumber); 157 | assertEquals(combine(0)(0), 0); 158 | assertEquals(combine(0)(1), 1); 159 | assertEquals(combine(1)(0), 1); 160 | assertEquals(combine(1)(1), 1); 161 | }); 162 | 163 | Deno.test("Combinable constant", () => { 164 | const { combine } = SG.constant("cake"); 165 | assertEquals(pipe("apple", combine("banana")), "cake"); 166 | }); 167 | 168 | Deno.test("Combinable combineAll", () => { 169 | const combineAllSum = SG.getCombineAll(N.InitializableNumberSum); 170 | 171 | assertEquals(typeof combineAllSum, "function"); 172 | 173 | assertEquals(combineAllSum(1, 2, 3), 6); 174 | }); 175 | -------------------------------------------------------------------------------- /testing/composable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import type * as C from "../composable.ts"; 4 | import * as F from "../fn.ts"; 5 | 6 | Deno.test("Composable interface", () => { 7 | const ComposableFn: C.Composable = { 8 | id: F.id, 9 | compose: F.compose, 10 | }; 11 | assertEquals(ComposableFn, ComposableFn); 12 | }); 13 | -------------------------------------------------------------------------------- /testing/failable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as F from "../failable.ts"; 4 | import * as E from "../either.ts"; 5 | 6 | Deno.test("Failable createTap", () => { 7 | const tap = F.createTap(E.FailableEither); 8 | 9 | let success: number | undefined; 10 | let failure: number | undefined; 11 | 12 | const _tap = tap((s: number) => { 13 | success = s; 14 | }, (f: number) => { 15 | failure = f; 16 | }); 17 | 18 | _tap(E.right(1)); 19 | _tap(E.left(2)); 20 | 21 | assertEquals([success, failure], [1, 2]); 22 | }); 23 | -------------------------------------------------------------------------------- /testing/flatmappable.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std/testing/asserts.ts"; 5 | 6 | import * as D from "../flatmappable.ts"; 7 | import * as O from "../option.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | Deno.test("Flatmappable createTap", () => { 11 | const tap = D.createTap(O.FlatmappableOption); 12 | let result: unknown = 0; 13 | pipe(O.some(1), tap((n) => result = n)); 14 | assertEquals(result, 1); 15 | }); 16 | 17 | Deno.test("Flatmappable createBind", () => { 18 | const bind = D.createBind(O.FlatmappableOption); 19 | assertEquals( 20 | pipe( 21 | O.some({}), 22 | bind("one", () => O.some(1)), 23 | ), 24 | O.some({ one: 1 }), 25 | ); 26 | }); 27 | 28 | Deno.test("Flatmappable createFlatmappable", () => { 29 | const M = D.createFlatmappable({ 30 | wrap: O.wrap, 31 | flatmap: O.flatmap, 32 | }); 33 | 34 | // wrap 35 | assertStrictEquals(O.wrap, M.wrap); 36 | 37 | // flatmap 38 | assertStrictEquals(O.flatmap, M.flatmap); 39 | 40 | // apply 41 | const add = (n: number) => n + 1; 42 | assertEquals(pipe(O.some(add), M.apply(O.some(1))), O.some(2)); 43 | assertEquals(pipe(O.some(add), M.apply(O.none)), O.none); 44 | assertEquals(pipe(O.none, M.apply(O.some(1))), O.none); 45 | assertEquals(pipe(O.none, M.apply(O.none)), O.none); 46 | 47 | // map 48 | assertEquals(pipe(O.some(1), M.map((n) => n + 1)), O.some(2)); 49 | assertEquals(pipe(O.constNone(), M.map((n) => n + 1)), O.none); 50 | }); 51 | -------------------------------------------------------------------------------- /testing/fn.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | assertThrows, 5 | } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 6 | 7 | import * as F from "../fn.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | const add = (n: number) => n + 1; 11 | 12 | Deno.test("Fn unary", () => { 13 | const variadic = (a: number, b: string) => a + b.length; 14 | const unary = F.unary(variadic); 15 | assertEquals(unary([1, "Hello"]), variadic(1, "Hello")); 16 | }); 17 | 18 | Deno.test("Fn curry2", () => { 19 | const curry = F.curry2((n: number, m: number) => n + m); 20 | assertEquals(curry(1)(2), 3); 21 | }); 22 | 23 | Deno.test("Fn uncurry2", () => { 24 | const uncurry = F.uncurry2((second: number) => (first: number) => 25 | first + second 26 | ); 27 | assertEquals(uncurry(1, 2), 3); 28 | }); 29 | 30 | Deno.test("Fn tryThunk", () => { 31 | assertEquals( 32 | F.tryThunk(F.todo, String), 33 | "Error: TODO: this function has not been implemented", 34 | ); 35 | assertEquals(F.tryThunk(() => 1, () => 2), 1); 36 | }); 37 | 38 | Deno.test("Fn handleThrow", () => { 39 | const throws = (_: number): number => F.todo(); 40 | 41 | assertEquals( 42 | F.handleThrow(throws, (a, d) => [a, d], (e, d) => [String(e).length, d])(1), 43 | [51, [1]], 44 | ); 45 | assertEquals( 46 | F.handleThrow( 47 | F.id(), 48 | (a, d) => [a, d], 49 | (e, d) => [String(e).length, d], 50 | )(1), 51 | [1, [1]], 52 | ); 53 | }); 54 | 55 | Deno.test("Fn tryCatch", () => { 56 | const throws = (_: number): number => F.todo(); 57 | const add = (n: number): number => n + 1; 58 | 59 | assertEquals(F.tryCatch(add, (n) => n)(1), 2); 60 | assertEquals( 61 | F.tryCatch(throws, (n) => n)(1), 62 | new Error("TODO: this function has not been implemented"), 63 | ); 64 | }); 65 | 66 | Deno.test("Fn memoize", () => { 67 | const obj = (n: number) => ({ n }); 68 | const memo = F.memoize(obj); 69 | 70 | assertEquals(memo(1), memo(1)); 71 | }); 72 | 73 | Deno.test("Fn todo", () => { 74 | assertThrows(F.todo); 75 | }); 76 | 77 | Deno.test("Fn unsafeCoerce", () => { 78 | const a = { n: 1 }; 79 | assertStrictEquals(F.unsafeCoerce(a), a); 80 | }); 81 | 82 | Deno.test("Fn pipe", () => { 83 | const fab = (n: number) => n + 1; 84 | 85 | const r0 = F.pipe(0); 86 | const r1 = F.pipe(0, fab); 87 | const r2 = F.pipe(0, fab, fab); 88 | const r3 = F.pipe(0, fab, fab, fab); 89 | const r4 = F.pipe(0, fab, fab, fab, fab); 90 | const r5 = F.pipe(0, fab, fab, fab, fab, fab); 91 | const r6 = F.pipe(0, fab, fab, fab, fab, fab, fab); 92 | const r7 = F.pipe(0, fab, fab, fab, fab, fab, fab, fab); 93 | const r8 = F.pipe(0, fab, fab, fab, fab, fab, fab, fab, fab); 94 | const r9 = F.pipe(0, fab, fab, fab, fab, fab, fab, fab, fab, fab); 95 | 96 | assertEquals([r0, r1, r2, r3, r4, r5, r6, r7, r8, r9], [ 97 | 0, 98 | 1, 99 | 2, 100 | 3, 101 | 4, 102 | 5, 103 | 6, 104 | 7, 105 | 8, 106 | 9, 107 | ]); 108 | }); 109 | 110 | Deno.test("Fn flow", () => { 111 | const fab = (n: number) => n + 1; 112 | 113 | const r1 = F.flow(fab); 114 | const r2 = F.flow(fab, fab); 115 | const r3 = F.flow(fab, fab, fab); 116 | const r4 = F.flow(fab, fab, fab, fab); 117 | const r5 = F.flow(fab, fab, fab, fab, fab); 118 | const r6 = F.flow(fab, fab, fab, fab, fab, fab); 119 | const r7 = F.flow(fab, fab, fab, fab, fab, fab, fab); 120 | const r8 = F.flow(fab, fab, fab, fab, fab, fab, fab, fab); 121 | const r9 = F.flow(fab, fab, fab, fab, fab, fab, fab, fab, fab); 122 | const r10 = F.flow(fab, fab, fab, fab, fab, fab, fab, fab, fab, fab); 123 | const r11 = F.flow(fab, fab, fab, fab, fab, fab, fab, fab, fab, fab, fab); 124 | const r12 = F.flow( 125 | ...([ 126 | fab, 127 | fab, 128 | fab, 129 | fab, 130 | fab, 131 | fab, 132 | fab, 133 | fab, 134 | fab, 135 | fab, 136 | fab, 137 | fab, 138 | ] as unknown as [typeof fab]), 139 | ); 140 | 141 | assertEquals(r1(0), 1); 142 | assertEquals(r2(0), 2); 143 | assertEquals(r3(0), 3); 144 | assertEquals(r4(0), 4); 145 | assertEquals(r5(0), 5); 146 | assertEquals(r6(0), 6); 147 | assertEquals(r7(0), 7); 148 | assertEquals(r8(0), 8); 149 | assertEquals(r9(0), 9); 150 | assertEquals(r10(0), 10); 151 | assertEquals(r11(0), 11); 152 | assertEquals(r12(0), 12); 153 | }); 154 | 155 | Deno.test("Fn of", () => { 156 | const a = F.wrap(1); 157 | assertEquals(a(null), 1); 158 | }); 159 | 160 | Deno.test("Fn ap", () => { 161 | assertEquals( 162 | pipe(F.wrap((n: number) => n + 1), F.apply(F.wrap(1)))(null), 163 | F.wrap(2)(null), 164 | ); 165 | }); 166 | 167 | Deno.test("Fn map", () => { 168 | assertEquals(pipe(F.wrap(0), F.map(add))(null), F.wrap(1)(null)); 169 | }); 170 | 171 | Deno.test("Fn flatmap", () => { 172 | const flatmap = F.flatmap((n: number) => F.wrap(n + 1)); 173 | assertEquals(flatmap(F.wrap(0))(null), F.wrap(1)(null)); 174 | }); 175 | 176 | Deno.test("Fn premap", () => { 177 | assertEquals( 178 | pipe( 179 | F.id(), 180 | F.premap((s: string) => s.length), 181 | )("Hello"), 182 | 5, 183 | ); 184 | }); 185 | 186 | Deno.test("Fn dimap", () => { 187 | assertEquals( 188 | pipe( 189 | F.id(), 190 | F.dimap((s: string) => s.length, (n) => 2 * n), 191 | )("Hello"), 192 | 10, 193 | ); 194 | }); 195 | 196 | Deno.test("Fn identity", () => { 197 | [0, "hello", undefined, null, {}].forEach((v) => { 198 | assertEquals(F.identity(v), v); 199 | }); 200 | }); 201 | 202 | Deno.test("Fn id", () => { 203 | assertStrictEquals(F.id(), F.identity); 204 | }); 205 | 206 | Deno.test("Fn compose", () => { 207 | const result = pipe( 208 | (n: [string, string]) => n[0], 209 | F.compose((s: string) => s.length), 210 | ); 211 | assertEquals(result(["hello", "wurl"]), 5); 212 | }); 213 | -------------------------------------------------------------------------------- /testing/fn_either.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std/testing/asserts.ts"; 5 | 6 | import * as FE from "../fn_either.ts"; 7 | import * as E from "../either.ts"; 8 | import * as F from "../fn.ts"; 9 | import { InitializableNumberSum } from "../number.ts"; 10 | import { pipe } from "../fn.ts"; 11 | 12 | const fn = FE.fromPredicate(Number.isInteger); 13 | 14 | Deno.test("FnEither tryCatch", () => { 15 | const throwOnZero = FE.tryCatch( 16 | (n: number) => { 17 | if (n === 0) { 18 | throw new Error("Zero"); 19 | } 20 | return n; 21 | }, 22 | (_, [n]) => n, 23 | ); 24 | assertEquals(throwOnZero(0)(0), E.left(0)); 25 | assertEquals(throwOnZero(1)(0), E.right(1)); 26 | }); 27 | 28 | Deno.test("FnEither left", () => { 29 | assertEquals(FE.left("Hello")(0), E.left("Hello")); 30 | }); 31 | 32 | Deno.test("FnEither right", () => { 33 | assertEquals(FE.right("Hello")(0), E.right("Hello")); 34 | }); 35 | 36 | Deno.test("FnEither fromEither", () => { 37 | assertEquals(FE.fromEither(E.left(0))(0), E.left(0)); 38 | assertEquals(FE.fromEither(E.right(0))(0), E.right(0)); 39 | }); 40 | 41 | Deno.test("FnEither fromFn", () => { 42 | assertEquals(FE.fromFn(F.id())(1), E.right(1)); 43 | }); 44 | 45 | Deno.test("FnEither fromPredicate", () => { 46 | const pred = FE.fromPredicate((n: number) => n > 0); 47 | const refine = FE.fromPredicate((s: string): s is "Big" => s === "Big"); 48 | 49 | assertEquals(pred(0), E.left(0)); 50 | assertEquals(pred(1), E.right(1)); 51 | assertEquals(refine("Hello"), E.left("Hello")); 52 | assertEquals(refine("Big"), E.right("Big")); 53 | }); 54 | 55 | Deno.test("FnEither wrap", () => { 56 | assertEquals(FE.wrap(0)(0), E.right(0)); 57 | }); 58 | 59 | Deno.test("FnEither fail", () => { 60 | assertEquals(FE.fail(1)(1), E.left(1)); 61 | }); 62 | 63 | Deno.test("FnEither apply", () => { 64 | const add = (n: number) => n + 1; 65 | assertEquals(pipe(FE.right(add), FE.apply(FE.right(1)))(0), FE.right(2)(0)); 66 | assertEquals(pipe(FE.right(add), FE.apply(FE.left(1)))(0), FE.left(1)(0)); 67 | assertEquals(pipe(FE.left(1), FE.apply(FE.right(1)))(0), FE.left(1)(0)); 68 | assertEquals(pipe(FE.left(1), FE.apply(FE.left(2)))(0), FE.left(2)(0)); 69 | }); 70 | 71 | Deno.test("FnEither alt", () => { 72 | assertEquals(pipe(FE.right(0), FE.alt(FE.right(1)))(0), FE.right(0)(0)); 73 | assertEquals(pipe(FE.right(0), FE.alt(FE.left(1)))(0), FE.right(0)(0)); 74 | assertEquals(pipe(FE.left(0), FE.alt(FE.right(1)))(0), FE.right(1)(0)); 75 | assertEquals(pipe(FE.left(0), FE.alt(FE.left(1)))(0), FE.left(1)(0)); 76 | }); 77 | 78 | Deno.test("FnEither bimap", () => { 79 | const bimap = FE.bimap((n: number) => n + n, (n: number) => n * n); 80 | assertEquals(bimap(FE.left(1))(0), E.left(2)); 81 | assertEquals(bimap(FE.right(1))(0), E.right(1)); 82 | }); 83 | 84 | Deno.test("FnEither map", () => { 85 | assertEquals(pipe(FE.right(1), FE.map((n) => n + 1))(0), E.right(2)); 86 | assertEquals(pipe(FE.left(1), FE.map((n: number) => n + 1))(0), E.left(1)); 87 | }); 88 | 89 | Deno.test("FnEither mapSecond", () => { 90 | assertEquals( 91 | pipe(FE.right(1), FE.mapSecond((n: number) => n + 1))(0), 92 | E.right(1), 93 | ); 94 | assertEquals( 95 | pipe(FE.left(1), FE.mapSecond((n: number) => n + 1))(0), 96 | E.left(2), 97 | ); 98 | }); 99 | 100 | Deno.test("FnEither join", () => { 101 | assertEquals(pipe(FE.wrap(FE.wrap(0)), FE.join)(0), FE.wrap(0)(0)); 102 | assertEquals(pipe(FE.wrap(FE.left(0)), FE.join)(0), FE.left(0)(0)); 103 | assertEquals(pipe(FE.left(0), FE.join)(0), FE.left(0)(0)); 104 | }); 105 | 106 | Deno.test("FnEither flatmap", () => { 107 | assertEquals( 108 | pipe( 109 | FE.wrap(0), 110 | FE.flatmap((n: number) => n === 0 ? FE.left(n) : FE.right(n)), 111 | )(0), 112 | FE.left(0)(0), 113 | ); 114 | assertEquals( 115 | pipe(FE.right(1), FE.flatmap((n) => n === 0 ? FE.left(n) : FE.right(n)))(0), 116 | FE.right(1)(0), 117 | ); 118 | assertEquals( 119 | pipe(FE.left(1), FE.flatmap((n) => n === 0 ? FE.left(n) : FE.right(n)))(0), 120 | FE.left(1)(0), 121 | ); 122 | }); 123 | 124 | Deno.test("FnEither recover", () => { 125 | const recover = FE.recover((n: number) => 126 | n > 0 ? FE.id() : FE.idLeft() 127 | ); 128 | assertEquals(pipe(recover(FE.id()))(0), E.right(0)); 129 | assertEquals(pipe(recover(FE.id()))(1), E.right(1)); 130 | assertEquals(pipe(recover(FE.idLeft()))(0), E.left(0)); 131 | assertEquals(pipe(recover(FE.idLeft()))(1), E.right(1)); 132 | }); 133 | 134 | Deno.test("FnEither premap", () => { 135 | const premap = pipe( 136 | FE.id(), 137 | FE.premap((d: Date) => d.valueOf()), 138 | ); 139 | assertEquals(premap(new Date(0)), E.right(0)); 140 | }); 141 | 142 | Deno.test("FnEither dimap", () => { 143 | const dimap = pipe( 144 | FE.id(), 145 | FE.dimap((d: Date) => d.valueOf(), (n) => n > 0), 146 | ); 147 | assertEquals(dimap(new Date(0)), E.right(false)); 148 | assertEquals(dimap(new Date(1)), E.right(true)); 149 | }); 150 | 151 | Deno.test("FnEither id", () => { 152 | assertEquals(FE.id()(1), E.right(1)); 153 | }); 154 | 155 | Deno.test("FnEither idLeft", () => { 156 | assertEquals(FE.idLeft()(1), E.left(1)); 157 | }); 158 | 159 | Deno.test("FnEither compose", () => { 160 | const isPositive = FE.fromPredicate((n: number) => n > 0); 161 | const composition = pipe(isPositive, FE.compose(fn)); 162 | 163 | assertEquals(composition(0), E.left(0)); 164 | assertEquals(composition(1), E.right(1)); 165 | assertEquals(composition(1.5), E.left(1.5)); 166 | }); 167 | 168 | Deno.test("FnEither BimappableFnEither", () => { 169 | assertStrictEquals(FE.BimappableFnEither.map, FE.map); 170 | assertStrictEquals(FE.BimappableFnEither.mapSecond, FE.mapSecond); 171 | }); 172 | 173 | Deno.test("FnEither FlatmappableFnEither", () => { 174 | assertStrictEquals(FE.FlatmappableFnEither.wrap, FE.wrap); 175 | assertStrictEquals(FE.FlatmappableFnEither.apply, FE.apply); 176 | assertStrictEquals(FE.FlatmappableFnEither.map, FE.map); 177 | assertStrictEquals(FE.FlatmappableFnEither.flatmap, FE.flatmap); 178 | }); 179 | 180 | Deno.test("FnEither PremappableFnEither", () => { 181 | assertStrictEquals(FE.PremappableFnEither.premap, FE.premap); 182 | }); 183 | 184 | Deno.test("FnEither getRightFlatmappable", () => { 185 | const { wrap, apply, map, flatmap } = FE.getRightFlatmappable( 186 | InitializableNumberSum, 187 | ); 188 | assertStrictEquals(wrap, FE.wrap); 189 | assertStrictEquals(map, FE.map); 190 | assertStrictEquals(flatmap, FE.flatmap); 191 | 192 | const add = (n: number) => n + 1; 193 | assertEquals(pipe(FE.right(add), apply(FE.right(1)))(1), FE.right(2)(0)); 194 | assertEquals(pipe(FE.right(add), apply(FE.left(1)))(1), FE.left(1)(0)); 195 | assertEquals(pipe(FE.left(1), apply(FE.right(1)))(1), FE.left(1)(0)); 196 | assertEquals(pipe(FE.left(1), apply(FE.left(2)))(1), FE.left(3)(0)); 197 | }); 198 | -------------------------------------------------------------------------------- /testing/foldable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as R from "../foldable.ts"; 4 | import * as A from "../array.ts"; 5 | import * as N from "../number.ts"; 6 | 7 | Deno.test("Foldable collect", () => { 8 | const collect = R.collect(A.FoldableArray, N.InitializableNumberSum); 9 | assertEquals(collect([]), 0); 10 | assertEquals(collect([1, 2, 3, 4]), 10); 11 | }); 12 | -------------------------------------------------------------------------------- /testing/free.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 5 | 6 | import * as F from "../free.ts"; 7 | import { pipe } from "../fn.ts"; 8 | 9 | Deno.test("Free node", () => { 10 | assertEquals(F.node(1), { tag: "Node", value: 1 }); 11 | }); 12 | 13 | Deno.test("Free link", () => { 14 | assertEquals(F.link(F.node(1), F.node(2)), { 15 | tag: "Link", 16 | first: { tag: "Node", value: 1 }, 17 | second: { tag: "Node", value: 2 }, 18 | }); 19 | }); 20 | 21 | Deno.test("Free isNode", () => { 22 | assertEquals(F.isNode(F.node(1)), true); 23 | assertEquals(F.isNode(F.link(F.node(1), F.node(2))), false); 24 | }); 25 | 26 | Deno.test("Free isLink", () => { 27 | assertEquals(F.isLink(F.node(1)), false); 28 | assertEquals(F.isLink(F.link(F.node(1), F.node(2))), true); 29 | }); 30 | 31 | Deno.test("Free match", () => { 32 | const sum = F.match( 33 | (n: number): number => n, 34 | (first: F.Free, second: F.Free): number => 35 | sum(first) + sum(second), 36 | ); 37 | 38 | assertEquals(sum(F.node(1)), 1); 39 | assertEquals(sum(F.link(F.node(1), F.node(1))), 2); 40 | }); 41 | 42 | Deno.test("Free combine", () => { 43 | assertEquals( 44 | pipe(F.node(1), F.combine(F.node(2))), 45 | F.link(F.node(1), F.node(2)), 46 | ); 47 | }); 48 | 49 | Deno.test("Free wrap", () => { 50 | assertEquals(F.wrap(1), F.node(1)); 51 | }); 52 | 53 | Deno.test("Free map", () => { 54 | assertEquals(pipe(F.node(1), F.map((n) => n + 1)), F.node(2)); 55 | assertEquals( 56 | pipe(F.link(F.node(1), F.node(2)), F.map((n) => n + 1)), 57 | F.link(F.node(2), F.node(3)), 58 | ); 59 | }); 60 | 61 | Deno.test("Free flatmap", () => { 62 | assertEquals( 63 | pipe( 64 | F.node(1), 65 | F.flatmap((n) => F.node(n + 1)), 66 | ), 67 | F.node(2), 68 | ); 69 | assertEquals( 70 | pipe( 71 | F.link(F.node(1), F.link(F.node(2), F.node(3))), 72 | F.flatmap((n) => F.link(F.node(n), F.node(n + 100))), 73 | ), 74 | F.link( 75 | F.link(F.node(1), F.node(101)), 76 | F.link(F.link(F.node(2), F.node(102)), F.link(F.node(3), F.node(103))), 77 | ), 78 | ); 79 | }); 80 | 81 | Deno.test("Free apply", () => { 82 | assertEquals( 83 | pipe( 84 | F.node((n: number) => n + 1), 85 | F.apply(F.node(1)), 86 | ), 87 | F.node(2), 88 | ); 89 | assertEquals( 90 | pipe( 91 | F.link(F.node((n: number) => n + 1), F.node((n) => n - 1)), 92 | F.apply(F.node(1)), 93 | ), 94 | F.link(F.node(2), F.node(0)), 95 | ); 96 | assertEquals( 97 | pipe( 98 | F.node((n: number) => n + 1), 99 | F.apply(F.link(F.node(1), F.node(2))), 100 | ), 101 | F.link(F.node(2), F.node(3)), 102 | ); 103 | assertEquals( 104 | pipe( 105 | F.link(F.node((n: number) => n + 1), F.node((n) => n + 10)), 106 | F.apply(F.link(F.node(1), F.node(2))), 107 | ), 108 | F.link( 109 | F.link(F.node(2), F.node(3)), 110 | F.link(F.node(11), F.node(12)), 111 | ), 112 | ); 113 | }); 114 | 115 | Deno.test("Free fold", () => { 116 | assertEquals( 117 | pipe(F.link(F.node(1), F.node(1)), F.fold((n, m) => n + m, 0)), 118 | 2, 119 | ); 120 | }); 121 | 122 | Deno.test("Free getCombinable", () => { 123 | const Combinable = F.getCombinable(); 124 | assertStrictEquals(Combinable.combine, F.combine); 125 | }); 126 | -------------------------------------------------------------------------------- /testing/identity.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import * as I from "../identity.ts"; 4 | import { pipe } from "../fn.ts"; 5 | 6 | const add = (n: number) => n + 1; 7 | 8 | Deno.test("Identity of", () => { 9 | assertEquals(I.wrap(1), 1); 10 | }); 11 | 12 | Deno.test("Identity ap", () => { 13 | assertEquals( 14 | pipe(I.wrap((n: number) => n + 1), I.apply(I.wrap(1))), 15 | I.wrap(2), 16 | ); 17 | }); 18 | 19 | Deno.test("Identity map", () => { 20 | const map = I.map(add); 21 | assertEquals(map(I.wrap(1)), I.wrap(2)); 22 | }); 23 | 24 | Deno.test("Identity flatmap", () => { 25 | const flatmap = I.flatmap((n: number) => I.wrap(n + 1)); 26 | assertEquals(flatmap(I.wrap(1)), I.wrap(2)); 27 | }); 28 | -------------------------------------------------------------------------------- /testing/initializable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import * as M from "../initializable.ts"; 4 | import * as N from "../number.ts"; 5 | import * as S from "../string.ts"; 6 | import { pipe } from "../fn.ts"; 7 | 8 | Deno.test("Initializable constant", () => { 9 | const { combine, init } = M.constant(0); 10 | assertEquals(init(), 0); 11 | assertEquals(pipe(1, combine(2)), 0); 12 | }); 13 | 14 | Deno.test("Initializable tuple", () => { 15 | const monoid = M.tuple( 16 | N.InitializableNumberSum, 17 | N.InitializableNumberProduct, 18 | ); 19 | const combine = M.getCombineAll(monoid); 20 | 21 | assertEquals(combine([1, 2], [3, 4]), [4, 8]); 22 | assertEquals(combine([1, 2]), [1, 2]); 23 | assertEquals(combine([1, 0], [1, 100], [1, -1]), [3, 0]); 24 | }); 25 | 26 | Deno.test("Initializable dual", () => { 27 | const reverseAll = pipe( 28 | S.InitializableString, 29 | M.intercalcate(" "), 30 | M.dual, 31 | M.getCombineAll, 32 | ); 33 | 34 | assertEquals(reverseAll("Hello", "World"), "World Hello "); 35 | }); 36 | 37 | Deno.test("Initializable intercalcate", () => { 38 | const { combine: toList } = pipe( 39 | S.InitializableString, 40 | M.intercalcate(", "), 41 | ); 42 | 43 | assertEquals( 44 | pipe( 45 | "apples", 46 | toList("oranges"), 47 | toList("and bananas"), 48 | ), 49 | "apples, oranges, and bananas", 50 | ); 51 | }); 52 | 53 | Deno.test("Initializable struct", () => { 54 | const monoid = M.struct({ 55 | sum: N.InitializableNumberSum, 56 | mult: N.InitializableNumberProduct, 57 | }); 58 | const combine = M.getCombineAll(monoid); 59 | 60 | assertEquals(combine({ sum: 1, mult: 2 }, { sum: 3, mult: 4 }), { 61 | sum: 4, 62 | mult: 8, 63 | }); 64 | assertEquals(combine(), { sum: 0, mult: 1 }); 65 | assertEquals(combine({ sum: 1, mult: 2 }), { sum: 1, mult: 2 }); 66 | }); 67 | 68 | Deno.test("Initializable getCombineAll", () => { 69 | const fold = M.getCombineAll(N.InitializableNumberSum); 70 | 71 | assertEquals(fold(), N.InitializableNumberSum.init()); 72 | assertEquals(fold(1, 2, 3), 6); 73 | }); 74 | -------------------------------------------------------------------------------- /testing/iterable.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 5 | 6 | import * as I from "../iterable.ts"; 7 | import * as O from "../option.ts"; 8 | import * as E from "../either.ts"; 9 | import * as P from "../pair.ts"; 10 | import * as N from "../number.ts"; 11 | import { pipe } from "../fn.ts"; 12 | 13 | Deno.test("Iterable iterable", () => { 14 | const iterable = I.iterable(function* () { 15 | yield 1; 16 | }); 17 | assertEquals(I.collect(iterable), [1]); 18 | }); 19 | 20 | Deno.test("Iterable clone", () => { 21 | const iterable = I.range(5, 1); 22 | const cloned = I.clone(iterable); 23 | assertEquals(I.collect(cloned), [1, 2, 3, 4, 5]); 24 | assertEquals(I.collect(cloned), [1, 2, 3, 4, 5]); 25 | }); 26 | 27 | Deno.test("Iterable range", () => { 28 | assertEquals(pipe(I.range(3), I.collect), [0, 1, 2]); 29 | assertEquals(pipe(I.range(), I.take(3), I.collect), [0, 1, 2]); 30 | assertEquals(pipe(I.range(3, 1), I.collect), [1, 2, 3]); 31 | assertEquals(pipe(I.range(3, 1, 2), I.collect), [1, 3, 5]); 32 | }); 33 | 34 | Deno.test("Iterable wrap", () => { 35 | assertEquals(pipe(I.wrap(1), I.collect), [1]); 36 | }); 37 | 38 | Deno.test("Iterable apply", () => { 39 | assertEquals( 40 | pipe( 41 | I.wrap((n: number) => n + 1), 42 | I.apply(I.range(3)), 43 | I.collect, 44 | ), 45 | [1, 2, 3], 46 | ); 47 | }); 48 | 49 | Deno.test("Iterable map", () => { 50 | assertEquals( 51 | pipe( 52 | I.range(3), 53 | I.map((n) => n + 1), 54 | I.collect, 55 | ), 56 | [1, 2, 3], 57 | ); 58 | }); 59 | 60 | Deno.test("Iterable flatmap", () => { 61 | assertEquals( 62 | pipe( 63 | I.range(3), 64 | I.flatmap((n) => I.range(3, n)), 65 | I.collect, 66 | ), 67 | [0, 1, 2, 1, 2, 3, 2, 3, 4], 68 | ); 69 | }); 70 | 71 | Deno.test("Iterable forEach", () => { 72 | const results: number[] = []; 73 | pipe(I.range(3), I.forEach((n) => results.push(n))); 74 | assertEquals(results, [0, 1, 2]); 75 | }); 76 | 77 | Deno.test("Iterable fold", () => { 78 | assertEquals( 79 | pipe( 80 | I.range(3), 81 | I.fold((n, m) => n + m, 0), 82 | ), 83 | 3, 84 | ); 85 | }); 86 | 87 | Deno.test("Iterable scan", () => { 88 | assertEquals( 89 | pipe( 90 | I.range(5), 91 | I.scan((sum, value) => sum + value, 0), 92 | I.collect, 93 | ), 94 | [0, 1, 3, 6, 10], 95 | ); 96 | }); 97 | 98 | Deno.test("Iterable filter", () => { 99 | assertEquals( 100 | pipe( 101 | I.range(3), 102 | I.filter((n) => n % 2 === 0), 103 | I.collect, 104 | ), 105 | [0, 2], 106 | ); 107 | }); 108 | 109 | Deno.test("Iterable filterMap", () => { 110 | assertEquals( 111 | pipe( 112 | I.range(3), 113 | I.filterMap(O.fromPredicate((n) => n % 2 === 0)), 114 | I.collect, 115 | ), 116 | [0, 2], 117 | ); 118 | }); 119 | 120 | Deno.test("Iterable partition", () => { 121 | assertEquals( 122 | pipe( 123 | I.range(3), 124 | I.partition((n) => n % 2 === 0), 125 | P.bimap(I.collect, I.collect), 126 | ), 127 | P.pair([0, 2], [1]), 128 | ); 129 | }); 130 | 131 | Deno.test("Iterable partition", () => { 132 | assertEquals( 133 | pipe( 134 | I.range(3), 135 | I.partition((n) => n % 2 === 0), 136 | P.bimap(I.collect, I.collect), 137 | ), 138 | P.pair([0, 2], [1]), 139 | ); 140 | }); 141 | 142 | Deno.test("Iterable partitionMap", () => { 143 | assertEquals( 144 | pipe( 145 | I.range(3), 146 | I.partitionMap(E.fromPredicate((n) => n % 2 === 0)), 147 | P.bimap(I.collect, I.collect), 148 | ), 149 | P.pair([0, 2], [1]), 150 | ); 151 | }); 152 | 153 | Deno.test("Iterable collect", () => { 154 | assertEquals( 155 | pipe( 156 | I.range(3), 157 | I.collect, 158 | ), 159 | [0, 1, 2], 160 | ); 161 | }); 162 | 163 | Deno.test("Iterable take", () => { 164 | assertEquals( 165 | pipe( 166 | I.range(Number.POSITIVE_INFINITY), 167 | I.take(3), 168 | I.collect, 169 | ), 170 | [0, 1, 2], 171 | ); 172 | }); 173 | 174 | Deno.test("Iterable takeUntil", () => { 175 | assertEquals( 176 | pipe( 177 | I.range(Number.POSITIVE_INFINITY), 178 | I.takeUntil((n) => n > 2), 179 | I.collect, 180 | ), 181 | [0, 1, 2], 182 | ); 183 | }); 184 | 185 | Deno.test("Iterable takeWhile", () => { 186 | assertEquals( 187 | pipe( 188 | I.range(Number.POSITIVE_INFINITY), 189 | I.takeWhile((n) => n < 3), 190 | I.collect, 191 | ), 192 | [0, 1, 2], 193 | ); 194 | }); 195 | 196 | Deno.test("Iterable tap", () => { 197 | const result: number[] = []; 198 | pipe( 199 | I.range(3), 200 | I.tap((n) => result.push(n)), 201 | I.collect, 202 | ); 203 | assertEquals(result, [0, 1, 2]); 204 | }); 205 | 206 | Deno.test("Iterable repeat", () => { 207 | assertEquals( 208 | pipe( 209 | I.range(3), 210 | I.repeat(3), 211 | I.collect, 212 | ), 213 | [0, 1, 2, 0, 1, 2, 0, 1, 2], 214 | ); 215 | }); 216 | 217 | Deno.test("Iterable init", () => { 218 | assertEquals( 219 | pipe( 220 | I.init(), 221 | I.collect, 222 | ), 223 | [], 224 | ); 225 | }); 226 | 227 | Deno.test("Iterable combine", () => { 228 | assertEquals(pipe(I.range(2), I.combine(I.range(2, 2)), I.collect), [ 229 | 0, 230 | 1, 231 | 2, 232 | 3, 233 | ]); 234 | }); 235 | 236 | Deno.test("Iterable getCombinable", () => { 237 | const { combine } = I.getCombinable(); 238 | assertStrictEquals(combine, I.combine); 239 | }); 240 | 241 | Deno.test("Iterable getInitializable", () => { 242 | const { init, combine } = I.getInitializable(); 243 | assertStrictEquals(combine, I.combine); 244 | assertStrictEquals(init, I.init); 245 | }); 246 | 247 | Deno.test("Iterable getShowable", () => { 248 | const { show } = I.getShowable(N.ShowableNumber); 249 | assertEquals(pipe(I.range(2), show), "Iterable[0, 1]"); 250 | }); 251 | -------------------------------------------------------------------------------- /testing/mappable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as M from "../mappable.ts"; 4 | import * as O from "../option.ts"; 5 | import { pipe } from "../fn.ts"; 6 | 7 | Deno.test("Mappable createBindTo", () => { 8 | const bindTo = M.createBindTo(O.MappableOption); 9 | 10 | const resultSome = pipe( 11 | O.some(1), 12 | bindTo("hello"), 13 | ); 14 | const resultNone = pipe( 15 | O.constNone(), 16 | bindTo("hello"), 17 | ); 18 | 19 | assertEquals(resultSome, O.some({ "hello": 1 })); 20 | assertEquals(resultNone, O.none); 21 | }); 22 | -------------------------------------------------------------------------------- /testing/newtype.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import * as N from "../newtype.ts"; 4 | import * as O from "../sortable.ts"; 5 | import * as Num from "../number.ts"; 6 | import { none, some } from "../option.ts"; 7 | 8 | type Real = N.Newtype<"Real", number>; 9 | const isoReal = N.iso(); 10 | 11 | Deno.test("Newtype getSetoid", () => { 12 | const { compare } = N.getComparable(Num.ComparableNumber); 13 | const int1 = isoReal.view(1); 14 | const int2 = isoReal.view(2); 15 | 16 | assertEquals(compare(int1)(int1), true); 17 | assertEquals(compare(int1)(int2), false); 18 | }); 19 | 20 | Deno.test("Newtype getSortable", () => { 21 | const ord = N.getSortable(Num.SortableNumber); 22 | const lte = O.lte(ord); 23 | const int1 = isoReal.view(1); 24 | const int2 = isoReal.view(2); 25 | 26 | assertEquals(lte(int1)(int1), true); 27 | assertEquals(lte(int1)(int2), false); 28 | assertEquals(lte(int2)(int1), true); 29 | }); 30 | 31 | Deno.test("Newtype getInitializable", () => { 32 | const { combine, init } = N.getInitializable( 33 | Num.InitializableNumberSum, 34 | ); 35 | const int0 = isoReal.view(0); 36 | const int1 = isoReal.view(1); 37 | const int2 = isoReal.view(2); 38 | 39 | assertEquals(combine(int1)(int1), int2); 40 | assertEquals(init(), int0); 41 | }); 42 | 43 | Deno.test("Newtype getCombinable", () => { 44 | const { combine } = N.getCombinable( 45 | Num.InitializableNumberSum, 46 | ); 47 | const int1 = isoReal.view(1); 48 | const int2 = isoReal.view(2); 49 | 50 | assertEquals(combine(int1)(int1), int2); 51 | }); 52 | 53 | Deno.test("Newtype iso", () => { 54 | const real = isoReal.view(1); 55 | assertEquals(real, 1 as unknown); 56 | assertEquals(isoReal.review(real), 1); 57 | }); 58 | 59 | Deno.test("Newtype prism", () => { 60 | type Integer = N.Newtype<"Integer", number>; 61 | const prism = N.prism(Number.isInteger); 62 | const int = 1 as unknown as Integer; // Cheating 63 | assertEquals(prism.view(1), some(int)); 64 | assertEquals(prism.view(1.1), none); 65 | assertEquals(prism.review(int), 1); 66 | }); 67 | -------------------------------------------------------------------------------- /testing/number.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as N from "../number.ts"; 4 | import { pipe } from "../fn.ts"; 5 | 6 | Deno.test("Number compare", () => { 7 | assertEquals(pipe(1, N.compare(1)), true); 8 | assertEquals(pipe(1, N.compare(2)), false); 9 | }); 10 | 11 | Deno.test("Number lte", () => { 12 | assertEquals(pipe(1, N.lte(1)), true); 13 | assertEquals(pipe(1, N.lte(2)), true); 14 | assertEquals(pipe(2, N.lte(1)), false); 15 | }); 16 | 17 | Deno.test("Number multiply", () => { 18 | assertEquals(pipe(1, N.multiply(1)), 1); 19 | assertEquals(pipe(1, N.multiply(2)), 2); 20 | assertEquals(pipe(2, N.multiply(1)), 2); 21 | assertEquals(pipe(2, N.multiply(2)), 4); 22 | }); 23 | 24 | Deno.test("Number add", () => { 25 | assertEquals(pipe(1, N.add(1)), 2); 26 | assertEquals(pipe(1, N.add(2)), 3); 27 | assertEquals(pipe(2, N.add(1)), 3); 28 | assertEquals(pipe(2, N.add(2)), 4); 29 | }); 30 | 31 | Deno.test("Number mod", () => { 32 | assertEquals(pipe(1, N.mod(1)), 0); 33 | assertEquals(pipe(1, N.mod(2)), 1); 34 | assertEquals(pipe(2, N.mod(1)), 0); 35 | assertEquals(pipe(2, N.mod(2)), 0); 36 | }); 37 | 38 | Deno.test("Number divides", () => { 39 | assertEquals(pipe(1, N.divides(2)), true); 40 | assertEquals(pipe(2, N.divides(3)), false); 41 | assertEquals(pipe(1, N.divides(1)), true); 42 | assertEquals(pipe(10, N.divides(100)), true); 43 | assertEquals(pipe(0, N.divides(10)), false); 44 | }); 45 | 46 | Deno.test("Number compare", () => { 47 | assertEquals(N.sort(1, 1), 0); 48 | assertEquals(N.sort(2, 1), 1); 49 | assertEquals(N.sort(1, 2), -1); 50 | }); 51 | 52 | Deno.test("Number initZero", () => { 53 | assertEquals(N.initZero(), 0); 54 | }); 55 | 56 | Deno.test("Number initOne", () => { 57 | assertEquals(N.initOne(), 1); 58 | }); 59 | 60 | Deno.test("Number initPosInf", () => { 61 | assertEquals(N.initPosInf(), Number.POSITIVE_INFINITY); 62 | }); 63 | 64 | Deno.test("Number initNegInf", () => { 65 | assertEquals(N.initNegInf(), Number.NEGATIVE_INFINITY); 66 | }); 67 | -------------------------------------------------------------------------------- /testing/pair.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 5 | 6 | import * as P from "../pair.ts"; 7 | import * as N from "../number.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | Deno.test("Pair pair", () => { 11 | assertEquals(P.pair(1, 2), [1, 2]); 12 | }); 13 | 14 | Deno.test("Pair dup", () => { 15 | assertEquals(P.dup(1), [1, 1]); 16 | }); 17 | 18 | Deno.test("Pair merge", () => { 19 | assertEquals( 20 | pipe( 21 | P.pair((n: number) => n + 1, 1), 22 | P.merge, 23 | ), 24 | 2, 25 | ); 26 | }); 27 | 28 | Deno.test("Pair mergeSecond", () => { 29 | assertEquals( 30 | pipe( 31 | P.pair(1, (n: number) => n + 1), 32 | P.mergeSecond, 33 | ), 34 | 2, 35 | ); 36 | }); 37 | 38 | Deno.test("Pair getFirst", () => { 39 | assertEquals(P.getFirst(P.pair(1, 2)), 1); 40 | }); 41 | 42 | Deno.test("Pair getSecond", () => { 43 | assertEquals(P.getSecond(P.pair(1, 2)), 2); 44 | }); 45 | 46 | Deno.test("Pair first", () => { 47 | assertEquals(pipe(1, P.first(2)), [2, 1]); 48 | }); 49 | 50 | Deno.test("Pair second", () => { 51 | assertEquals(pipe(1, P.second(2)), [1, 2]); 52 | }); 53 | 54 | Deno.test("Pair swap", () => { 55 | assertEquals(P.swap(P.pair(1, 2)), [2, 1]); 56 | }); 57 | 58 | Deno.test("Pair map", () => { 59 | assertEquals(pipe(P.pair(1, 2), P.map((n) => n + 1)), [2, 2]); 60 | }); 61 | 62 | Deno.test("Pair mapSecond", () => { 63 | assertEquals(pipe(P.pair(1, 2), P.mapSecond((n) => n + 1)), [1, 3]); 64 | }); 65 | 66 | Deno.test("Pair bimap", () => { 67 | assertEquals(pipe(P.pair(1, 2), P.bimap((n) => n + 1, (n) => n + 1)), [2, 3]); 68 | }); 69 | 70 | Deno.test("Pair unwrap", () => { 71 | assertEquals(P.unwrap(P.pair(1, 2)), 1); 72 | }); 73 | 74 | Deno.test("Pair fold", () => { 75 | assertEquals( 76 | pipe(P.pair(10, 20), P.fold(Math.max, Number.NEGATIVE_INFINITY)), 77 | 20, 78 | ); 79 | }); 80 | 81 | Deno.test("Pair traverse", () => { 82 | const M = P.getRightFlatmappable(N.InitializableNumberSum); 83 | const traversePair = P.traverse(M); 84 | assertEquals( 85 | pipe( 86 | P.pair("Brandon", "Blaylock"), 87 | traversePair((name) => P.pair(name, 37)), 88 | ), 89 | [["Brandon", "Blaylock"], 37], 90 | ); 91 | }); 92 | 93 | Deno.test("Pair MappablePair", () => { 94 | assertStrictEquals(P.MappablePair.map, P.map); 95 | }); 96 | 97 | Deno.test("Pair BimappablePair", () => { 98 | assertStrictEquals(P.BimappablePair.map, P.map); 99 | assertStrictEquals(P.BimappablePair.mapSecond, P.mapSecond); 100 | }); 101 | 102 | Deno.test("Pair FoldablePair", () => { 103 | assertStrictEquals(P.FoldablePair.fold, P.fold); 104 | }); 105 | 106 | Deno.test("Pair TraversablePair", () => { 107 | assertStrictEquals(P.TraversablePair.fold, P.fold); 108 | assertStrictEquals(P.TraversablePair.traverse, P.traverse); 109 | assertStrictEquals(P.TraversablePair.map, P.map); 110 | }); 111 | 112 | Deno.test("Pair getRightFlatmappable", () => { 113 | // TODO Work on generic Flatmappable tests 114 | const { apply, flatmap, map, wrap } = P.getRightFlatmappable( 115 | N.InitializableNumberSum, 116 | ); 117 | 118 | assertEquals(wrap(1), [1, N.InitializableNumberSum.init()]); 119 | assertEquals( 120 | pipe( 121 | P.pair((s: string) => s.toUpperCase(), 5), 122 | apply(P.pair("brandon", 10)), 123 | ), 124 | [ 125 | "BRANDON", 126 | 15, 127 | ], 128 | ); 129 | assertEquals(pipe(P.pair("brandon", 10), map((s) => s.toUpperCase())), [ 130 | "BRANDON", 131 | 10, 132 | ]); 133 | assertEquals( 134 | pipe( 135 | P.pair("brandon", 10), 136 | flatmap((name) => P.pair(name.toUpperCase(), -5)), 137 | ), 138 | ["BRANDON", 5], 139 | ); 140 | }); 141 | 142 | Deno.test("Pair getShowablePair", () => { 143 | const { show } = P.getShowablePair(N.ShowableNumber, N.ShowableNumber); 144 | assertEquals(show(P.pair(1, 2)), "Pair(1, 2)"); 145 | }); 146 | 147 | Deno.test("Pair getCombinablePair", () => { 148 | const { combine } = P.getCombinablePair( 149 | N.CombinableNumberMax, 150 | N.CombinableNumberMin, 151 | ); 152 | assertEquals(combine(P.pair(1, 1))(P.pair(2, 2)), P.pair(2, 1)); 153 | assertEquals(combine(P.pair(2, 2))(P.pair(1, 1)), P.pair(2, 1)); 154 | }); 155 | 156 | Deno.test("Pair getInitializablePair", () => { 157 | const { init, combine } = P.getInitializablePair( 158 | N.InitializableNumberMax, 159 | N.InitializableNumberMin, 160 | ); 161 | assertEquals( 162 | init(), 163 | P.pair(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY), 164 | ); 165 | assertEquals(combine(P.pair(1, 1))(P.pair(2, 2)), P.pair(2, 1)); 166 | assertEquals(combine(P.pair(2, 2))(P.pair(1, 1)), P.pair(2, 1)); 167 | }); 168 | 169 | Deno.test("Pair getComparablePair", () => { 170 | const { compare } = P.getComparablePair( 171 | N.ComparableNumber, 172 | N.ComparableNumber, 173 | ); 174 | assertEquals(compare(P.pair(1, 1))(P.pair(1, 1)), true); 175 | assertEquals(compare(P.pair(1, 1))(P.pair(2, 2)), false); 176 | assertEquals(compare(P.pair(2, 2))(P.pair(1, 1)), false); 177 | assertEquals(compare(P.pair(1, 1))(P.pair(1, 2)), false); 178 | assertEquals(compare(P.pair(1, 2))(P.pair(1, 1)), false); 179 | assertEquals(compare(P.pair(2, 1))(P.pair(1, 1)), false); 180 | assertEquals(compare(P.pair(1, 1))(P.pair(2, 1)), false); 181 | }); 182 | 183 | Deno.test("Pair getSortablePair", () => { 184 | const { sort } = P.getSortablePair(N.SortableNumber, N.SortableNumber); 185 | assertEquals(sort(P.pair(0, 0), P.pair(0, 0)), 0); 186 | assertEquals(sort(P.pair(0, 0), P.pair(0, 1)), -1); 187 | assertEquals(sort(P.pair(0, 1), P.pair(0, 0)), 1); 188 | assertEquals(sort(P.pair(0, 0), P.pair(1, 0)), -1); 189 | assertEquals(sort(P.pair(1, 0), P.pair(0, 0)), 1); 190 | }); 191 | -------------------------------------------------------------------------------- /testing/predicate.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 5 | 6 | import * as P from "../predicate.ts"; 7 | import * as R from "../refinement.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | Deno.test("Predicate premap", () => { 11 | const isGreaterThan3 = (n: number) => n > 3; 12 | const isLongerThan3 = pipe( 13 | isGreaterThan3, 14 | P.premap((s: string) => s.length), 15 | ); 16 | 17 | assertEquals(isLongerThan3("Hello"), true); 18 | assertEquals(isLongerThan3("Hi"), false); 19 | }); 20 | 21 | Deno.test("Predicate not", () => { 22 | const isGreaterThan3 = (n: number) => n > 3; 23 | const isLongerThan3 = pipe( 24 | isGreaterThan3, 25 | P.premap((s: string) => s.length), 26 | ); 27 | const isNotLongerThan3 = P.not(isLongerThan3); 28 | 29 | assertEquals(isNotLongerThan3("Hello"), false); 30 | assertEquals(isNotLongerThan3("Hi"), true); 31 | }); 32 | 33 | Deno.test("Predicate or", () => { 34 | const stringOrNumber = pipe( 35 | R.string, 36 | P.or(R.number), 37 | ); 38 | 39 | assertEquals(stringOrNumber("Hello"), true); 40 | assertEquals(stringOrNumber(1), true); 41 | assertEquals(stringOrNumber({}), false); 42 | }); 43 | 44 | Deno.test("Predicate and", () => { 45 | const isPositive = (n: number) => n > 0; 46 | const isInteger = (n: number) => Number.isInteger(n); 47 | 48 | const isPositiveInteger = pipe( 49 | isPositive, 50 | P.and(isInteger), 51 | ); 52 | 53 | assertEquals(isPositiveInteger(0), false); 54 | assertEquals(isPositiveInteger(1), true); 55 | assertEquals(isPositiveInteger(2.2), false); 56 | assertEquals(isPositiveInteger(-2.2), false); 57 | }); 58 | 59 | Deno.test("Predicate getCombinableAny", () => { 60 | assertStrictEquals(P.getCombinableAny().combine, P.or); 61 | }); 62 | 63 | Deno.test("Predicate getCombinableAll", () => { 64 | assertStrictEquals(P.getCombinableAll().combine, P.and); 65 | }); 66 | 67 | Deno.test("Predicate getInitializableAny", () => { 68 | assertStrictEquals(P.getInitializableAny().combine, P.or); 69 | assertEquals(P.getInitializableAny().init()(0), false); 70 | }); 71 | 72 | Deno.test("Predicate getInitializableAll", () => { 73 | assertStrictEquals(P.getInitializableAll().combine, P.and); 74 | assertEquals(P.getInitializableAll().init()(0), true); 75 | }); 76 | -------------------------------------------------------------------------------- /testing/promise.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 5 | 6 | import * as P from "../promise.ts"; 7 | import * as E from "../either.ts"; 8 | import * as N from "../number.ts"; 9 | import { pipe, todo } from "../fn.ts"; 10 | 11 | Deno.test("Promise deferred", async () => { 12 | const promise = P.deferred(); 13 | setTimeout(() => promise.resolve(1), 100); 14 | 15 | assertEquals(await promise, 1); 16 | }); 17 | 18 | Deno.test({ 19 | name: "Promise abortable aborted", 20 | async fn() { 21 | const controller = new AbortController(); 22 | const slow = P.wait(100).then(() => 1); 23 | const wrapped = pipe( 24 | slow, 25 | P.abortable(controller.signal, (msg) => msg), 26 | ); 27 | setTimeout(() => controller.abort("Hi"), 50); 28 | 29 | assertEquals(await wrapped, E.left("Hi")); 30 | }, 31 | sanitizeOps: false, 32 | sanitizeResources: false, 33 | }); 34 | 35 | Deno.test({ 36 | name: "Promise abortable early", 37 | async fn() { 38 | const controller = new AbortController(); 39 | controller.abort("Hi"); 40 | const slow = P.wait(100).then(() => 1); 41 | const wrapped = pipe( 42 | slow, 43 | P.abortable(controller.signal, (msg) => msg), 44 | ); 45 | 46 | assertEquals(await wrapped, E.left("Hi")); 47 | }, 48 | sanitizeOps: false, 49 | sanitizeResources: false, 50 | }); 51 | 52 | Deno.test({ 53 | name: "Promise abortable ok", 54 | async fn() { 55 | const controller = new AbortController(); 56 | const slow = P.wait(100).then(() => 1); 57 | const wrapped = pipe( 58 | slow, 59 | P.abortable(controller.signal, (msg) => msg), 60 | ); 61 | 62 | assertEquals(await wrapped, E.right(1)); 63 | }, 64 | sanitizeOps: false, 65 | sanitizeResources: false, 66 | }); 67 | 68 | Deno.test("Promise wait", async () => { 69 | const start = Date.now(); 70 | const waiter = P.wait(200); 71 | await waiter; 72 | const end = Date.now(); 73 | assertEquals(end - start >= 100, true); 74 | 75 | { 76 | using _ = P.wait(100); 77 | } 78 | }); 79 | 80 | Deno.test("Promise delay", async () => { 81 | const start = Date.now(); 82 | const waiter = pipe(P.wrap(1), P.delay(200)); 83 | await waiter; 84 | const end = Date.now(); 85 | assertEquals(end - start >= 100, true); 86 | assertEquals(await waiter, 1); 87 | }); 88 | 89 | Deno.test("Promise resolve", async () => { 90 | assertEquals(await P.resolve(1), 1); 91 | }); 92 | 93 | Deno.test("Promise reject", async () => { 94 | assertEquals(await pipe(P.reject(1), P.catchError(() => 2)), 2); 95 | }); 96 | 97 | Deno.test("Promise then", async () => { 98 | assertEquals(await pipe(P.wrap(1), P.then((n) => n + 1)), 2); 99 | assertEquals(await pipe(P.wrap(1), P.then((n) => P.wrap(n + 1))), 2); 100 | }); 101 | 102 | Deno.test("Promise catchError", async () => { 103 | assertEquals(await pipe(P.reject(1), P.catchError(() => 2)), 2); 104 | }); 105 | 106 | Deno.test("Promise all", async () => { 107 | assertEquals(await P.all(P.wrap(1), P.wrap("Hello")), [1, "Hello"]); 108 | assertEquals( 109 | await pipe( 110 | P.all(P.wrap(1), P.reject("Hello")), 111 | P.catchError(() => [0, "Goodbye"]), 112 | ), 113 | [0, "Goodbye"], 114 | ); 115 | }); 116 | 117 | Deno.test({ 118 | name: "Promise race", 119 | async fn() { 120 | const one = pipe(P.wait(200), P.map(() => "one")); 121 | const two = pipe(P.wait(300), P.map(() => "two")); 122 | assertEquals(await P.race(one, two), "one"); 123 | }, 124 | sanitizeOps: false, 125 | sanitizeResources: false, 126 | }); 127 | 128 | Deno.test("Promise of", async () => { 129 | assertEquals(await P.wrap(1), 1); 130 | }); 131 | 132 | Deno.test("Promise ap", async () => { 133 | assertEquals( 134 | await pipe( 135 | P.wrap((n: number) => n + 1), 136 | P.apply(P.wrap(1)), 137 | ), 138 | 2, 139 | ); 140 | }); 141 | 142 | Deno.test("Promise map", async () => { 143 | assertEquals(await pipe(P.wrap(1), P.map((n) => n + 1)), 2); 144 | }); 145 | 146 | Deno.test("Promise flatmap", async () => { 147 | assertEquals(await pipe(P.wrap(1), P.flatmap((n) => P.wrap(n + 1))), 2); 148 | }); 149 | 150 | Deno.test("Promise fail", async () => { 151 | assertEquals(await pipe(P.fail(1), P.catchError(P.wrap)), await P.wrap(1)); 152 | }); 153 | 154 | Deno.test("Promise tryCatch", async () => { 155 | const add = (n: number) => n + 1; 156 | const throwSync = (_: number): number => todo(); 157 | const throwAsync = (_: number): Promise => P.reject("Ha!"); 158 | 159 | const catchAdd = P.tryCatch(add, () => -1); 160 | const catchSync = P.tryCatch(throwSync, () => -1); 161 | const catchAsync = P.tryCatch(throwAsync, () => -1); 162 | 163 | assertEquals(await catchAdd(1), 2); 164 | assertEquals(await catchSync(1), -1); 165 | assertEquals(await catchAsync(1), -1); 166 | }); 167 | 168 | Deno.test("Promise getCombinablePromise", async () => { 169 | const { combine } = P.getCombinablePromise(N.CombinableNumberSum); 170 | assertEquals(await combine(P.wrap(1))(P.wrap(2)), await P.wrap(3)); 171 | }); 172 | 173 | Deno.test("Promise getInitializablePromise", async () => { 174 | const { combine, init } = P.getInitializablePromise(N.InitializableNumberSum); 175 | assertEquals(await init(), await P.wrap(0)); 176 | assertEquals(await combine(P.wrap(1))(P.wrap(2)), await P.wrap(3)); 177 | }); 178 | 179 | Deno.test("Promise MappablePromise", () => { 180 | assertStrictEquals(P.MappablePromise.map, P.map); 181 | }); 182 | 183 | Deno.test("Promise ApplicablePromise", () => { 184 | assertStrictEquals(P.ApplicablePromise.apply, P.apply); 185 | assertStrictEquals(P.ApplicablePromise.map, P.map); 186 | assertStrictEquals(P.ApplicablePromise.wrap, P.wrap); 187 | }); 188 | 189 | Deno.test("Promise FlatmappablePromise", () => { 190 | assertStrictEquals(P.FlatmappablePromise.wrap, P.wrap); 191 | assertStrictEquals(P.FlatmappablePromise.apply, P.apply); 192 | assertStrictEquals(P.FlatmappablePromise.map, P.map); 193 | assertStrictEquals(P.FlatmappablePromise.flatmap, P.flatmap); 194 | }); 195 | -------------------------------------------------------------------------------- /testing/refinement.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import * as R from "../refinement.ts"; 4 | import * as O from "../option.ts"; 5 | import * as E from "../either.ts"; 6 | import { pipe } from "../fn.ts"; 7 | 8 | Deno.test("Refinement fromOption", () => { 9 | const refine = R.fromOption((u: unknown) => 10 | typeof u === "number" ? O.some(u) : O.none 11 | ); 12 | assertEquals(refine(null), false); 13 | assertEquals(refine(1), true); 14 | }); 15 | 16 | Deno.test("Refinement fromEither", () => { 17 | const refine = R.fromEither((u: unknown) => 18 | typeof u === "number" ? E.right(u) : E.left(u) 19 | ); 20 | assertEquals(refine(0), true); 21 | assertEquals(refine(null), false); 22 | }); 23 | 24 | Deno.test("Refinement and", () => { 25 | const refine = pipe( 26 | R.struct({ two: R.boolean }), 27 | R.and(R.struct({ one: R.number })), 28 | ); 29 | assertEquals(refine({}), false); 30 | assertEquals(refine(1), false); 31 | assertEquals(refine({ two: true }), false); 32 | assertEquals(refine({ one: 1 }), false); 33 | assertEquals(refine({ one: 1, two: true }), true); 34 | }); 35 | 36 | Deno.test("Refinement or", () => { 37 | const refine = pipe( 38 | R.number, 39 | R.or(R.boolean), 40 | ); 41 | assertEquals(refine(null), false); 42 | assertEquals(refine("asdf"), false); 43 | assertEquals(refine(1), true); 44 | assertEquals(refine(true), true); 45 | }); 46 | 47 | Deno.test("Refinement id", () => { 48 | assertEquals(R.id()(1), true); 49 | }); 50 | 51 | Deno.test("Refinement compose", () => { 52 | const isBig = (s: string): s is "Big" => s === "Big"; 53 | const composed = pipe(R.string, R.compose(isBig)); 54 | assertEquals(composed(null), false); 55 | assertEquals(composed("Big"), true); 56 | }); 57 | 58 | Deno.test("Refinement unknown", () => { 59 | assertEquals(R.unknown(1), true); 60 | }); 61 | 62 | Deno.test("Refinement string", () => { 63 | assertEquals(R.string("asdf"), true); 64 | assertEquals(R.string(1), false); 65 | }); 66 | 67 | Deno.test("Refinement number", () => { 68 | assertEquals(R.number("asdf"), false); 69 | assertEquals(R.number(1), true); 70 | }); 71 | 72 | Deno.test("Refinement boolean", () => { 73 | assertEquals(R.boolean(true), true); 74 | assertEquals(R.boolean(false), true); 75 | assertEquals(R.boolean("asdf"), false); 76 | }); 77 | 78 | Deno.test("Refinement literal", () => { 79 | const literal = R.literal(1, 2, 3); 80 | assertEquals(literal(1), true); 81 | assertEquals(literal(2), true); 82 | assertEquals(literal(3), true); 83 | assertEquals(literal(4), false); 84 | }); 85 | 86 | Deno.test("Refinement undefinable", () => { 87 | const refine = R.undefinable(R.literal(1)); 88 | assertEquals(refine(1), true); 89 | assertEquals(refine(undefined), true); 90 | assertEquals(refine(null), false); 91 | }); 92 | 93 | Deno.test("Refinement nullable", () => { 94 | const refine = R.nullable(R.literal(1)); 95 | assertEquals(refine(1), true); 96 | assertEquals(refine(undefined), false); 97 | assertEquals(refine(null), true); 98 | }); 99 | 100 | Deno.test("Refinement record", () => { 101 | const refine = R.record(R.number); 102 | assertEquals(refine({}), true); 103 | assertEquals(refine({ "one": 1 }), true); 104 | assertEquals(refine({ "one": false }), false); 105 | assertEquals(refine(1), false); 106 | }); 107 | 108 | Deno.test("Refinement array", () => { 109 | const refine = R.array(R.boolean); 110 | assertEquals(refine(true), false); 111 | assertEquals(refine([]), true); 112 | assertEquals(refine([true]), true); 113 | assertEquals(refine(["asdf"]), false); 114 | }); 115 | 116 | Deno.test("Refinement tuple", () => { 117 | const refine = R.tuple(R.literal("Left", "Right"), R.number); 118 | assertEquals(refine(["Left", 1]), true); 119 | assertEquals(refine(["Right", 1]), true); 120 | assertEquals(refine(false), false); 121 | assertEquals(refine(["Left"]), false); 122 | }); 123 | 124 | Deno.test("Refinement struct", () => { 125 | const refine = R.struct({ one: R.number }); 126 | assertEquals(refine({}), false); 127 | assertEquals(refine(1), false); 128 | assertEquals(refine({ one: false }), false); 129 | assertEquals(refine({ one: 1 }), true); 130 | }); 131 | 132 | Deno.test("Refinement partial", () => { 133 | const refine = R.partial({ one: R.number }); 134 | assertEquals(refine({}), true); 135 | assertEquals(refine(1), false); 136 | assertEquals(refine({ one: false }), false); 137 | assertEquals(refine({ one: 1 }), true); 138 | }); 139 | 140 | Deno.test("Refinement intersect", () => { 141 | const refine = pipe( 142 | R.struct({ two: R.boolean }), 143 | R.intersect(R.struct({ one: R.number })), 144 | ); 145 | assertEquals(refine({}), false); 146 | assertEquals(refine(1), false); 147 | assertEquals(refine({ two: true }), false); 148 | assertEquals(refine({ one: 1 }), false); 149 | assertEquals(refine({ one: 1, two: true }), true); 150 | }); 151 | 152 | Deno.test("Refinement union", () => { 153 | const refine = pipe( 154 | R.number, 155 | R.union(R.boolean), 156 | ); 157 | assertEquals(refine(null), false); 158 | assertEquals(refine("asdf"), false); 159 | assertEquals(refine(1), true); 160 | assertEquals(refine(true), true); 161 | }); 162 | 163 | Deno.test("Refinement lazy", () => { 164 | const refine = R.lazy("One", () => R.number); 165 | assertEquals(refine(1), true); 166 | assertEquals(refine(true), false); 167 | }); 168 | -------------------------------------------------------------------------------- /testing/schemable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as S from "../schemable.ts"; 4 | import * as D from "../decoder.ts"; 5 | import * as R from "../refinement.ts"; 6 | import * as J from "../json_schema.ts"; 7 | import * as E from "../either.ts"; 8 | import { pipe } from "../fn.ts"; 9 | 10 | type Lazy = { 11 | foo: string; 12 | lazy: ReadonlyArray; 13 | }; 14 | 15 | const lazy = (): Lazy => ({ 16 | foo: "hello", 17 | lazy: [{ foo: "goodbye", lazy: [] }], 18 | }); 19 | 20 | type BigType = { 21 | unknown: unknown; 22 | string: string; 23 | number: number; 24 | boolean: boolean; 25 | literal: 1 | "hello"; 26 | nullable: number | null; 27 | undefinable: number | undefined; 28 | record: Readonly>; 29 | array: ReadonlyArray; 30 | tuple: [string, number]; 31 | union: string | number; 32 | partial?: number; 33 | }; 34 | 35 | const big = (): BigType => ({ 36 | unknown: undefined, 37 | string: "hello", 38 | number: 1, 39 | boolean: true, 40 | literal: 1, 41 | nullable: null, 42 | undefinable: undefined, 43 | record: { one: 1 }, 44 | array: [1], 45 | tuple: ["hello", 1], 46 | union: 1, 47 | }); 48 | 49 | Deno.test("Schemable schema", () => { 50 | const LazySchema: S.Schema = S.schema((s) => 51 | s.lazy( 52 | "Lazy", 53 | () => s.struct({ foo: s.string(), lazy: s.array(LazySchema(s)) }), 54 | ) 55 | ); 56 | 57 | const decodeLazy = LazySchema(D.SchemableDecoder); 58 | 59 | assertEquals(decodeLazy(lazy()), E.right(lazy())); 60 | 61 | const BigSchema: S.Schema = S.schema((s) => 62 | pipe( 63 | s.struct({ 64 | unknown: s.unknown(), 65 | string: s.string(), 66 | number: s.number(), 67 | boolean: s.boolean(), 68 | literal: s.literal(1, "hello"), 69 | nullable: s.nullable(s.number()), 70 | undefinable: s.undefinable(s.number()), 71 | record: s.record(s.number()), 72 | array: s.array(s.number()), 73 | tuple: s.tuple(s.string(), s.number()), 74 | union: pipe(s.string(), s.union(s.number())), 75 | }), 76 | s.intersect(s.partial({ 77 | partial: s.number(), 78 | })), 79 | ) 80 | ); 81 | 82 | const decode = BigSchema(D.SchemableDecoder); 83 | const refine = BigSchema(R.SchemableRefinement); 84 | 85 | // Shouldn't throw 86 | BigSchema(J.SchemableJsonBuilder); 87 | 88 | const shouldPass: unknown = big(); 89 | const shouldFail: unknown = Object.assign(big(), { number: "hello" }); 90 | 91 | assertEquals(decode(shouldPass), E.right(shouldPass)); 92 | assertEquals(E.isLeft(decode(shouldFail)), true); 93 | assertEquals(refine(shouldPass), true); 94 | assertEquals(refine(shouldFail), false); 95 | }); 96 | -------------------------------------------------------------------------------- /testing/showable.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as S from "../showable.ts"; 4 | import * as N from "../number.ts"; 5 | import * as St from "../string.ts"; 6 | 7 | Deno.test("Showable struct", () => { 8 | const struct1 = S.struct({ 9 | number: N.ShowableNumber, 10 | string: St.ShowableString, 11 | }); 12 | const struct2 = S.struct({}); 13 | 14 | assertEquals( 15 | struct1.show({ number: 1, string: "Hello" }), 16 | '{ number: 1, string: "Hello" }', 17 | ); 18 | assertEquals(struct2.show({}), "{}"); 19 | }); 20 | 21 | Deno.test("Showable tuple", () => { 22 | const tuple1 = S.tuple(N.ShowableNumber, St.ShowableString); 23 | const tuple2 = S.tuple(); 24 | 25 | assertEquals(tuple1.show([1, "Hello"]), '[1, "Hello"]'); 26 | assertEquals(tuple2.show([]), "[]"); 27 | }); 28 | -------------------------------------------------------------------------------- /testing/sortable.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std/testing/asserts.ts"; 5 | 6 | import * as O from "../sortable.ts"; 7 | import * as N from "../number.ts"; 8 | import * as S from "../string.ts"; 9 | import { pipe } from "../fn.ts"; 10 | 11 | Deno.test("Sortable sign", () => { 12 | assertEquals(O.sign(Number.NEGATIVE_INFINITY), -1); 13 | assertEquals(O.sign(Number.POSITIVE_INFINITY), 1); 14 | assertEquals(O.sign(0), 0); 15 | assertEquals(O.sign(-56), -1); 16 | assertEquals(O.sign(56), 1); 17 | assertEquals(O.sign(-0.34), -1); 18 | assertEquals(O.sign(0.45), 1); 19 | }); 20 | 21 | Deno.test("Sortable fromSort", () => { 22 | const { sort } = O.fromSort(N.sort); 23 | // sort 24 | assertEquals(sort(0, 0), 0); 25 | assertEquals(sort(0, 1), -1); 26 | assertEquals(sort(1, 0), 1); 27 | }); 28 | 29 | Deno.test("Sortable fromCurriedSort", () => { 30 | const { sort } = O.fromCurriedSort((second) => (first) => 31 | N.sort(first, second) 32 | ); 33 | // sort 34 | assertEquals(sort(0, 0), 0); 35 | assertEquals(sort(0, 1), -1); 36 | assertEquals(sort(1, 0), 1); 37 | }); 38 | 39 | Deno.test("Sortable lt", () => { 40 | const lt = O.lt(N.SortableNumber); 41 | assertEquals(lt(0)(0), false); 42 | assertEquals(lt(0)(1), false); 43 | assertEquals(lt(1)(0), true); 44 | }); 45 | 46 | Deno.test("Sortable gt", () => { 47 | const gt = O.gt(N.SortableNumber); 48 | assertEquals(gt(0)(0), false); 49 | assertEquals(gt(0)(1), true); 50 | assertEquals(gt(1)(0), false); 51 | }); 52 | 53 | Deno.test("Sortable lte", () => { 54 | const lte = O.lte(N.SortableNumber); 55 | assertEquals(lte(0)(0), true); 56 | assertEquals(lte(0)(1), false); 57 | assertEquals(lte(1)(0), true); 58 | }); 59 | 60 | Deno.test("Sortable gte", () => { 61 | const gte = O.gte(N.SortableNumber); 62 | assertEquals(gte(0)(0), true); 63 | assertEquals(gte(1)(0), false); 64 | assertEquals(gte(0)(1), true); 65 | }); 66 | 67 | Deno.test("Sortable min", () => { 68 | const min = O.min(N.SortableNumber); 69 | assertEquals(min(0)(0), 0); 70 | assertEquals(min(0)(1), 0); 71 | assertEquals(min(1)(0), 0); 72 | }); 73 | 74 | Deno.test("Sortable max", () => { 75 | const max = O.max(N.SortableNumber); 76 | assertEquals(max(0)(0), 0); 77 | assertEquals(max(0)(1), 1); 78 | assertEquals(max(1)(0), 1); 79 | }); 80 | 81 | Deno.test("Sortable clamp", () => { 82 | const clamp = O.clamp(N.SortableNumber); 83 | assertEquals(clamp(0, 2)(-1), 0); 84 | assertEquals(clamp(0, 2)(0), 0); 85 | assertEquals(clamp(0, 2)(1), 1); 86 | assertEquals(clamp(0, 2)(2), 2); 87 | assertEquals(clamp(0, 2)(3), 2); 88 | }); 89 | 90 | Deno.test("Sortable between", () => { 91 | const between = O.between(N.SortableNumber); 92 | assertEquals(between(0, 2)(-1), false); 93 | assertEquals(between(0, 2)(0), false); 94 | assertEquals(between(0, 2)(1), true); 95 | assertEquals(between(0, 10)(5), true); 96 | assertEquals(between(0, 2)(2), false); 97 | assertEquals(between(0, 2)(3), false); 98 | }); 99 | 100 | Deno.test("Sortable trivial", () => { 101 | // Every Date is the same! 102 | const trivial = O.trivial(); 103 | const now = new Date(); 104 | const later = new Date(Date.now() + 60 * 60 * 1000); 105 | 106 | assertEquals(trivial.sort(now, later), 0); 107 | assertEquals(trivial.sort(later, now), 0); 108 | assertEquals(trivial.sort(later, later), 0); 109 | assertEquals(trivial.sort(now, now), 0); 110 | }); 111 | 112 | Deno.test("Sortable reverse", () => { 113 | const reverse = O.reverse(N.SortableNumber); 114 | 115 | assertEquals(reverse.sort(0, 0), 0); 116 | assertEquals(reverse.sort(0, 1), 1); 117 | assertEquals(reverse.sort(1, 0), -1); 118 | }); 119 | 120 | Deno.test("Sortable tuple", () => { 121 | const tuple = O.tuple(N.SortableNumber, N.SortableNumber); 122 | 123 | assertEquals(tuple.sort([1, 1], [2, 1]), -1); 124 | assertEquals(tuple.sort([1, 1], [1, 2]), -1); 125 | assertEquals(tuple.sort([1, 1], [1, 1]), 0); 126 | assertEquals(tuple.sort([1, 1], [1, 0]), 1); 127 | assertEquals(tuple.sort([1, 1], [0, 1]), 1); 128 | }); 129 | 130 | Deno.test("Sortable struct", () => { 131 | const tuple = O.struct({ 132 | name: S.SortableString, 133 | age: N.SortableNumber, 134 | }); 135 | 136 | assertEquals( 137 | tuple.sort({ name: "Brandon", age: 37 }, { name: "Emily", age: 32 }), 138 | -1, 139 | ); 140 | assertEquals( 141 | tuple.sort({ name: "Brandon", age: 37 }, { name: "Brandon", age: 32 }), 142 | 1, 143 | ); 144 | assertEquals( 145 | tuple.sort({ name: "Brandon", age: 37 }, { name: "Emily", age: 0 }), 146 | -1, 147 | ); 148 | assertEquals( 149 | tuple.sort({ name: "Brandon", age: 37 }, { name: "Brandon", age: 37 }), 150 | 0, 151 | ); 152 | }); 153 | 154 | Deno.test("Sortable premap", () => { 155 | type Person = { name: string; age: number }; 156 | const byAge = pipe( 157 | N.SortableNumber, 158 | O.premap((p: Person) => p.age), 159 | ); 160 | 161 | assertEquals( 162 | byAge.sort({ name: "Brandon", age: 37 }, { name: "Emily", age: 32 }), 163 | 1, 164 | ); 165 | assertEquals( 166 | byAge.sort({ name: "Brandon", age: 37 }, { name: "Brandon", age: 37 }), 167 | 0, 168 | ); 169 | assertEquals( 170 | byAge.sort({ name: "Brandon", age: 37 }, { name: "Brian", age: 37 }), 171 | 0, 172 | ); 173 | assertEquals( 174 | byAge.sort({ name: "Brandon", age: 37 }, { name: "Patrick", age: 50 }), 175 | -1, 176 | ); 177 | }); 178 | -------------------------------------------------------------------------------- /testing/state.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 2 | 3 | import * as S from "../state.ts"; 4 | import * as N from "../number.ts"; 5 | import { pipe } from "../fn.ts"; 6 | 7 | const add = (n: number) => n + 1; 8 | 9 | const assertEqualsS = ( 10 | a: S.State, 11 | b: S.State, 12 | ) => assertEquals(a(1), b(1)); 13 | 14 | Deno.test("State id", () => { 15 | assertEquals(S.id()(0), [0, 0]); 16 | }); 17 | 18 | Deno.test("State put", () => { 19 | assertEquals(S.put(0)(-1), [undefined, 0]); 20 | }); 21 | 22 | Deno.test("State modify", () => { 23 | assertEqualsS(S.modify((n: number) => n + 1), S.put(2)); 24 | }); 25 | 26 | Deno.test("State gets", () => { 27 | assertEquals(S.gets((n: number) => n.toString())(0), ["0", 0]); 28 | }); 29 | 30 | Deno.test("State state", () => { 31 | assertEqualsS(S.state(1, 1), S.id()); 32 | }); 33 | 34 | Deno.test("State wrap", () => { 35 | assertEqualsS(S.wrap(1), S.id()); 36 | }); 37 | 38 | Deno.test("State apply", () => { 39 | }); 40 | 41 | Deno.test("State map", () => { 42 | assertEqualsS(pipe(S.id(), S.map(add)), S.state(2, 1)); 43 | }); 44 | 45 | Deno.test("State flatmap", () => { 46 | assertEqualsS( 47 | pipe(S.id(), S.flatmap((n) => S.gets((m) => n + m))), 48 | S.state(2, 1), 49 | ); 50 | }); 51 | 52 | Deno.test("State evaluate", () => { 53 | assertEquals(pipe(S.id(), S.evaluate(0)), 0); 54 | }); 55 | 56 | Deno.test("State execute", () => { 57 | assertEquals(pipe(S.id(), S.execute(0)), 0); 58 | }); 59 | 60 | Deno.test("State getCombinableState", () => { 61 | const { combine } = S.getCombinableState( 62 | N.CombinableNumberMax, 63 | N.CombinableNumberMin, 64 | ); 65 | assertEquals( 66 | combine((n: number) => [n, n + 1])((n: number) => [n + 100, n])(1), 67 | [1, 2], 68 | ); 69 | }); 70 | 71 | Deno.test("State getInitializableState", () => { 72 | const { combine, init } = S.getInitializableState( 73 | N.InitializableNumberSum, 74 | N.InitializableNumberProduct, 75 | ); 76 | assertEquals(init()(1), [1, 1]); 77 | assertEquals( 78 | combine((n: number) => [n, n])((n: number) => [n, n])(2), 79 | [4, 4], 80 | ); 81 | }); 82 | 83 | Deno.test("State Do, bind, bindTo", () => { 84 | assertEquals( 85 | pipe( 86 | S.wrap, number>({}), 87 | S.bind("one", () => S.state(1, 1)), 88 | S.bind("two", ({ one }) => S.state(one + one, 1)), 89 | S.map(({ one, two }) => one + two), 90 | )(1), 91 | S.state(3, 1)(1), 92 | ); 93 | assertEquals( 94 | pipe( 95 | S.state(1, 1), 96 | S.bindTo("one"), 97 | )(1), 98 | S.state({ one: 1 }, 1)(1), 99 | ); 100 | }); 101 | -------------------------------------------------------------------------------- /testing/string.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as S from "../string.ts"; 4 | import * as O from "../option.ts"; 5 | import { pipe } from "../fn.ts"; 6 | 7 | Deno.test("String compare", () => { 8 | const hello = "Hello"; 9 | const hi = "hi"; 10 | 11 | assertEquals(pipe(hello, S.compare(hi)), false); 12 | assertEquals(pipe(hi, S.compare(hi)), true); 13 | }); 14 | 15 | Deno.test("String combine", () => { 16 | assertEquals(pipe("Hello", S.combine("World")), "HelloWorld"); 17 | }); 18 | 19 | Deno.test("String init", () => { 20 | assertEquals(S.init(), ""); 21 | }); 22 | 23 | Deno.test("String sort", () => { 24 | assertEquals(S.sort("aa", "aa"), 0); 25 | assertEquals(S.sort("aa", "ab"), -1); 26 | assertEquals(S.sort("ab", "aa"), 1); 27 | assertEquals(S.sort("aa", "bb"), -1); 28 | assertEquals(S.sort("a", "aa"), -1); 29 | }); 30 | 31 | Deno.test("String isString", () => { 32 | assertEquals(S.isString(1), false); 33 | assertEquals(S.isString(""), true); 34 | }); 35 | 36 | Deno.test("String isEmpty", () => { 37 | assertEquals(S.isEmpty("Hello"), false); 38 | assertEquals(S.isEmpty(""), true); 39 | }); 40 | 41 | Deno.test("String length", () => { 42 | assertEquals(S.length("Hello"), 5); 43 | assertEquals(S.length(""), 0); 44 | }); 45 | 46 | Deno.test("String split", () => { 47 | const splitEmpty = S.split(""); 48 | const splitSpace = S.split(" "); 49 | const splitWords = S.split(/\s+/); 50 | const splitHello = S.split("Hello"); 51 | 52 | assertEquals(splitEmpty(""), [""]); 53 | assertEquals(splitEmpty("Hello"), ["H", "e", "l", "l", "o"]); 54 | assertEquals(splitSpace(""), [""]); 55 | assertEquals(splitSpace("Hello World"), ["Hello", "", "World"]); 56 | assertEquals(splitWords(""), [""]); 57 | assertEquals(splitWords("Hello World"), ["Hello", "World"]); 58 | assertEquals(splitHello("Garage"), ["Garage"]); 59 | }); 60 | 61 | Deno.test("String includes", () => { 62 | const inc1 = S.includes("ello"); 63 | const inc2 = S.includes("ello", 2); 64 | 65 | assertEquals(inc1("Hello World"), true); 66 | assertEquals(inc1(""), false); 67 | assertEquals(inc2("Hello World"), false); 68 | assertEquals(inc1("World Hello"), true); 69 | }); 70 | 71 | Deno.test("String startsWith", () => { 72 | const inc1 = S.startsWith("ello"); 73 | const inc2 = S.startsWith("ello", 1); 74 | 75 | assertEquals(inc1("Hello World"), false); 76 | assertEquals(inc1(""), false); 77 | assertEquals(inc2("Hello World"), true); 78 | assertEquals(inc1("World Hello"), false); 79 | }); 80 | 81 | Deno.test("String endsWith", () => { 82 | const inc1 = S.endsWith("orld"); 83 | const inc2 = S.endsWith("orld", 1); 84 | 85 | assertEquals(inc1("Hello World"), true); 86 | assertEquals(inc1(""), false); 87 | assertEquals(inc2("Hello World"), false); 88 | assertEquals(inc1("World Hello"), false); 89 | }); 90 | 91 | Deno.test("String toUpperCase", () => { 92 | assertEquals(S.toUpperCase("Hello"), "HELLO"); 93 | }); 94 | 95 | Deno.test("String toLowerCase", () => { 96 | assertEquals(S.toLowerCase("Hello"), "hello"); 97 | }); 98 | 99 | Deno.test("String replace", () => { 100 | const rep1 = S.replace("hello", "Hello"); 101 | const rep2 = S.replace(/hello/gi, "Hello"); 102 | 103 | assertEquals(rep1("hello world"), "Hello world"); 104 | assertEquals(rep1(""), ""); 105 | assertEquals(rep2("hElLo World"), "Hello World"); 106 | }); 107 | 108 | Deno.test("String trim", () => { 109 | assertEquals(S.trim("Hello World"), "Hello World"); 110 | assertEquals(S.trim(" Hello World"), "Hello World"); 111 | assertEquals(S.trim("Hello World "), "Hello World"); 112 | assertEquals(S.trim(" Hello World "), "Hello World"); 113 | }); 114 | 115 | Deno.test("String trimStart", () => { 116 | assertEquals(S.trimStart("Hello World"), "Hello World"); 117 | assertEquals(S.trimStart(" Hello World"), "Hello World"); 118 | assertEquals(S.trimStart("Hello World "), "Hello World "); 119 | assertEquals(S.trimStart(" Hello World "), "Hello World "); 120 | }); 121 | 122 | Deno.test("String trimEnds", () => { 123 | assertEquals(S.trimEnd("Hello World"), "Hello World"); 124 | assertEquals(S.trimEnd(" Hello World"), " Hello World"); 125 | assertEquals(S.trimEnd("Hello World "), "Hello World"); 126 | assertEquals(S.trimEnd(" Hello World "), " Hello World"); 127 | }); 128 | 129 | Deno.test("String plural", () => { 130 | const are = S.plural("is", "are"); 131 | const rabbits = S.plural("rabbit", "rabbits"); 132 | const sentence = (n: number) => `There ${are(n)} ${n} ${rabbits(n)}`; 133 | assertEquals(sentence(1), "There is 1 rabbit"); 134 | assertEquals(sentence(4), "There are 4 rabbits"); 135 | assertEquals(sentence(0), "There are 0 rabbits"); 136 | }); 137 | 138 | Deno.test("String slice", () => { 139 | const slice = S.slice(1, 5); 140 | assertEquals(slice("Hello"), "ello"); 141 | assertEquals(slice(""), ""); 142 | assertEquals(slice("Hel"), "el"); 143 | }); 144 | 145 | Deno.test("String match", () => { 146 | const words = S.match(/\w+/g); 147 | assertEquals(words(""), O.none); 148 | assertEquals(words("Hello World"), O.some(["Hello", "World"])); 149 | assertEquals(words("Hello World"), O.some(["Hello", "World"])); 150 | }); 151 | 152 | Deno.test("String test", () => { 153 | const hasHello = S.test(/hello/i); 154 | 155 | assertEquals(hasHello("Hello World"), true); 156 | assertEquals(hasHello("Goodbye"), false); 157 | }); 158 | -------------------------------------------------------------------------------- /testing/sync.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as S from "../sync.ts"; 4 | import * as O from "../option.ts"; 5 | import * as N from "../number.ts"; 6 | import { pipe } from "../fn.ts"; 7 | 8 | const add = (n: number) => n + 1; 9 | 10 | // deno-lint-ignore no-explicit-any 11 | const assertEqualsSync = (a: S.Sync, b: S.Sync) => 12 | assertEquals(a(), b()); 13 | 14 | Deno.test("Sync wrap", () => { 15 | assertEqualsSync(S.wrap(1), S.wrap(1)); 16 | }); 17 | 18 | Deno.test("Sync apply", () => { 19 | assertEquals(pipe(S.wrap(add), S.apply(S.wrap(1)))(), S.wrap(2)()); 20 | }); 21 | 22 | Deno.test("Sync map", () => { 23 | const map = S.map(add); 24 | assertEqualsSync(map(S.wrap(1)), S.wrap(2)); 25 | }); 26 | 27 | Deno.test("Sync flatmap", () => { 28 | const flatmap = S.flatmap((n: number) => S.wrap(n + 1)); 29 | assertEqualsSync(flatmap(S.wrap(1)), S.wrap(2)); 30 | }); 31 | 32 | Deno.test("Sync fold", () => { 33 | const fold = S.fold((acc: number, cur: number) => acc + cur, 0); 34 | assertEquals(fold(S.wrap(1)), 1); 35 | }); 36 | 37 | Deno.test("Sync traverse", () => { 38 | const fold = O.match(() => -1, (n: S.Sync) => n()); 39 | const t0 = S.traverse(O.FlatmappableOption); 40 | const t1 = t0((n: number) => n === 0 ? O.none : O.some(n)); 41 | const t2 = fold(t1(S.wrap(0))); 42 | const t3 = fold(t1(S.wrap(1))); 43 | 44 | assertEquals(t2, -1); 45 | assertEquals(t3, 1); 46 | }); 47 | 48 | Deno.test("Sync getCombinableSync", () => { 49 | const { combine } = S.getCombinableSync(N.CombinableNumberSum); 50 | assertEquals(combine(S.wrap(1))(S.wrap(1))(), 2); 51 | }); 52 | 53 | Deno.test("Sync getInitializableSync", () => { 54 | const { combine, init } = S.getInitializableSync(N.InitializableNumberSum); 55 | assertEquals(init()(), 0); 56 | assertEquals(combine(S.wrap(1))(S.wrap(1))(), 2); 57 | }); 58 | 59 | Deno.test("Sync Do, bind, bindTo", () => { 60 | assertEqualsSync( 61 | pipe( 62 | S.wrap({}), 63 | S.bind("one", () => S.wrap(1)), 64 | S.bind("two", ({ one }) => S.wrap(one + one)), 65 | S.map(({ one, two }) => one + two), 66 | ), 67 | S.wrap(3), 68 | ); 69 | assertEqualsSync( 70 | pipe( 71 | S.wrap(1), 72 | S.bindTo("one"), 73 | ), 74 | S.wrap({ one: 1 }), 75 | ); 76 | }); 77 | -------------------------------------------------------------------------------- /testing/sync_either.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStrictEquals, 4 | } from "https://deno.land/std/testing/asserts.ts"; 5 | 6 | import * as SE from "../sync_either.ts"; 7 | import * as S from "../sync.ts"; 8 | import * as E from "../either.ts"; 9 | import * as N from "../number.ts"; 10 | import { constant, pipe, todo } from "../fn.ts"; 11 | 12 | const assertEqualsIO = ( 13 | // deno-lint-ignore no-explicit-any 14 | a: SE.SyncEither, 15 | // deno-lint-ignore no-explicit-any 16 | b: SE.SyncEither, 17 | ) => assertEquals(a(), b()); 18 | 19 | Deno.test("SyncEither left", () => { 20 | assertEqualsIO(SE.left(1), SE.left(1)); 21 | }); 22 | 23 | Deno.test("SyncEither right", () => { 24 | assertEqualsIO(SE.right(1), SE.right(1)); 25 | }); 26 | 27 | Deno.test("SyncEither tryCatch", () => { 28 | assertEqualsIO(SE.tryCatch(todo, () => 0), SE.left(0)); 29 | assertEqualsIO(SE.tryCatch(constant(1), () => 0), SE.right(1)); 30 | }); 31 | 32 | Deno.test("SyncEither fromEither", () => { 33 | assertEqualsIO(SE.fromEither(E.right(1)), SE.right(1)); 34 | assertEqualsIO(SE.fromEither(E.left(1)), SE.left(1)); 35 | }); 36 | 37 | Deno.test("SyncEither fromSync", () => { 38 | assertEquals(SE.fromSync(S.wrap(1))(), E.right(1)); 39 | }); 40 | 41 | Deno.test("SyncEither wrap", () => { 42 | assertEqualsIO(SE.wrap(1), SE.wrap(1)); 43 | }); 44 | 45 | Deno.test("SyncEither apply", () => { 46 | const add = (n: number) => n + 1; 47 | 48 | assertEquals(pipe(SE.wrap(add), SE.apply(SE.wrap(1)))(), SE.wrap(2)()); 49 | assertEquals(pipe(SE.left(1), SE.apply(SE.wrap(1)))(), SE.left(1)()); 50 | assertEquals(pipe(SE.wrap(add), SE.apply(SE.left(1)))(), SE.left(1)()); 51 | assertEquals(pipe(SE.left(1), SE.apply(SE.left(2)))(), SE.left(2)()); 52 | }); 53 | 54 | Deno.test("SyncEither map", () => { 55 | const fab = (n: number) => n + 1; 56 | const map = SE.map(fab); 57 | 58 | assertEqualsIO(map(SE.left(0)), SE.left(0)); 59 | assertEqualsIO(map(SE.right(0)), SE.right(1)); 60 | }); 61 | 62 | Deno.test("SyncEither flatmap", () => { 63 | const flatmap = SE.flatmap((n: number) => n === 0 ? SE.left(0) : SE.right(n)); 64 | 65 | assertEqualsIO(flatmap(SE.right(0)), SE.left(0)); 66 | assertEqualsIO(flatmap(SE.right(1)), SE.right(1)); 67 | assertEqualsIO(flatmap(SE.left(0)), SE.left(0)); 68 | }); 69 | 70 | Deno.test("SyncEither fail", () => { 71 | assertEqualsIO(SE.fail(0), SE.left(0)); 72 | }); 73 | 74 | Deno.test("SyncEither mapSecond", () => { 75 | const mapSecond = SE.mapSecond((n: number) => n + 1); 76 | 77 | assertEqualsIO(mapSecond(SE.left(0)), SE.left(1)); 78 | assertEqualsIO(mapSecond(SE.right(0)), SE.right(0)); 79 | }); 80 | 81 | Deno.test("SyncEither fold", () => { 82 | const fold = SE.fold((a: number, c: number) => a + c, 0); 83 | 84 | assertEquals(fold(SE.left(-1)), 0); 85 | assertEquals(fold(SE.right(1)), 1); 86 | }); 87 | 88 | Deno.test("SyncEither alt", () => { 89 | assertEqualsIO(pipe(SE.right(0), SE.alt(SE.right(1))), SE.right(0)); 90 | assertEqualsIO(pipe(SE.right(0), SE.alt(SE.left(1))), SE.right(0)); 91 | assertEqualsIO(pipe(SE.left(0), SE.alt(SE.right(1))), SE.right(1)); 92 | assertEqualsIO(pipe(SE.left(0), SE.alt(SE.left(1))), SE.left(1)); 93 | }); 94 | 95 | Deno.test("SyncEither recover", () => { 96 | const recover = SE.recover((n: number) => 97 | n === 0 ? SE.left(n + 1) : SE.right(n + 1) 98 | ); 99 | 100 | assertEqualsIO(recover(SE.right(0)), SE.right(0)); 101 | assertEqualsIO(recover(SE.right(1)), SE.right(1)); 102 | assertEqualsIO(recover(SE.left(0)), SE.left(1)); 103 | assertEqualsIO(recover(SE.left(1)), SE.right(2)); 104 | }); 105 | 106 | Deno.test("SyncEither getCombinableSyncEither", () => { 107 | const { combine } = SE.getCombinableSyncEither( 108 | N.InitializableNumberSum, 109 | N.InitializableNumberSum, 110 | ); 111 | assertEquals(combine(SE.wrap(1))(SE.wrap(2))(), E.right(3)); 112 | assertEquals(combine(SE.wrap(1))(SE.fail(2))(), E.left(2)); 113 | assertEquals(combine(SE.fail(1))(SE.wrap(2))(), E.left(1)); 114 | assertEquals(combine(SE.fail(1))(SE.fail(2))(), E.left(3)); 115 | }); 116 | 117 | Deno.test("SyncEither getInitializableSyncEither", () => { 118 | const { init, combine } = SE.getInitializableSyncEither( 119 | N.InitializableNumberSum, 120 | N.InitializableNumberSum, 121 | ); 122 | assertEquals(init()(), E.right(0)); 123 | assertEquals(combine(SE.wrap(1))(SE.wrap(2))(), E.right(3)); 124 | assertEquals(combine(SE.wrap(1))(SE.fail(2))(), E.left(2)); 125 | assertEquals(combine(SE.fail(1))(SE.wrap(2))(), E.left(1)); 126 | assertEquals(combine(SE.fail(1))(SE.fail(2))(), E.left(3)); 127 | }); 128 | 129 | Deno.test("SyncEither getFlatmappableSyncRight", () => { 130 | const { apply, map, flatmap, wrap } = SE.getFlatmappableSyncRight( 131 | N.InitializableNumberSum, 132 | ); 133 | const add = (n: number) => n + 1; 134 | 135 | assertEquals(pipe(wrap(add), apply(wrap(2)))(), E.right(3)); 136 | assertEquals(pipe(wrap(add), apply(SE.fail(2)))(), E.left(2)); 137 | assertEquals(pipe(SE.fail(1), apply(wrap(2)))(), E.left(1)); 138 | assertEquals(pipe(SE.fail(1), apply(SE.fail(2)))(), E.left(3)); 139 | 140 | assertStrictEquals(map, SE.map); 141 | assertStrictEquals(flatmap, SE.flatmap); 142 | assertStrictEquals(wrap, SE.wrap); 143 | }); 144 | 145 | Deno.test("Datum Do, bind, bindTo", () => { 146 | assertEqualsIO( 147 | pipe( 148 | SE.wrap({}), 149 | SE.bind("one", () => SE.right(1)), 150 | SE.bind("two", ({ one }) => SE.right(one + one)), 151 | SE.map(({ one, two }) => one + two), 152 | ), 153 | SE.right(3), 154 | ); 155 | assertEqualsIO( 156 | pipe( 157 | SE.right(1), 158 | SE.bindTo("one"), 159 | ), 160 | SE.right({ one: 1 }), 161 | ); 162 | }); 163 | -------------------------------------------------------------------------------- /testing/tree.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; 2 | 3 | import * as T from "../tree.ts"; 4 | import * as O from "../option.ts"; 5 | import * as N from "../number.ts"; 6 | import { pipe } from "../fn.ts"; 7 | 8 | const add = (n: number) => n + 1; 9 | 10 | Deno.test("Tree wrap", () => { 11 | assertEquals(T.wrap(1), { value: 1, forest: [] }); 12 | assertEquals(T.wrap(1, [T.wrap(2)]), { 13 | value: 1, 14 | forest: [{ value: 2, forest: [] }], 15 | }); 16 | assertEquals(T.wrap(1), T.wrap(1)); 17 | }); 18 | 19 | Deno.test("Tree unwrap", () => { 20 | assertEquals(pipe(T.wrap(1), T.unwrap), 1); 21 | }); 22 | 23 | Deno.test("Tree getShowable", () => { 24 | const { show } = T.getShowable(N.ShowableNumber); 25 | assertEquals(show(T.wrap(1)), "1"); 26 | assertEquals( 27 | show(T.wrap(1, [T.wrap(2)])), 28 | `1 29 | └─ 2`, 30 | ); 31 | }); 32 | 33 | Deno.test("Tree apply", () => { 34 | assertEquals( 35 | pipe(T.wrap((n: number) => n + 1), T.apply(T.wrap(1))), 36 | T.wrap(2), 37 | ); 38 | assertEquals( 39 | pipe( 40 | T.wrap((n: number) => n + 1, [T.wrap((n: number) => n + n)]), 41 | T.apply(T.wrap(2)), 42 | ), 43 | T.wrap(3, [T.wrap(4)]), 44 | ); 45 | }); 46 | 47 | Deno.test("Tree map", () => { 48 | assertEquals(pipe(T.wrap(1), T.map(add)), T.wrap(2)); 49 | }); 50 | 51 | Deno.test("Tree flatmap", () => { 52 | const flatmap = T.flatmap((n: number) => 53 | n === 0 ? T.wrap(0) : T.wrap(n, [T.wrap(1)]) 54 | ); 55 | assertEquals(flatmap(T.wrap(2)), T.wrap(2, [T.wrap(1)])); 56 | assertEquals(flatmap(T.wrap(0)), T.wrap(0)); 57 | }); 58 | 59 | Deno.test("Tree fold", () => { 60 | const fold = T.fold((n: number, i: number) => n + i, 0); 61 | assertEquals(fold(T.wrap(1)), 1); 62 | assertEquals(fold(T.wrap(1, [T.wrap(2)])), 3); 63 | }); 64 | 65 | Deno.test("Tree traverse", () => { 66 | const t1 = T.traverse(O.FlatmappableOption); 67 | const t2 = t1((n: number) => n === 0 ? O.none : O.some(n)); 68 | assertEquals(t2(T.wrap(0)), O.none); 69 | assertEquals(t2(T.wrap(1)), O.some(T.wrap(1))); 70 | assertEquals(t2(T.wrap(1, [T.wrap(0)])), O.none); 71 | assertEquals(t2(T.wrap(1, [T.wrap(2)])), O.some(T.wrap(1, [T.wrap(2)]))); 72 | }); 73 | 74 | Deno.test("Tree drawForest", () => { 75 | const ta = T.wrap(1, [T.wrap(2), T.wrap(3)]); 76 | const tb = pipe(ta, T.map((n) => n.toString())); 77 | assertEquals(T.drawForest(tb.forest), "\n├─ 2\n└─ 3"); 78 | }); 79 | 80 | Deno.test("Tree drawTree", () => { 81 | const ta = T.wrap(1, [T.wrap(2), T.wrap(3)]); 82 | const tb = pipe(ta, T.map((n) => n.toString())); 83 | assertEquals(T.drawTree(tb), "1\n├─ 2\n└─ 3"); 84 | }); 85 | 86 | Deno.test("Tree match", () => { 87 | const match = T.match((a: number, bs: number[]) => 88 | bs.reduce((n: number, m: number) => n + m, a) 89 | ); 90 | assertEquals(match(T.wrap(1)), 1); 91 | assertEquals(match(T.wrap(1, [T.wrap(2, [T.wrap(3)])])), 6); 92 | }); 93 | 94 | Deno.test("Tree getComparableTree", () => { 95 | const { compare } = T.getComparableTree(N.ComparableNumber); 96 | const tree = T.tree(0); 97 | assertEquals(pipe(tree, compare(tree)), true); 98 | assertEquals(pipe(T.tree(0), compare(T.tree(0))), true); 99 | assertEquals( 100 | pipe(T.tree(0, [T.tree(1)]), compare(T.tree(0, [T.tree(1)]))), 101 | true, 102 | ); 103 | assertEquals( 104 | pipe(T.tree(0), compare(T.tree(0, [T.tree(1)]))), 105 | false, 106 | ); 107 | assertEquals(pipe(T.tree(0), compare(T.tree(1))), false); 108 | assertEquals(pipe(T.tree(1), compare(T.tree(0))), false); 109 | }); 110 | 111 | Deno.test("Tree bind, bindTo", () => { 112 | assertEquals( 113 | pipe( 114 | T.wrap(1), 115 | T.bindTo("one"), 116 | T.bind("two", ({ one }) => T.wrap(one + one)), 117 | T.map(({ one, two }) => one + two), 118 | ), 119 | T.wrap(3), 120 | ); 121 | assertEquals( 122 | pipe( 123 | T.wrap(1), 124 | T.bindTo("one"), 125 | ), 126 | T.wrap({ one: 1 }), 127 | ); 128 | }); 129 | -------------------------------------------------------------------------------- /traversable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Traversable is a structure that encapsulates the idea of iterating through 3 | * data and collecting it into another structure. This can be as simple as 4 | * turning an Array> into Option> or as complicated 5 | * as creating all combinations of numbers in three Array. 6 | * 7 | * @module Traversable 8 | * @since 2.0.0 9 | */ 10 | 11 | import type { $, Hold, Kind } from "./kind.ts"; 12 | import type { Applicable } from "./applicable.ts"; 13 | import type { Mappable } from "./mappable.ts"; 14 | import type { Foldable } from "./foldable.ts"; 15 | 16 | /** 17 | * A Traversable structure extends Mappable and Foldable. It contains the 18 | * methods map, fold, and traverse. 19 | * 20 | * @since 2.0.0 21 | */ 22 | export interface Traversable 23 | extends Mappable, Foldable, Hold { 24 | readonly traverse: ( 25 | A: Applicable, 26 | ) => ( 27 | faui: (a: A) => $, 28 | ) => ( 29 | ta: $, 30 | ) => $, J, K], [L], [M]>; 31 | } 32 | -------------------------------------------------------------------------------- /wrappable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrappable is a structure that allows a value to be wrapped inside of the 3 | * associated concrete structure. 4 | * 5 | * @module Wrappable 6 | * @since 2.0.0 7 | */ 8 | 9 | import type { $, Hold, Kind } from "./kind.ts"; 10 | 11 | /** 12 | * A Wrappable structure has the method wrap. 13 | * 14 | * @example 15 | * ```ts 16 | * import type { Wrappable } from "./wrappable.ts"; 17 | * import * as O from "./option.ts"; 18 | * 19 | * // Example with Option wrappable 20 | * const wrapped = O.WrappableOption.wrap(42); 21 | * console.log(wrapped); // Some(42) 22 | * ``` 23 | * 24 | * @since 2.0.0 25 | */ 26 | export interface Wrappable extends Hold { 27 | readonly wrap: ( 28 | a: A, 29 | ) => $; 30 | } 31 | --------------------------------------------------------------------------------