// import "../../lab_modules/036";
Under Utilized Functions
In this lab I'm going to explore some of the other functions in fp-ts that I don't use as often. Some of these are just concepts that I don't understand (yet) and others are just functions that I've never had a need for.
Apply
const double = (n: number): number => n * 2;
const increment = (n: number): number => n + 1;
const sequenceOptionStruct = Ap.sequenceS(O.Applicative);
const sequenceOptionTuple = Ap.sequenceT(O.Applicative);
const structResult = pipe(
sequenceOptionStruct({
a: O.some(1),
b: O.some(2),
c: O.some(3),
}),
O.map(({ a, b, c }) => a + b + c),
O.map(increment),
);
const tupleResult = pipe(
sequenceOptionTuple(O.some(1), O.some(2), O.some(3)),
O.map(A.map(double)),
);
console.log(structResult); // Output: Some(7)
console.log(tupleResult); // Output: Some([2, 4, 6])
Apply is a type class that extends Functor. It is used to apply a function
contained in a context to a value contained in a context. The sequenceS
and
sequenceT
functions are used to apply a function to a struct or tuple of
values.
This seems very useful when needing to do validation on a struct or tuple of values. Especially when applying multiple validations to a struct or tuple of values.
Do
const doResult = pipe(
O.Do,
O.bind("a", () => O.some(1)),
O.bind("b", ({ a }) => O.some(increment(a))),
O.bind("c", ({ b }) => O.some(increment(b))),
O.map(({ a, b, c }) => a + b + c),
O.map(increment),
);
console.log(doResult); // Output: Some(7)
The Do
function is used to chain computations in a Monad
. In combination
with the bind
function, it can be used create computed objects which are
chained together. The resulting right
value is the final computation, can be
used with map
to apply a function to the final value.
I think this is a very useful function, and I'm going to start using it more
often. Especially when used as TaskEither
or IO
to chain together
computations that rely on multiple successful steps.
Reader
interface Config {
port: number;
host: string;
}
const getConfig = R.ask<Config>();
// const getPort = (config: Config): number => config.port;
const getPort = pipe(
getConfig,
R.map((config) => config.port),
);
const readerResult = pipe(
O.some({ port: 3000, host: "localhost" }),
O.map(getPort),
);
console.log(readerResult); // Output: Some(3000)
The Reader
type is used to manipulate the object passed to a function. It is
used create a function that takes a config object and returns a value. The ask
function is used to get the config object from the context.
I have a little harder time finding good examples of when this would be useful. I think it would be useful when you have a function that needs to access a configuration object, but you don't want to pass the configuration object to the function every time it is called.
Covariant
const someNumber = O.some(1);
const someNumberResult = pipe(someNumber, O.map(double));
console.log(someNumberResult); // Output: Some(2)
const maybeNumber = E.right<string, number>(1);
const maybeNumberResult = pipe(
maybeNumber,
E.bimap(
(error) => `Not a number ${error}`, // on left side; add error message
double, // on right side; double the number
),
);
console.log(maybeNumberResult); // Output: Right(2)
The Covariant
type class is the group of types that support the map
function. Similar to the map
functions that iterates over the right
value of
an Either
, or the some
value of an Option
. The bimap
function is used to
apply a function to the left
or right
value of an Either
.
This can be useful when consolidating functions that might return an Either
or
an Option
. Such as when editing error messages, or when you want to apply a
function to the right
value of an Either
.
Contravariant
type User = {
id: number;
name: string;
};
const sortById = pipe(
N.Ord, // Ord<number>
Ord.contramap((user: User) => user.id), // Ord<User>
);
const sortResult = pipe(
[
{ id: 2, name: "John" },
{ id: 1, name: "Jane" },
],
A.sort(sortById),
A.map((user) => user.name),
);
console.log(sortResult); // Output: [ 'Jane', 'John' ]
The Contravariant
type class is the group of types that support the
contramap
function. This function is used to apply a function to the input of
a function.
This can be useful when you want to sort a list of objects by a property of the
object. The contramap
function can be used to apply a function to the input of
the Ord
function.
Profunctor
type UserProfile = User & {
email: string;
verified: boolean;
};
const someUser: UserProfile = {
id: 1,
name: "John",
email: "john@gmail.com",
verified: false,
};
const isEligible = pipe(
(user: UserProfile) => user.email, // map the input (B -> C)
R.promap(
(user: UserProfile) => ({ ...user, email: user.email.toLowerCase() }), // pre-process the input (A -> B)
(email: string) => /@gmail.com$/.test(email), // post-process the output (C -> D)
),
);
const isEligibleResult = pipe(someUser, isEligible);
console.log(isEligibleResult); // Output: true
const isVerified = pipe(
O.fromPredicate((user: UserProfile) => user.verified),
R.promap(
(user: UserProfile) => ({ ...user, verified: user.verified === true }),
E.fromOption(() => "User is not verified"),
),
);
const isVerifiedResult = isVerified(someUser);
console.log(isVerifiedResult); // Output: Left("User is not verified")
The Profunctor
type class is the group of types that support the promap
function. This function is used to apply a function to the input and output of a
function.
This was a little harder for me to wrap my head around. I understand that it can be used to apply a function to the input and output of a function, but I'm not sure when I would use this.
Moniod
type Shape = "Circle" | "Box" | "Pyramid";
type SomeObj = {
id: number;
count: number;
group: Shape;
};
const someObj: SomeObj[] = [
{
id: 1,
count: 2,
group: "Circle",
},
{
id: 2,
count: 4,
group: "Circle",
},
{
id: 3,
count: 3,
group: "Box",
},
{
id: 4,
count: 2,
group: "Pyramid",
},
];
const byGroup = pipe(
S.Ord,
Ord.contramap((obj: SomeObj) => obj.group),
);
const byCount = pipe(
N.Ord,
Ord.contramap((obj: SomeObj) => obj.count),
);
const SomeM = Ord.getMonoid<SomeObj>();
const byBothOrds = M.concatAll(SomeM)([byGroup, byCount]);
const sortSomeObj = pipe(
someObj,
A.sort(byBothOrds),
A.map((obj) => obj.id),
);
console.log(sortSomeObj); // Output: [ 3, 1, 2, 4 ]
const sortByArray = pipe(
someObj,
A.sortBy([byGroup, byCount]),
A.map((obj) => obj.id),
);
console.log(sortByArray); // Output: [ 3, 1, 2, 4 ]
// order by first "Circle" then "Box" then "Pyramid"
const byOrdinal = pipe(
Ord.fromCompare((a: Shape, b: Shape) =>
pipe(
O.Do,
O.bind("ordinal", () => O.fromNullable(["Circle", "Box", "Pyramid"])),
O.bind("aIdx", ({ ordinal }) => A.findIndex((x) => x === a)(ordinal)),
O.bind("bIdx", ({ ordinal }) => A.findIndex((x) => x === b)(ordinal)),
O.match(
() => 0,
({ aIdx, bIdx }) => N.Ord.compare(aIdx, bIdx),
),
),
),
Ord.contramap((obj: SomeObj) => obj.group),
);
The Monoid
type class is the group of types that support the concatAll
function. This function is used to combine multiple Ord
functions into a
single Ord
function.
This can be useful when you want to sort a list of objects by multiple
properties of the object. I've done this through the use of M.concatAll
and by
A.sortBy
as well as trying out custom ordinal sorting.
ReadonlyArray
[
{
"name": "john",
"age": 21,
"classes": {
"history": {
"grade": 89,
"semester": "spring",
"category": "humanities"
},
"math": {
"grade": 95,
"semester": "all_year",
"category": "quantitative"
},
"physics": {
"grade": 81,
"semester": "fall",
"category": "quantitative"
},
"literature": {
"grade": 77,
"semester": "spring",
"category": "humanities"
}
}
},
{
"name": "amanda",
"age": 20,
"classes": {
"history": {
"grade": 95,
"semester": "spring",
"category": "humanities"
},
"math": {
"grade": 99,
"semester": "all_year",
"category": "quantitative"
},
"physics": {
"grade": 89,
"semester": "fall",
"category": "quantitative"
},
"literature": {
"grade": 65,
"semester": "spring",
"category": "humanities"
}
}
},
{
"name": "rachel",
"age": 19,
"classes": {
"history": {
"grade": 80,
"semester": "spring",
"category": "humanities"
},
"math": {
"grade": 90,
"semester": "all_year",
"category": "quantitative"
},
"physics": {
"grade": 100,
"semester": "fall",
"category": "quantitative"
},
"literature": {
"grade": 88,
"semester": "spring",
"category": "humanities"
}
}
}
]
import studentsGrades from "./data.json";
const gradesByCategory = (groupBy: string) =>
pipe(
studentsGrades,
RA.chain(({ classes }) => Object.values(classes)),
RA.filterMap(({ category, grade }) =>
category === groupBy ? O.some(grade) : O.none,
),
);
console.log(gradesByCategory("quantitative"));
const groupGrades = pipe(
studentsGrades,
RA.reduce({} as Record<string, Array<number>>, (acc, { classes }) => {
const grades = Object.values(classes);
return pipe(
grades,
RA.reduce(acc, (acc, { category, grade }) => {
const current = acc[category] ?? [];
return {
...acc,
[category]: [...current, grade],
};
}),
);
}),
Re.toEntries,
A.map<[string, number[]], [string, number]>(([category, grades]) => [
category,
pipe(
grades,
RA.reduce(0, (acc, grade) => acc + grade),
(x) => +(x / grades.length).toFixed(2),
),
]),
Re.fromEntries,
);
console.log(groupGrades);
The ReadonlyArray
type class is the group of types can not be mutated. This
helps to prevent accidental mutation of an array. When using ReadonlyArray
I I
explored how to use it group and aggregate values across an array of objects.