0

I want to be able to fold/reduce a Map, just like I can with Array and Set. The closest I see is something called getFoldableWithIndex but I don't know how to use it or get it to compile with Typescript. 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. My workaround is leaving fp-ts, generating an array or iterable of [key,value] pairs, and doing a simple reduce on it to find the person with the max age.

import { map as MAP, ord as ORD } from "fp-ts"

type Person = string;
type Age = number;

const oldestPerson = (ps:Map<Person, Age>) =>
  pipe(ps, MAP.getFoldableWithIndex<Person>(ORD.fromCompare<Person>((a,b)=>0)))......

Just noticed a more recent unreleased version has support for reduce. Still don't know why ORD is required or how to use getFolderableWithIndex or when the newer version will be released.

https://github.com/gcanti/fp-ts/blob/2.11/src/ReadonlyMap.ts

JustinM
  • 913
  • 9
  • 24

2 Answers2

2

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 Maps 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.

Peter Murphy
  • 74
  • 1
  • 3
0

You can:

import { ordNumber } from 'fp-ts/Ord'

type Name = string
type Age = number
type Persons = Map<Name, Age>
const persons: Persons = new Map()

persons.set('Joao', 13)
persons.set('Roberto', 20)

const sorted = new Map(
  [...persons.entries()].sort((a, b) => ordNumber.compare(b[1], a[1]))
)

However, it is important to note that in this example, you are mutating the persons variable by calling .set.

A more pure, functional approach would be:

import { ordNumber } from 'fp-ts/Ord'
import { Eq as StringEq } from 'fp-ts/string'
import * as F from 'fp-ts/function'
import * as M from 'fp-ts/Map'

type Name = string
type Age = number
const insertIntoPersons = M.upsertAt(StringEq)

const persons = F.pipe(
  new Map<Name, Age>(),
  insertIntoPersons('Joao', 13),
  insertIntoPersons('Roberto', 20)
)

const sorted = new Map(
  [...persons.entries()].sort((a, b) => ordNumber.compare(b[1], a[1]))
)
kibe
  • 90
  • 1
  • 8
  • 26
  • Thanks. But I'm wondering if there is a way to do this using fp-ts. It has an Array.reduce and I'm looking for something similar with Set and Map. – JustinM Jul 20 '21 at 12:53
  • @JustinM mmm, not that I know of, no. but I guess you can convert the `Map` to an `Array` and call a reduce from there? what's your end goal? – kibe Jul 20 '21 at 12:58
  • In this case to do an efficient reduce/fold on Map and Set. To find the max, you don't need to sort - just loop through and keep track of the max. I find it odd that fp-ts doesn't allow this, especially with functions like "getFoldableWithIndex" and "toUnfoldable" where the docs make it sound almost like what I want. I'd really like to understand how fp-ts works, not just get the code written. – JustinM Jul 20 '21 at 13:05
  • This kind of works... pipe(ps, MAP.toUnfoldable(ordString, ARRAY.Unfoldable)). It creates an array of [key,value] tuples. Still requires an unnecessary sort. Might as well break out of fp-ts to avoid the sort. – JustinM Jul 20 '21 at 13:13
  • actually, fp-ts allows you to fold a `Map` using `getFoldableWithIndex` as you mentioned, as along as you provide a `Ord` instance for the keys... which is odd. you can check the code here: https://github.com/gcanti/fp-ts/blob/master/src/Map.ts#L662 i honestly don't know why is that. i guess you can copy the `reduceWithIndex` function and modify it to get the keys unsorted, unless there is a reason for them to be sorted I don't know of – kibe Jul 20 '21 at 13:19
  • I couldn’t figure out how to use getFoldableWithIndex. Do you know how with my simple example? – JustinM Jul 20 '21 at 14:31
  • can't paste code here I guess, https://chat.stackoverflow.com/rooms/235100/fp-ts-map – kibe Jul 20 '21 at 14:34