import * as Eq from 'fp-ts/lib/Eq' import { identity } from 'fp-ts/lib/function' import { Functor1 } from 'fp-ts/lib/Functor' import * as O from 'fp-ts/lib/Option' import { pipe } from 'fp-ts/lib/pipeable' import * as RA from 'fp-ts/lib/ReadonlyArray' import * as Show from 'fp-ts/lib/Show' import * as RAZ from '@monorail/sharedHelpers/fp-ts-ext/ReadonlyArrayZipper' import { matchI } from '@monorail/sharedHelpers/matchers' /** * A variant of ReadonlyArrayOrZipper which represents the case where no item is selected */ export type IsReadonlyArray = { readonly tag: 'isReadonlyArray' readonly value: ReadonlyArray } /** * A variant of ReadonlyArrayOrZipper which represents the case where an item is selected */ export type IsReadonlyArrayZipper = { readonly tag: 'isReadonlyArrayZipper' readonly value: RAZ.ReadonlyArrayZipper } /** * Represents a an array-like structure where zero or one item is focused/selected. This is basically a tagged union * with a member that is just a ReadonlyArray and a member that is a ReadonlyArrayZipper. Focus can be set or unset * by using functions that might switch the value between these types. * * If you need a collection that is guaranteed to have a single item focused/selected at all times, see ReadonlyArrayZipper. * * The purpose of this is to make sure the selected item is tracked as an actual member of the collection and not * an optional value that is tracked off to the side. The selected item is a first-class member of the collection. */ export type ReadonlyArrayOrZipper = | IsReadonlyArray | IsReadonlyArrayZipper /** * Constructs the variant that has no focus (ReadonlyArray) */ export const makeWithNoFocus = ( value: ReadonlyArray, ): IsReadonlyArray => ({ tag: 'isReadonlyArray', value, }) /** * Alias for `makeWithNoFocus` */ export const makeWithReadonlyArray = makeWithNoFocus /** * Constructs the variant that has a focused value (ReadonlyArrayZipper) */ export const makeWithFocus = ( value: RAZ.ReadonlyArrayZipper, ): IsReadonlyArrayZipper => ({ tag: 'isReadonlyArrayZipper', value }) /** * Alias for `makeWithFocus` */ export const makeWithReadonlyArrayZipper = makeWithFocus /** * Constructs with the given value at the focus */ export const of = (focus: A): ReadonlyArrayOrZipper => makeWithFocus(RAZ.of(focus)) /** * Constructs an empty ReadonlyArrayOrZipper, which is the variant with no focus (and no values) */ export const empty: ReadonlyArrayOrZipper = makeWithNoFocus([]) /** * Converts the given value to a ReadonlyArray with each item and an indicator for each item for whether it was focused */ export const toReadonlyArrayWithFocusFlag = ( fa: ReadonlyArrayOrZipper, ): ReadonlyArray<{ value: A; isFocus: boolean }> => matchI(fa)({ isReadonlyArray: a => a.value.map(value => ({ value, isFocus: false })), isReadonlyArrayZipper: a => RAZ.toReadonlyArrayWithFocusFlag(a.value), }) /** * Converts the given value to a ReadonlyArray */ export const toReadonlyArray = ( fa: ReadonlyArrayOrZipper, ): ReadonlyArray => matchI(fa)({ isReadonlyArray: a => a.value, isReadonlyArrayZipper: a => RAZ.toReadonlyArray(a.value), }) /** * Indicates if the given value has a focused item */ export const hasFocus = (fa: ReadonlyArrayOrZipper): boolean => matchI(fa)({ isReadonlyArray: _ => false, isReadonlyArrayZipper: _ => true, }) /** * Gets the currently-focused value (if there is one) */ export const getFocus = (fa: ReadonlyArrayOrZipper): O.Option => matchI(fa)>({ isReadonlyArray: _ => O.none, isReadonlyArrayZipper: a => O.some(a.value.focus), }) /** * Clears the focus (if there is one) */ export const clearFocus = ( fa: ReadonlyArrayOrZipper, ): IsReadonlyArray => matchI(fa)>({ isReadonlyArray: identity, isReadonlyArrayZipper: ({ value }) => makeWithNoFocus(RAZ.toReadonlyArray(value)), }) /** * Attempts to move the focus to the given item by finding it in the collection */ export const find = (eq: Eq.Eq) => (item: A) => ( fa: ReadonlyArrayOrZipper, ): O.Option> => matchI(fa)({ isReadonlyArray: ({ value }) => pipe( RAZ.fromReadonlyArray(value), O.chain(raz => RAZ.find(eq)(item)(raz)), O.map(raz => makeWithFocus(raz)), ), isReadonlyArrayZipper: ({ value }) => pipe( value, RAZ.find(eq)(item), O.map(raz => makeWithFocus(raz)), ), }) /** * Attempts to move the focus to the given item, and if no item is found, returns the input collection unchanged */ export const findOrKeep = (eq: Eq.Eq) => (item: A) => ( fa: ReadonlyArrayOrZipper, ): ReadonlyArrayOrZipper => pipe( find(eq)(item)(fa), O.getOrElse(() => fa), ) /** * Attempts to move the focus to the given item, and if it's not found, clears the focus. */ export const findOrClear = (eq: Eq.Eq) => (item: A) => ( fa: ReadonlyArrayOrZipper, ): ReadonlyArrayOrZipper => pipe( find(eq)(item)(fa), O.getOrElse>(() => clearFocus(fa)), ) /** * If the given item is none, clears the focus. If the given item is some, it attempts to focus on it. If the item is not found, * the focus is kept as-is. */ export const findOptionalOrKeep = (eq: Eq.Eq) => (oa: O.Option) => ( fa: ReadonlyArrayOrZipper, ): ReadonlyArrayOrZipper => pipe( oa, O.fold( () => clearFocus(fa), a => findOrKeep(eq)(a)(fa), ), ) /** * If the given item is none, clears the focus. If the given item is some, it attempts to focus on it. If the item is not found, * the focus is cleared. */ export const findOptionalOrClear = (eq: Eq.Eq) => (oa: O.Option) => ( fa: ReadonlyArrayOrZipper, ): ReadonlyArrayOrZipper => pipe( oa, O.fold( () => clearFocus(fa), a => findOrClear(eq)(a)(fa), ), ) /** * Maps a function over the collection */ export const map_ = ( fa: ReadonlyArrayOrZipper, f: (a: A) => B, ): ReadonlyArrayOrZipper => matchI(fa)>({ isReadonlyArray: ({ value }) => makeWithNoFocus(value.map(f)), isReadonlyArrayZipper: ({ value }) => makeWithFocus(RAZ.map(f)(value)), }) /** * Maps a function over the collection (pipeable) */ export const map = (f: (a: A) => B) => ( fa: ReadonlyArrayOrZipper, ): ReadonlyArrayOrZipper => map_(fa, f) export const URI = 'ReadonlyArrayOrZipper' export type URI = typeof URI declare module 'fp-ts/lib/HKT' { interface URItoKind { ReadonlyArrayOrZipper: ReadonlyArrayOrZipper } } export const getShow: ( showA: Show.Show, ) => Show.Show> = showA => { return { show: raof => matchI(raof)({ isReadonlyArray: x => `IsReadonlyArray(${RA.getShow(showA).show(x.value)})`, isReadonlyArrayZipper: x => `IsReadonlyArrayZipper(${RAZ.getShow(showA).show(x.value)})`, }), } } export const getEq = (eqA: Eq.Eq): Eq.Eq> => { return { equals: (a, b) => { if (a.tag === 'isReadonlyArray' && b.tag === 'isReadonlyArray') { return RA.getEq(eqA).equals(a.value, b.value) } else if ( a.tag === 'isReadonlyArrayZipper' && b.tag === 'isReadonlyArrayZipper' ) { return RAZ.getEq(eqA).equals(a.value, b.value) } else { return false } }, } } export const Functor: Functor1 = { URI, map: map_, } export const readonlyArrayOrZipper: Functor1 = { URI, map: map_, } // TODO: add more stuff as needed, including fp-ts typeclass machinery, etc. // Should be able to add these, and maybe more: // getSemigroup // getMonoid // FunctorWithIndex1 // Apply1 // Applicative1 // (no Monad - RAZ is not a Monad) // Foldable1 // Traversable1 // (no Comonad - RA is not a Comonad)