This repository has been archived on 2023-05-29. You can view files and clone it, but cannot push or open issues or pull requests.
fp-ts-training/src/exo7/exo7.ts

232 lines
8.0 KiB
TypeScript

// `fp-ts` training Exercise 7
// Manipulate collections with type-classes
import {
number,
readonlyArray,
readonlyMap,
readonlySet,
semigroup,
string,
} from "fp-ts";
import { pipe } from "fp-ts/lib/function";
// In this exercise, we will learn how to manipulate essential collections
// such as `Set` and `Map`.
//
// These collections have very important properties that make them slightly
// more complex than the venerable `Array`:
// - `Set` requires each element to be unique
// - `Map` associates unique keys to values
//
// In fact, it can sometimes be helpful to think of `Set` as a special case
// of `Map` where `Set<T>` is strictly equivalent to `Map<T, void>`.
//
// To manipulate these collections, we often need to inform `fp-ts` on
// how to uphold the properties outlined above (eg. how to determine whether
// two elements or keys have the same value, how to combine values together
// in case of key collision or how to order the values when converting back
// to an array).
//
// And the way to describe those properties for the specific inner types of a
// given `Set` or `Map` is... TYPECLASSES!
///////////////////////////////////////////////////////////////////////////////
// SET //
///////////////////////////////////////////////////////////////////////////////
// A `Set` is pretty straightforward, it stores values but doesn't care at all
// about the ordering of those values. Furthermore, it ignores duplicates.
export const numberArray: ReadonlyArray<number> = [7, 42, 1337, 1, 0, 1337, 42];
// Construct `numberSet` from the provided `numberArray`.
// You need to use the `ReadonlySet` module from `fp-ts` instead of the
// JavaScript standard constructor.
//
// HINTS:
// - You can look into `readonlySet.fromReadonlyArray`
// - `fp-ts` doesn't know how you want to define equality for the inner type
// and requires you to provide an `Eq` instance
export const numberSet: ReadonlySet<number> = readonlySet.fromReadonlyArray(
number.Eq,
)(numberArray);
// Convert `numberSet` back to an array in `numberArrayFromSet`.
// You need to use the `ReadonlySet` module from `fp-ts` instead of the
// JavaScript standard constructor.
//
// HINTS:
// - You can look into `readonlySet.toReadonlyArray`
// - The elements in `numberSet` have no guarantees whatsoever regarding
// their ordering. This ordering could be totally random. But remember that
// functional programming is all about purity. Converting a set to an array
// is a pure operation and as such, should return the same value for the
// same input. This means you **need** to instruct `fp-ts` on how you wish
// the values to be ordered in the output array, by providing an `Ord`
// instance.
export const numberArrayFromSet: ReadonlyArray<number> = readonlySet
.toReadonlyArray(number.Ord)(numberSet);
///////////////////////////////////////////////////////////////////////////////
// MAP //
///////////////////////////////////////////////////////////////////////////////
// A `Map` associates a set of unique keys to arbitrary values. Values
// themselves can be duplicated across various keys but keys have to be unique.
//
// This means than when constructing a `Map` from an `Array`, you need to be
// explicit on how you wish to combine values in case of key collision (maybe
// you want to only insert the last value provided, maybe the first, maybe you
// want to combine both values in a specific way eg. concatenate strings, add
// numbers, etc...)
export const associativeArray: ReadonlyArray<[number, string]> = [
[1, "Alice"],
[2, "Bob"],
[3, "Clara"],
[4, "Denise"],
[2, "Robert"],
];
// Construct `mapWithLastEntry` from the provided `associativeArray`.
// You need to use the `ReadonlyMap` module from `fp-ts` instead of the
// JavaScript standard constructor.
//
// The resulting `Map` should have the following shape:
// 1 => 'Alice'
// 2 => 'Robert'
// 3 => 'Clara'
// 4 => 'Denise'
//
// HINTS:
// - You can look into `readonlyMap.fromFoldable`
// - You need to provide an `Eq` instance for the key type
// - You need to provide a `Magma` instance for the value type. In this case,
// the `Magma` instance should ignore the first value and return the second.
// (You can define your own, or look into the `Magma` or `Semigroup` module)
// - You need to provide the `Foldable` instance for the input container type.
// Just know that you can construct a `Map` from other types than `Array` as
// long as they implement `Foldable`. Here, you can simply pass the standard
// `readonlyArray.Foldable` instance.
export const mapWithLastEntry: ReadonlyMap<number, string> = pipe(
associativeArray,
readonlyMap.fromFoldable(
number.Eq,
semigroup.last<string>(),
readonlyArray.Foldable,
),
);
// Same thing as above, except that upon key collision we don't want to simply
// select the newest entry value but append it to the previous one.
//
// Basically, the resulting `Map` here should have the following shape:
// 1 => 'Alice'
// 2 => 'BobRobert'
// 3 => 'Clara'
// 4 => 'Denise'
//
// HINT:
// - You can look into the `Semigroup` typeclass as it is a super-class of
// `Magma`, meaning that a `Semigroup<A>` is also necessarily a `Magma<A>`.
// The `string` module may contain what you need ;)
//
// Bonus point:
// Did you find something in the `Semigroup` module that may have been
// helpful in defining `mapWithLastEntry`?
export const mapWithConcatenatedEntries: ReadonlyMap<number, string> = pipe(
associativeArray,
readonlyMap.fromFoldable(
number.Eq,
string.Semigroup,
readonlyArray.Foldable,
),
);
///////////////////////////////////////////////////////////////////////////////
// DIFFERENCE / UNION / INTERSECTION //
///////////////////////////////////////////////////////////////////////////////
export const primes = new Set([2, 3, 5, 7]);
export const odds = new Set([1, 3, 5, 7, 9]);
// Construct the set `nonPrimeOdds` from the two sets defined above. It should
// only include the odd numbers that are not prime.
//
// HINT:
// - Be mindful of the order of operands for the operator you will choose.
export const nonPrimeOdds: ReadonlySet<number> = pipe(
odds,
readonlySet.difference(number.Eq)(primes),
);
// Construct the set `primeOdds` from the two sets defined above. It should
// only include the odd numbers that are also prime.
export const primeOdds: ReadonlySet<number> = pipe(
odds,
readonlySet.intersection(number.Eq)(primes),
);
///////////////////////////////////////////////////////////////////////////////
export type Analytics = {
page: string;
views: number;
};
// These example Maps are voluntarily written in a non fp-ts way to not give
// away too much obviously ;)
//
// As an exercise for the reader, they may rewrite those with what they've
// learned earlier.
export const pageViewsA = new Map(
[
{ page: "home", views: 5 },
{ page: "about", views: 2 },
{ page: "blog", views: 7 },
].map((entry) => [entry.page, entry]),
);
export const pageViewsB = new Map(
[
{ page: "home", views: 10 },
{ page: "blog", views: 35 },
{ page: "faq", views: 5 },
].map((entry) => [entry.page, entry]),
);
const AnalyticsSemigroup = semigroup.struct({
page: semigroup.first<string>(),
views: number.SemigroupSum,
});
// Construct the `Map` with the total page views for all the pages in both sources
// of analytics `pageViewsA` and `pageViewsB`.
//
// In case a page appears in both sources, their view count should be summed.
export const allPageViews: ReadonlyMap<string, Analytics> = pipe(
pageViewsA,
readonlyMap.union(string.Eq, AnalyticsSemigroup)(
pageViewsB,
),
);
// Construct the `Map` with the total page views but only for the pages that
// appear in both sources of analytics `pageViewsA` and `pageViewsB`.
export const intersectionPageViews: ReadonlyMap<string, Analytics> = pipe(
pageViewsA,
readonlyMap.intersection(string.Eq, AnalyticsSemigroup)(
pageViewsB,
),
);