Higher Kinded Introduction
This post series intends to cover the features of hkts, which is a sort of port of fp-ts to the deno runtime with some spice by pelotom thrown in.
My goal with hkts is two-fold. First, to learn more about functional programming by implementing some common type classes (specifically, the ones defined in the static-land spec). Second, to put together a pragmatic, easy to read/understand library of tools based on fp-ts, io-ts, and monocle-ts that are deno native.
To help with my first goal I intend to create a blog series covering the concepts that I have learned by implementing them in hkts with some practical usage examples. This is that series.
It all starts with examples
Before diving into the nitty gritty of how hkts implements higher kinded types, how hkts handles type-class constructor lengths, or how one might implement Monad for These, I will begin at the end. This is to say, I will be starting with examples of how to use Option, Either, Task, and more. Then I will endeavor to explain what type classes like Functor, Monad, Traverse, and Monoid mean. Lastly, I'll get into the typescript abstractions that make hkts possible.
If my examples are too foreign to you, or perhaps you want a primer on what functional programming even is, I'd recommend looking through Brian Lonsdorf's Mostly Adequate Guide to Functional Programming.
That said, as an introduction, this is a little light, so just below I have a small example of how hkts can help you fetch data and be confident that the response is what you think it is.
A First Example
Fetching data is a standard practice on today's internet. Deno seeks to be compatible with modern web apis wherever possible, so it implements a browser compatible fetch right out of the box.
Well, that certainly makes fetching data easy.. but what about handling the handful of errors that can occur when we try to fetch data? Say the network is down, or we called the wrong url and got back the wrong data, or the url we called gave us json that's different from what we expected! The most fetch can do for us here is to throw an error when the network is down or the response isn't json. Following is a little example of how we can really handle all the edge cases of a fetch request.
import * as D from "https://deno.land/x/hkts@v0.0.28/decoder.ts";
import * as E from "https://deno.land/x/hkts@v0.0.28/either.ts";
import * as TE from "https://deno.land/x/hkts@v0.0.28/task_either.ts";
import { flow, pipe } from "https://deno.land/x/hkts@v0.0.28/fns.ts";
// Here we wrap fetch in a TaskEither type, and default to unwrapping a json body
const fetchTaskEither = (
url: string | Request | URL,
): TE.TaskEither<string, unknown> =>
pipe(
() => fetch(url).then((res) => res.json()),
TE.fromFailableTask(String),
);
// Here we take a Decoder (which validates data) and validate the response
// of a fetch call, drawing a pretty tree if the data is bad and otherwise
// passing a fetch error through.
const fromDecode = <A>(decoder: D.Decoder<unknown, A>) =>
flow(
fetchTaskEither,
TE.chain(flow(
decoder.decode,
E.mapLeft(D.draw),
TE.fromEither,
)),
);
// Here we create a Decoder for the data that the xkcd.com api returns
const Comic = D.intersect(
D.type({
year: D.string,
month: D.string,
day: D.string,
num: D.number,
title: D.string,
safe_title: D.string,
img: D.string,
}),
D.partial({
link: D.string,
news: D.string,
transcript: D.string,
alt: D.string,
}),
);
// Since we are using typescript, we can also pull the type of our Decoder
// instead of creating it manually
type Comic = D.TypeOf<typeof Comic>;
// This function takes a url and validates the response is a Comic
const getComic = fromDecode(Comic);
// This creates a TaskEither for the current comic xkcd api
const getLatestComic = getComic("https://xkcd.com/info.0.json");
// This creates a TaskEither against google.com, which won't return a comic so it will
// error.
const getBadUrl = getComic("https://google.com");
// Get the latest comic, validate, and print
getLatestComic().then(E.fold(console.error, console.log));
// Get some bad data from google, validate, and print
getBadUrl().then(E.fold(console.error, console.log));
It is my hope that by the end of this blog series you will understand how and why the various parts of this example work. Of course, since I'll be starting with real world examples you might prefer to just use this code. If that's your jam then you're welcome to.
To test out this code you can copy/paste it into a file called something like hkts-test.ts
and run it with deno run --allow-net hkts-test.ts
. It's fully typed and handles payload validation, json parsing problems, and no network access. If you do copy and past it, mouse over some of the terms to see how hkts keeps all of the types well defined for you.
If this type of code appeals to or confuses you, stay tuned for the next entry in the series where we explore the Either
type and how to use it. Additionally, if you're interested in hearty open source work I am accepting contributions to hkts. The core library features are all pretty much done, but there is room for dog fooding apis, types, names, and for more testing.
Top comments (0)