If you wanted to stay "within fp-ts
" you could do something like the following:
import { flow, pipe, tuple } from "fp-ts/function"
import * as Mn from "fp-ts/Monoid"
import * as N from "fp-ts/number"
import * as O from "fp-ts/Option"
import * as Ord from "fp-ts/Ord"
import * as RM from "fp-ts/ReadonlyMap"
import * as RT from "fp-ts/ReadonlyTuple"
import * as Sg from "fp-ts/Semigroup"
import * as S from "fp-ts/string"
type Person = string
type Age = number
type PersonAge = readonly [Person, Age]
const ordByAge: Ord.Ord<PersonAge> = pipe(N.Ord, Ord.contramap(RT.snd))
const monoidMaxByAge: Mn.Monoid<O.Option<PersonAge>> = pipe(
ordByAge,
Sg.max,
O.getMonoid
)
const oldestPerson = (ps: ReadonlyMap<Person, Age>) =>
pipe(
ps,
RM.foldMapWithIndex(S.Ord)(monoidMaxByAge)(flow(tuple, O.some)),
O.map(RT.fst)
)
Which is pretty verbose and doesn't address your concern about performance.
One thing I find annoying is that it requires an ORD. Maybe this is required to make the function more deterministic but sorting is not important for many folding/reducing tasks and decreases performance.
You're right that it's about determinism. The native JS Map
stores entries in insertion order, so without an Ord
instance for the keys, this oldestPerson
function could yield different results for two given Map
s that are equivalent aside from insertion order. I would think of that as unexpected behavior.
Here's what I would expect to hold:
import * as assert from "assert"
const aliceBob: ReadonlyMap<Person, Age> = new Map([
["Alice", 25],
["Bob", 25],
])
const bobAlice: ReadonlyMap<Person, Age> = new Map([
["Bob", 25],
["Alice", 25],
])
const emptyMap: ReadonlyMap<Person, Age> = new Map([])
assert.deepStrictEqual(oldestPerson(aliceBob), oldestPerson(bobAlice))
assert.deepStrictEqual(oldestPerson(emptyMap), O.none)
I agree that a lot of the functions in fp-ts/ReadonlyMap
aren't very efficient, I think they're just meant to be a minimal "functional API" wrapper around a native data structure that doesn't play well with immutability and determinism. If performance is more of a concern I would probably do as you did and use the native methods.
something called getFoldableWithIndex
but I don't know how to use it or get it to compile with Typescript.
getFoldableWithIndex
will return a FoldableWithIndex
type class instance which isn't going to be useful to you here. This is a value that you would pass as an argument to a function that expects a FoldableWithIndex
instance, which I'm not even sure there's a good example of one . There's a traverse_
in fp-ts/Foldable
which takes a Foldable
instance, but no corresponding traverseWithIndex_
, though that hypothetical function would take a FoldableWithIndex
instance. There are some functions in fp-ts/FoldableWithIndex
, but those are about composing two FoldableWithIndex
instances into another (which is not useful here).
You could use that FoldableWithIndex
directly: the type class instance is an object with an uncurried foldMapWithIndex
method on it, so if you wanted to make the original snippet even more verbose, you could do:
const oldestPerson = (ps: ReadonlyMap<Person, Age>) =>
pipe(
RM.getFoldableWithIndex(S.Ord).foldMapWithIndex(monoidMaxByAge)(
ps,
flow(tuple, O.some)
),
O.map(RT.fst)
)
but those instances aren't really meant to be used directly in a pipe
, which is why I'd use the top-level RM.foldMapWithIndex
. There's more about how type classes in fp-ts
work here.