import {
Applicative as ApplicativeHKT,
Applicative1,
} from 'fp-ts/lib/Applicative'
import { Apply1 } from 'fp-ts/lib/Apply'
import { Comonad1 } from 'fp-ts/lib/Comonad'
import * as Eq from 'fp-ts/lib/Eq'
import { Extend1 } from 'fp-ts/lib/Extend'
import { Foldable1 } from 'fp-ts/lib/Foldable'
import { identity } from 'fp-ts/lib/function'
import { Functor1 } from 'fp-ts/lib/Functor'
import { FunctorWithIndex1 } from 'fp-ts/lib/FunctorWithIndex'
import { HKT } from 'fp-ts/lib/HKT'
import * as Mn from 'fp-ts/lib/Monoid'
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 RNEA from 'fp-ts/lib/ReadonlyNonEmptyArray'
import * as Sg from 'fp-ts/lib/Semigroup'
import * as Show from 'fp-ts/lib/Show'
import { Traversable1 } from 'fp-ts/lib/Traversable'
import * as RAExt from '@monorail/sharedHelpers/fp-ts-ext/ReadonlyArray'
/**
* ReadonlyArrayZipper is a "zipper" type for arrays - an array with a selected or "focused" item.
*
* Note: there is a fp-ts-contrib/lib/Zipper, but that is backed by `Array` rather than `ReadonlyArray`, so I'll go ahead with this
* for now. Some of the functions were copied from fp-ts-contrib/lib/Zipper.
*
* If you need a type where zero or one items can be selected, see `ReadonlyArrayOrZipper`
*
* Some implementations of list zippers use the FP-style linked list, and with that, it makes sense to store the lefts
* in reverse for O(1) moving the focus to the left. However, this is just using an Array, so I'm not going to reverse
* the lefts.
*
* For more information, see here: http://data.tmorris.net/talks/zippers/bd054c210649101b84662c614fc45af3c27a5eef/zippers.pdf
*/
export type ReadonlyArrayZipper = {
readonly lefts: ReadonlyArray
readonly focus: A
readonly rights: ReadonlyArray
}
/**
* Constructs with the given value at the focus
*/
export const of = (focus: A): ReadonlyArrayZipper => ({
lefts: [],
focus,
rights: [],
})
/**
* Constructs from the given parts
*/
export const make = (
lefts: ReadonlyArray,
focus: A,
rights: ReadonlyArray,
): ReadonlyArrayZipper => ({ lefts, focus, rights })
/**
* Constructs from the given ReadonlyArray. The first item in the array will be the focus. If the array is empty, none is returned.
*/
export const fromReadonlyArray = (
ra: ReadonlyArray,
): O.Option> =>
pipe(
RAExt.headAndTailS(ra),
O.map(({ head, tail }) => ({ lefts: [], focus: head, rights: tail })),
)
/**
* Constructs from the given ReadonlyNonEmptyArray. The head will be the focus and the tail the rights.
*/
export const fromReadonlyNonEmptyArray = (
rnea: RNEA.ReadonlyNonEmptyArray,
): ReadonlyArrayZipper => make([], RNEA.head(rnea), RNEA.tail(rnea))
/**
* Dumps the data structure into flattened array with each item flagged with whether it was focused
*/
export const toReadonlyArrayWithFocusFlag = (
raz: ReadonlyArrayZipper,
): ReadonlyArray<{ value: A; isFocus: boolean }> =>
raz.lefts
.map(value => ({ value, isFocus: false }))
.concat({ value: raz.focus, isFocus: true })
.concat(raz.rights.map(value => ({ value, isFocus: false })))
/**
* Dumps the collection into a flattened array
*/
export const toReadonlyArray = (
raz: ReadonlyArrayZipper,
): ReadonlyArray => raz.lefts.concat(raz.focus).concat(raz.rights)
/**
* Gets the length of the array
*/
export const length = (raz: ReadonlyArrayZipper): number =>
raz.lefts.length + 1 + raz.rights.length
/**
* Moves the focus one item to the left. If the focus is at the left-most item, none is returned.
*/
export const moveLeft = (
raz: ReadonlyArrayZipper,
): O.Option> =>
pipe(
RAExt.initAndLastT(raz.lefts),
O.map(([leftInit, leftLast]) => ({
lefts: leftInit,
focus: leftLast,
rights: [raz.focus].concat(raz.rights),
})),
)
/**
* Moves the focus one item to the left. If the focus is at the left-most item, the array is returned unchanged.
*/
export const moveLeftWithClamp = (
raz: ReadonlyArrayZipper,
): ReadonlyArrayZipper =>
pipe(
moveLeft(raz),
O.getOrElse(() => raz),
)
/**
* Moves the focus one item to the right. If the focus is at the right-most item, none is returned.
*/
export const moveRight = (
raz: ReadonlyArrayZipper,
): O.Option> =>
pipe(
RAExt.headAndTailT(raz.rights),
O.map(([rightHead, rightTail]) => ({
lefts: raz.lefts.concat(raz.focus),
focus: rightHead,
rights: rightTail,
})),
)
/**
* Moves the focus one item to the right. If the focus is at the right-most item, the array is returned unchanged.
*/
export const moveRightWithClamp = (
raz: ReadonlyArrayZipper,
): ReadonlyArrayZipper =>
pipe(
moveRight(raz),
O.getOrElse(() => raz),
)
/**
* Overwrites the focus with the given value
*/
export const setFocus = (newFocus: A) => (
raz: ReadonlyArrayZipper,
): ReadonlyArrayZipper => ({
lefts: raz.lefts,
focus: newFocus,
rights: raz.rights,
})
/**
* Modifies the focus with the given function.
*/
export const modifyFocus = (f: (a: A) => A) => (
raz: ReadonlyArrayZipper,
): ReadonlyArrayZipper => ({
lefts: raz.lefts,
focus: f(raz.focus),
rights: raz.rights,
})
/**
* Attempts to find the given value by checking just the focus. If the focus matches the given item,
* the RAZ is returned wrapped in a some.
*/
export const findInFocus = (eq: Eq.Eq) => (item: A) => (
raz: ReadonlyArrayZipper,
): O.Option> =>
eq.equals(raz.focus, item) ? O.some(raz) : O.none
/**
* Attempts to find the given value by checking just the values to the left of the focus.
* If found, a new array is returned focused on the given item.
*/
export const findInLefts = (eq: Eq.Eq) => (item: A) => (
raz: ReadonlyArrayZipper,
): O.Option> =>
pipe(
moveLeft(raz),
O.chain(razNext =>
pipe(
findInFocus(eq)(item)(razNext),
O.alt(() => findInLefts(eq)(item)(razNext)), // TODO: not tail recursive
),
),
)
/**
* Attempts to find the given value by checking just the values to the right of the focus.
* If found, a new array is returned focused on the given item.
*/
export const findInRights = (eq: Eq.Eq) => (item: A) => (
raz: ReadonlyArrayZipper,
): O.Option> =>
pipe(
moveRight(raz),
O.chain(razNext =>
pipe(
findInFocus(eq)(item)(razNext),
O.alt(() => findInRights(eq)(item)(razNext)), // TODO: not tail recursive
),
),
)
/**
* Attempts to find the given value by checking the focus, the values to the left of the focus and the values to the right of the focus.
* If found, a new array is returned focused on the given item.
*/
export const find = (eq: Eq.Eq) => (item: A) => (
raz: ReadonlyArrayZipper,
): O.Option> =>
pipe(
findInFocus(eq)(item)(raz),
O.alt(() => findInLefts(eq)(item)(raz)),
O.alt(() => findInRights(eq)(item)(raz)),
)
/**
* Attempts to find the given value by checking the focus, the values to the left of the focus and the values to the right of the focus.
* If found, a new array is returned focused on the given item.
* If not found, the input array is returned unchanged.
*/
export const findOrKeep = (eq: Eq.Eq) => (item: A) => (
raz: ReadonlyArrayZipper,
): ReadonlyArrayZipper =>
pipe(
find(eq)(item)(raz),
O.getOrElse(() => raz),
)
/**
* Maps a function over the collection
*/
export const map_: Functor1['map'] = (raz, f) => ({
lefts: raz.lefts.map(f),
focus: f(raz.focus),
rights: raz.rights.map(f),
})
/**
* Maps a function (with index) over the collection
*/
export const mapWithIndex_: FunctorWithIndex1['mapWithIndex'] = <
A,
B
>(
raz: ReadonlyArrayZipper,
f: (index: number, a: A) => B,
): ReadonlyArrayZipper =>
make(
raz.lefts.map((value, index) => f(index, value)),
f(raz.lefts.length, raz.focus),
raz.rights.map((value, index) => f(raz.lefts.length + 1 + index, value)),
)
/**
* Applies a wrapped function to a collection
*/
export const ap_: Apply1['ap'] = (fab, fa) => ({
lefts: RA.readonlyArray.ap(fab.lefts, fa.lefts),
focus: fab.focus(fa.focus),
rights: RA.readonlyArray.ap(fab.rights, fa.rights),
})
export const extend_: Extend1['extend'] = (fa, f) => {
const lefts = fa.lefts.map((a, i) =>
f(
make(
pipe(fa.lefts, RA.takeLeft(i)),
a,
RA.snoc(pipe(fa.lefts, RA.dropLeft(i + 1)), fa.focus).concat(fa.rights),
),
),
)
const rights = fa.rights.map((a, i) =>
f(
make(
RA.snoc(fa.lefts, fa.focus).concat(pipe(fa.rights, RA.takeLeft(i))),
a,
pipe(fa.rights, RA.dropLeft(i + 1)),
),
),
)
return make(lefts, f(fa), rights)
}
const reduce_: Foldable1['reduce'] = (fa, b, f) =>
fa.rights.reduce(f, f(fa.lefts.reduce(f, b), fa.focus))
const reduceRight_: Foldable1['reduceRight'] = (fa, b, f) => {
const rights = fa.rights.reduceRight((acc, a) => f(a, acc), b)
const focus = f(fa.focus, rights)
return fa.lefts.reduceRight((acc, a) => f(a, acc), focus)
}
const foldMap_: Foldable1['foldMap'] = M => (fa, f) => {
const lefts = fa.lefts.reduce((acc, a) => M.concat(acc, f(a)), M.empty)
const rights = fa.rights.reduce((acc, a) => M.concat(acc, f(a)), M.empty)
return M.concat(M.concat(lefts, f(fa.focus)), rights)
}
const traverse_ = (
F: ApplicativeHKT,
): ((
ta: ReadonlyArrayZipper,
f: (a: A) => HKT,
) => HKT>) => {
const traverseF = RA.readonlyArray.traverse(F)
return (ta: ReadonlyArrayZipper, f: (a: A) => HKT) =>
F.ap(
F.ap(
F.map(
traverseF(ta.lefts, f),
lefts => (focus: B) => (rights: ReadonlyArray) =>
make(lefts, focus, rights),
),
f(ta.focus),
),
traverseF(ta.rights, f),
)
}
export const map = (f: (a: A) => B) => (
raz: ReadonlyArrayZipper,
): ReadonlyArrayZipper => map_(raz, f)
export const mapWithIndex = (f: (index: number, a: A) => B) => (
raz: ReadonlyArrayZipper,
): ReadonlyArrayZipper => mapWithIndex_(raz, f)
export const ap = (fa: ReadonlyArrayZipper) => (
fab: ReadonlyArrayZipper<(a: A) => B>,
): ReadonlyArrayZipper => ap_(fab, fa)
export const apFirst = (fb: ReadonlyArrayZipper) => (
fa: ReadonlyArrayZipper,
): ReadonlyArrayZipper =>
pipe(
fa,
map(a => (_: B) => a),
ap(fb),
)
export const apSecond = (fb: ReadonlyArrayZipper) => (
fa: ReadonlyArrayZipper,
): ReadonlyArrayZipper =>
pipe(
fa,
map(_a => (b: B) => b),
ap(fb),
)
export const extend: (
f: (fa: ReadonlyArrayZipper) => B,
) => (wa: ReadonlyArrayZipper) => ReadonlyArrayZipper = f => wa =>
extend_(wa, f)
export const duplicate: (
wa: ReadonlyArrayZipper,
) => ReadonlyArrayZipper> = extend(identity)
export const foldMap: (
M: Mn.Monoid,
) => (f: (a: A) => M) => (fa: ReadonlyArrayZipper) => M = M => f => fa =>
foldMap_(M)(fa, f)
export const reduce: (
b: B,
f: (b: B, a: A) => B,
) => (fa: ReadonlyArrayZipper) => B = (b, f) => fa => reduce_(fa, b, f)
export const reduceRight: (
b: B,
f: (a: A, b: B) => B,
) => (fa: ReadonlyArrayZipper) => B = (b, f) => fa => reduceRight_(fa, b, f)
export const sequence: Traversable1['sequence'] = (
F: ApplicativeHKT,
): ((
ta: ReadonlyArrayZipper>,
) => HKT>) => {
const sequenceF = RA.readonlyArray.sequence(F)
return (ta: ReadonlyArrayZipper>) =>
F.ap(
F.ap(
F.map(
sequenceF(ta.lefts),
lefts => (focus: A) => (rights: ReadonlyArray) =>
make(lefts, focus, rights),
),
ta.focus,
),
sequenceF(ta.rights),
)
}
export const extract: Comonad1['extract'] = fa => fa.focus
export const URI = 'ReadonlyArrayZipper'
export type URI = typeof URI
declare module 'fp-ts/lib/HKT' {
interface URItoKind {
ReadonlyArrayZipper: ReadonlyArrayZipper
}
}
export const getShow: (
showA: Show.Show,
) => Show.Show> = showA => {
const showRA = RA.getShow(showA)
return {
show: raz =>
`ReadonlyArrayZipper(${showRA.show(raz.lefts)}, ${showA.show(
raz.focus,
)}, ${showRA.show(raz.rights)})`,
}
}
export const getEq = (eqA: Eq.Eq): Eq.Eq> => {
const eqRA = RA.getEq(eqA)
return {
equals: (a, b) =>
eqRA.equals(a.lefts, b.lefts) &&
eqA.equals(a.focus, b.focus) &&
eqRA.equals(a.rights, b.rights),
}
}
export const getSemigroup = (
semigroupA: Sg.Semigroup,
): Sg.Semigroup> => {
return {
concat: (a, b) =>
make(
a.lefts.concat(b.lefts),
semigroupA.concat(a.focus, b.focus),
a.rights.concat(b.rights),
),
}
}
export const getMonoid = (
monoidA: Mn.Monoid,
): Mn.Monoid> => {
return {
...getSemigroup(monoidA),
empty: make(RA.empty, monoidA.empty, RA.empty),
}
}
export const Functor: Functor1 = {
URI,
map: map_,
}
export const FunctorWithIndex: FunctorWithIndex1 = {
...Functor,
mapWithIndex: mapWithIndex_,
}
export const Apply: Apply1 = {
...Functor,
ap: ap_,
}
export const Applicative: Applicative1 = {
...Apply,
of,
}
export const Foldable: Foldable1 = {
URI,
foldMap: foldMap_,
reduce: reduce_,
reduceRight: reduceRight_,
}
export const Traversable: Traversable1 = {
...Functor,
...Foldable,
traverse: traverse_,
sequence,
}
export const Comonad: Comonad1 = {
...Functor,
extend: extend_,
extract,
}
export const readonlyArrayZipper: Applicative1 &
Foldable1 &
Traversable1 &
Comonad1 &
FunctorWithIndex1 = {
URI,
map: map_,
of,
ap: ap_,
extend: extend_,
extract,
reduce: reduce_,
reduceRight: reduceRight_,
foldMap: foldMap_,
traverse: traverse_,
sequence,
mapWithIndex: mapWithIndex_,
}