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)