Mappables in typescript
michael matos
Posted on May 12, 2024
*note: I could never explain these things better than: this guy so I suggest you to go there and read this as complementary material and *
Table of contents
- Functors == mappable
- are there other functors, Maybe
- Trees can be mappables
- So graphs too
- Benefits of using functors in your code
Functors == mappable
Once upon a time in the kingdom of Functional Paradise, there lived a clever magician named Funcio. Funcio had a magical box called "The Functor" that could transform ordinary objects into extraordinary ones.
One sunny day, a young princess named Polya approached Funcio with a basket of fruits. She wanted to make these fruits more delightful, so Funcio decided to demonstrate the power of his Functor. He waved his wand and exclaimed, "Behold the magic of mapping!"
With a flick of his wrist, Funcio turned the basket of fruits into a basket of fruity delights. Each fruit was transformed into its juiciest and ripest version, making them even more delicious than before.
Princess Polya was amazed and asked Funcio, "How did you do that?"
Funcio chuckled and explained, "You see, dear princess, the Functor is like a magical function that takes ordinary things and makes them extraordinary. Just like how we transformed your fruits, functors in functional programming take values and apply a function to each of them, creating new values without changing the original ones."
// and here's the interface ("typeclass") for mappable things
interface Functor<F> { map<T, U>(fa: F, func: (x: T) => U): F; }
// and here's the example impl.
type List<T> = null | { head: T; tail: List<T> };
function cons<T>(head: T, tail: List<T>): List<T> {
return { head, tail };
}
function map<T, U>(list: List<T>, func: (x: T) => U): List<U> {
if (list === null) {
return null;
} else {
return cons(func(list.head), map(list.tail, func));
}
}
// Example usage:
function transformToFruityDelight(fruit: string): string {
return "Delightful " + fruit;
}
// Create a list of fruits
const fruits: List<string> = cons("apple", cons("banana", cons("orange", null)));
// Apply the transformation function to each fruit
const fruityDelights: List<string> = map(fruits, transformToFruityDelight);
// Print the original and transformed lists
console.log("Original list:", fruits);
console.log("Transformed list:", fruityDelights);
From that day on, Princess Polya became fascinated with functors and their magical powers. She embarked on a journey to learn more about functional programming, guided by the wisdom of Funcio the magician.
And so, in the kingdom of Functional Paradise, the tale of Funcio and his Functor spread far and wide, inspiring many to explore the enchanting world of functional programming.
Are there other functors, Maybe
Funcio, being a wise magician, knew that there were many more magical Functors beyond just lists. So, he gathered Princess Polya and his other curious apprentices for a special lesson.
Gathering them around his magical Functor box, Funcio began, "Dear friends, behold the wonders of Functors are not confined to just lists. They can take many forms, each with its own unique magic."
He then waved his wand and summoned an array of objects: trees, potions, and even a flock of birds.
"These," Funcio proclaimed, "are all Functors in disguise!"
Princess Polya and the apprentices gasped in awe.
Funcio continued, "Just as we transformed lists of fruits into a basket of delights, we can apply the Functor's magic to arrays, streams, Maybe types, and many more! Each Functor has its own way of applying transformations, but the essence remains the same: taking ordinary values and bestowing upon them the magic of functional programming."
With wide eyes, Princess Polya asked, "But how do we know which types can be Functors?"
Funcio smiled warmly, "Ah, dear princess, any type that can map a function over its values while preserving its structure can be a Functor. It's not about what it is, but how it behaves!"
And so, Funcio and his apprentices delved deeper into the world of Functors, exploring the diverse forms they could take and the magical transformations they could bring. With each lesson, they discovered new wonders and expanded their understanding of functional programming in the kingdom of Functional Paradise.
// Define Just and Nothing data types
type Just<T> = { kind: "just"; value: T };
type Nothing = { kind: "nothing" };
// Define Maybe Functor ADT
type Maybe<T> = Just<T> | Nothing;
// Define a function to wrap a value in Just
function just<T>(value: T): Just<T> {
return { kind: "just", value };
}
// Define a constant for Nothing
const nothing: Nothing = { kind: "nothing" };
// Define a function to apply a transformation over Maybe Functor
function mapMaybe<T, U>(maybe: Maybe<T>, fn: (val: T) => U): Maybe<U> {
switch (maybe.kind) {
case "just":
return just(fn(maybe.value));
case "nothing":
return nothing;
}
}
// Define a Tree ADT
type Tree = { hasFruit: boolean };
// Define a function to bear fruit
function bearFruit(tree: Tree): string {
return tree.hasFruit ? "🍎" : "No Fruit";
}
// Create some trees
const tree1: Tree = { hasFruit: true };
const tree2: Tree = { hasFruit: false };
const tree3: Tree = { hasFruit: true };
// Wrap trees in Maybe Functor
const maybeTree1: Maybe<Tree> = tree1.hasFruit ? just(tree1) : nothing;
const maybeTree2: Maybe<Tree> = tree2.hasFruit ? just(tree2) : nothing;
const maybeTree3: Maybe<Tree> = tree3.hasFruit ? just(tree3) : nothing;
// Map the bearFruit function over Maybe Functor
const result1: Maybe<string> = mapMaybe(maybeTree1, bearFruit); // Returns Just("🍎")
const result2: Maybe<string> = mapMaybe(maybeTree2, bearFruit); // Returns Nothing
const result3: Maybe<string> = mapMaybe(maybeTree3, bearFruit); // Returns Just("🍎")
// Log results
console.log(result1);
console.log(result2);
console.log(result3);
Trees can be mappables
// Define a binary tree data type
type Tree<T> = null | { value: T; left: Tree<T>; right: Tree<T> };
// Function to create a new tree node
function treeNode<T>(value: T, left: Tree<T> = null, right: Tree<T> = null): Tree<T> {
return { value, left, right };
}
// Functor interface for trees
interface Functor<F> {
map<T, U>(fa: F, func: (x: T) => U): F;
}
// Tree Functor implementation
class TreeFunctor implements Functor<Tree<any>> {
map<T, U>(tree: Tree<T>, func: (x: T) => U): Tree<U> {
if (tree === null) {
return null;
} else {
return treeNode(func(tree.value), this.map(tree.left, func), this.map(tree.right, func));
}
}
}
// Example usage:
function square(x: number): number {
return x * x;
}
// Create a sample binary tree
const binaryTree: Tree<number> = treeNode(
1,
treeNode(2, treeNode(3), treeNode(4)),
treeNode(5, null, treeNode(6))
);
// Create a TreeFunctor instance
const treeFunctor = new TreeFunctor();
// Map over the binary tree to square each node value
const squaredTree: Tree<number> = treeFunctor.map(binaryTree, square);
// Print the original and transformed trees
console.log("Original tree:", binaryTree);
console.log("Squared tree:", squaredTree);
So graphs too
type Edge<T> = {
target: Vertex<T>;
weight?: number; // Optional weight for the edge
};
class Vertex<T> {
value: T;
edges: Array<Edge<T>>;
constructor(value: T) {
this.value = value;
this.edges = [];
}
addEdge(target: Vertex<T>, weight?: number): void {
this.edges.push({ target, weight });
}
}
class Graph<T> {
vertices: Array<Vertex<T>>;
constructor(vertices: Array<Vertex<T>>) {
this.vertices = vertices;
}
}
interface Functor<F> {
map<T, U>(graph: F, func: (x: T) => U): F;
}
class GraphFunctor implements Functor<Graph<any>> {
map<T, U>(graph: Graph<T>, func: (x: T) => U): Graph<U> {
const newVertices: Array<Vertex<U>> = graph.vertices.map(vertex => {
return {
value: func(vertex.value),
edges: vertex.edges.map(edge => {
return {
target: this.mapVertex(edge.target, func),
weight: edge.weight
};
})
};
});
return new Graph(newVertices);
}
private mapVertex<T, U>(vertex: Vertex<T>, func: (x: T) => U): Vertex<U> {
return {
value: func(vertex.value),
edges: vertex.edges.map(edge => {
return {
target: this.mapVertex(edge.target, func),
weight: edge.weight
};
})
};
}
}
// Define the graph data structure, Functor interface, and GraphFunctor implementation as provided previously...
// Create vertices
const v1 = new Vertex("A");
const v2 = new Vertex("B");
const v3 = new Vertex("C");
// Add edges between vertices
v1.addEdge(v2);
v1.addEdge(v3);
v2.addEdge(v3);
// Create the graph
const graph = new Graph([v1, v2, v3]);
// Create a function to transform vertex values (e.g., converting to uppercase)
function transformToUpper(value: string): string {
return value.toUpperCase();
}
// Create an instance of the GraphFunctor
const graphFunctor = new GraphFunctor();
// Map over the graph, transforming vertex values to uppercase
const transformedGraph = graphFunctor.map(graph, transformToUpper);
// Output the original and transformed graphs
console.log("Original Graph:");
console.log(graph);
console.log("\nTransformed Graph:");
console.log(transformedGraph);
Benefits of using functors in your code
Functional programming leverages functors for various reasons, each contributing to the paradigm's elegance, composability, and safety. Here are some benefits of using functors in FP:
Abstraction: Functors provide a powerful abstraction mechanism that enables developers to encapsulate common patterns of computation. They allow us to define generic operations over different data types, promoting code reuse and modularity.
Composition: Functors support composition, allowing developers to build complex transformations by combining simpler ones. By chaining multiple functor operations together, we can express intricate computations concisely and clearly.
Laws and Guarantees: Functors come with laws that ensure their behavior is predictable and consistent. For example, the functor laws dictate that mapping the identity function over a functor should return the original functor, and composing two functor mappings should be equivalent to mapping the composition of the functions. These laws provide guarantees about functor behavior, helping developers reason about their code's correctness.
Separation of Concerns: Functors encourage separating concerns by distinguishing between data transformation and the application of specific functions. This separation enhances code clarity and maintainability by decoupling the logic of the transformation from the specifics of the operations applied.
Error Handling: Functors facilitate error handling by providing a structured way to handle failures or exceptions within computations. They allow developers to define functor-specific error-handling strategies, such as lifting errors into a functor context or mapping over functors to propagate errors.
Parallelism and Concurrency: Functors support parallelism and concurrency by enabling the application of pure functions to data in a distributed manner. By abstracting over data transformation, functors make it easier to parallelize computations without worrying about synchronization or side effects.
Type Safety: Functors promote type safety by constraining the types of data that can be transformed and the functions that can be applied. This ensures that operations are applied in a consistent and predictable manner, reducing the likelihood of runtime errors and bugs.
Interoperability: Functors facilitate interoperability between different libraries and frameworks by providing a common interface for data transformation. This enables developers to seamlessly integrate code written in different paradigms or languages, fostering collaboration and code reuse.
thank you...
FIN!!!
Posted on May 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.